Kevin Pluck

@kevpluck

Some polish

This is part 9 of an N part series detailing how I make my animations.

Prev Next

I have discovered that there is a command in Processing to plot a point instead of drawing a small ellipse like I have done! I had looked to see how Processing sets pixels which it does with the set() command but the pixels were too small. Processing also has a point() command which I disregarded as it looked the same. The difference is that point() plots a circle using strokeWeight() as the size!

strokeWeight(10);
stroke(255,255,0);
point(50,50);

Produces:

Woohoo, now back to our regularly scheduled program…

My maths teacher drummed into my head “always label your axes” so let’s do that along with the actual axes themselves.

Oh, but our scale is continuously changing. This makes it trickier but not impossible…

First we had better make some room for them, currently our plot is shown hard up against the edges of the window.

I was going to just create a margin variable and then simply add it to the x and y values but that turned out to be trickier than I thought as I then had to modify the scale algorithms in a non intuitive manner (what actually happened is that I failed to scale it correctly at all and gave up in disgust).

So a far simpler way is to pull out the code into a function and pass in the new position and the width and height. Sounds like a lot of work but it isn’t. First rename the draw() function to drawGraph(). Create a new draw() function and put in the body drawGraph(); like so:

void draw()
{
drawGraph();
}
void drawGraph()
{
...
}

Now add xPos, yPos, graphWidth, graphHeight integers to drawGraph().

void drawGraph(int xPos, int yPos, int graphWidth, int graphHeight)

I think that a margin of 60 pixels is a good starting point. So let’s make a global constant with that value. Add this line just before draw():

final int MARGIN = 60;

Marking something as final means it cannot be modified and by convention these variables are named using all caps.

Now all we do is to call drawGraph() from draw() with the following:

drawGraph(MARGIN, height - MARGIN, width - MARGIN, height - MARGIN);

So let’s start using those values in drawGraph():

First replace all uses of width and height with graphWidth and graphHeight respectively.

Then replace the ellipse() function with the newly discovered point() function:

point(xPos + x * xScale, yPos - (y * yScale));

And you’re done. Drawing with point() also seems a lot faster! Oh yeah, add strokeWeight(3) to setup() otherwise your points will be 1 pixel in size.

Ok, let’s draw some axes. Simple lines from top to bottom using MARGIN so in the draw() function:

line(MARGIN, 0, MARGIN, height);
line(0, height - MARGIN, width, height - MARGIN);

Hmm, that didn’t work first time for me. This was due to leaving background(0) in the drawGraph() function which was clearing the drawn lines. Move that to the first line of draw() and it will all work.

Result should look like this:

The line thickness is inherited from the strokeWeight command in setup(). So let’s move that to the first line of drawGraph() and add strokeWeight(1) to the first line of draw(). That will make a thin line for the axes and heavy points for the graph.

So now we need to add tick marks. As the tick marks need to be scaled to the same as the graph then they’ll need to be drawn from inside the drawGraph() function. Let’s do the x axis first by marking each year.

Yikes. This isn’t as easy as I thought it would be. Because each data point is recorded on a Saturday and we are simply multiplying our dataIndex by 7 to get the number of days from the start date to find the date to get the data it makes working out the tick marks non trivial.

After a fair bit of experimentation I was able to reduce the complexity to the following function:

void drawXAxis(int xAxisMaximum, float xScale)
{
int yPos = height - MARGIN;
float xAxisTick = 279.0 / 7.0;
int axisYear = 1958;

while(xAxisTick <= xAxisMaximum)
{
int tickLength = 5;
int xPos = MARGIN + int(xAxisTick * xScale);

if(axisYear % 5 == 0)
{
text(axisYear, xPos, yPos + 22);
tickLength = 10;
}

line(xPos, yPos, xPos, yPos + tickLength);

xAxisTick += 365.25 / 7.0;
axisYear++;
}
}

Where the xAxisMaximum starts off as the width of the plot and then as the scale increases the scaled max x value. The initial value of the xAxisTick is the number of days left in the year after 29/3/1958 (279) converted to weeks by dividing by 7.

Here I am introducing a while loop. This type of loop keeps executing while the contents of the parenthesis is true. In this case while the current position of the tick is less than the maximum x value on the axis.

The line if(axisYear % 5 == 0) makes use of the modulus operator % to make a longer tick mark and display the year every fifth year. We then simply draw a line for the tick followed by incrementing the tick position by the average number of weeks in a year.

This results in:

Which I’m rather pleased with. Here’s the complete code:

import java.time.*;
import de.looksgood.ani.*;
Ani _ani;
FloatDict _data = new FloatDict();
LocalDate _startDate = LocalDate.of(1958, 3, 29);
void setup()
{
loadData();
size(500,500);
background(0);
stroke(255,255,0);
textAlign(CENTER);

Ani.init(this);
Ani.setDefaultTimeMode(Ani.FRAMES);
_ani = new Ani(this, 1032, "_change", 3, Ani.EXPO_IN);
}
float yScale = 20.0;
boolean _coda = false;
int _codaCount = 0;
float _change = 1.0;
final int MARGIN = 60;
void draw()
{
background(0);
strokeWeight(1);

line(MARGIN, 0, MARGIN, height);
line(0, height - MARGIN, width, height - MARGIN);

drawGraph(MARGIN, height - MARGIN, width - MARGIN, height - MARGIN);
}
void drawGraph(int xPos, int yPos, int graphWidth, int graphHeight)
{
int deltaX = int(frameCount * _change);

float xScale = 1.0;

int xAxisMaximum = graphWidth;

if(deltaX > graphWidth)
{
xAxisMaximum = deltaX;
xScale = float(graphWidth) / float(deltaX);
}

drawXAxis(xAxisMaximum, xScale);

strokeWeight(3);

for(int dataIndex = 1; dataIndex <= deltaX; dataIndex++)
{

int daysFromStart = (dataIndex - 1) * 7;
LocalDate frameDate = _startDate.plusDays(daysFromStart);


if(_data.hasKey(frameDate.toString()))
{
float co2 = _data.get(frameDate.toString());

float x = dataIndex;
float y = (co2 - 313.04) ;

if(y * yScale > graphHeight)
{
yScale = float(graphHeight)/y;
}

point(xPos + x * xScale, yPos - (y * yScale));
}
}
}
void drawXAxis(int xAxisMaximum, float xScale)
{
int yPos = height - MARGIN;
float xAxisTick = 279.0 / 7.0;
int axisYear = 1958;

while(xAxisTick <= xAxisMaximum)
{
int tickLength = 5;
int xPos = MARGIN + int(xAxisTick * xScale);

if(axisYear % 5 == 0)
{
text(axisYear, xPos, yPos + 22);
tickLength = 10;
}

line(xPos, yPos, xPos, yPos + tickLength);

xAxisTick += 365.25 / 7.0;
axisYear++;
}
}
void loadData()
{
String[] lines = loadStrings("weekly_in_situ_co2_mlo.csv");

for (String line : lines)
{
if( line.startsWith("\"") ) continue;

String[] values = split(line, ',');
String date = values[0];
float co2 = parseFloat(values[1]);
_data.set(date, co2);
}
}

That was a whole lot more complicated than I was expecting. I intended this post to cover both the x and y axis along with titles etc! I guess that’ll be next week then…

More by Kevin Pluck

Topics of interest

More Related Stories