Kevin Pluck

@kevpluck

More polish

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

Prev Next

Last week I managed to show you how to render the x-axis which took a bit more effort than I was expecting so hopefully the y-axis won’t be so challenging especially seeing as it is a simple scalar value.

The y-axis is the concentration of CO2 measured in PPM (parts per million) with a range of about 310–400 PPM.

As an aside; to comprehend those figures here’s an image of what 400 PPM looks like:

First thing I’ve noticed about working out the y-axis is that unlike the x-axis where we know the maximum x value ahead of rendering as it’s simply a time variable we don’t know the maximum y value until it’s been extracted from the data and scaled. Not a problem, we simply draw the y-axis after the values have been plotted.

First of all we need a variable to record the maximum CO2 value:

float co2Max = 0.0;

Pop that just before the for loop in drawGraph().

Just after where the CO2 value is extracted in the for loop add:

if(co2 > co2Max) co2Max = co2;

After the for loop add:

drawYAxis(co2Max, yScale);

That’ll get red squigglies underneath it as we haven’t created drawYAxis() yet. So let’s do that just after drawGraph():

void drawYAxis(float co2Max, float yScale)
{

}

I think a tick mark starting at 320PPM showing every 10PPM should suffice so let’s set the start value:

float co2Tick = 320.0;

Now while co2Tick is less than co2Max we want to draw a line for the tick mark and label it with the value, then increase it by 10:

while(co2Tick <= co2Max)
{
float yAxisTickPos = height - MARGIN - (co2Tick - 313.04) * yScale;
 line(MARGIN, yAxisTickPos, MARGIN - 5.0, yAxisTickPos);
text(int(co2Tick), MARGIN - 15, yAxisTickPos+3);

co2Tick += 10.0;
}

Remember as the y axis is flipped for computers we need to subtract values from the height. Hmm, 313.04 is the minimum CO2 level from the data, perhaps should make that obvious by creating a global constant:

final float CO2MIN = 313.04;

That’s better:

float yAxisTickPos = height - MARGIN - (co2Tick - CO2MIN) * yScale;

The line co2Tick += 10.0 is a shorthand way of incrementing a variable by 10. Equivalent to co2Tick = co2Tick + 10.0.

Let’s give that a whirl:

That’s pretty good, but, it looks like we need to set the initial max CO2 so we get some tick marks to start with. The initial max CO2 will have to be the CO2 value that would be at the top of the graph. Pop this code just before we call drawYAxis():

if((co2Max - CO2MIN) * yScale < graphHeight) 
co2Max = graphHeight / yScale + CO2MIN;

Resulting in:

Much better.

Now we need to label the axes. The x axis is straight forward:

text("Year", MARGIN + graphWidth / 2, height - 20);

But the y axis needs to be rotated. Brace yourself as this involves some matrix manipulation. Don’t worry, we aren’t going to be multiplying matrices or doing dot products (although that would be fun). We are simply going to use these two functions: translate() and rotate().

If you imagine a grid overlaying our drawing space with the origin sitting in the top left corner then calling translate(x,y) shifts the origin along with the whole grid to your new location while rotate(radian) rotates the whole grid about the origin. After calling these matrix manipulation functions all following drawing functions will plot their points on the modified grid.

This means that when we draw some rotated text we have to rotate the grid back again. Ugh. Thankfully Processing has a simple solution to that; it’s the matrix stack. (Not sure what a stack is? Here we go.) It has two commands; pushMatrix() and popMatrix(). Basically pushMatrix() remembers the current orientation of the grid so you can manipulate it how you want and then popMatrix() restores it. Because it’s a stack you can call those methods repeatedly as long as a popMatrix() call is paired with a preceding pushMatrix().

So just before drawYAxis() add:

pushMatrix();
translate(0, graphWidth / 2);
rotate(-PI / 2.0);
text("CO₂ concentration (PPM)", 0, 20);
popMatrix();

When working with radians (the best unit of angles of course) I like to work with fractions of PI as it’s easier to comprehend as all you need to remember is that 180° is PI, 90° is PI / 2, 45° is PI / 4 etc.

After titling the main graph we get:

And the now rather long and 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, 530, "_change", 6, Ani.EXPO_IN);
}
float yScale = 20.0;
boolean _coda = false;
int _codaCount = 0;
float _change = 1.0;
final int MARGIN = 60;
final float CO2MIN = 313.04;
void draw()
{
// The following pauses the animation before exiting
if(_change >= 6.0)
{
if(!_coda)
{
_coda = true;
_codaCount = frameCount;
}
if(frameCount - _codaCount > 120)
{
exit();
}
return;
}

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)
{
text("Atmospheric CO₂ concentration 1958 - 2017\nR. F. Keeling, S. J. Walker, S. C. Piper and A. F. Bollenbacher\nMauna Loa, Observatory, Hawaii", MARGIN + graphWidth / 2.0, MARGIN);
int deltaX = int(frameCount * _change);

float xScale = 1.0;

int xAxisMaximum = graphWidth;


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

text("Year", MARGIN + graphWidth / 2, height - 20);
drawXAxis(xAxisMaximum, xScale);

strokeWeight(3);

float co2Max = 0.0;

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());

if(co2 > co2Max) co2Max = co2;

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

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

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

if((co2Max - CO2MIN) * yScale < graphHeight)
co2Max = graphHeight / yScale + CO2MIN;

pushMatrix();
translate(0, graphWidth / 2);
rotate(-PI / 2.0);
text("CO₂ concentration (PPM)", 0, 20);
popMatrix();

drawYAxis(co2Max, yScale);
}
void drawYAxis(float co2Max, float yScale)
{
float co2Tick = 320.0;
strokeWeight(1);

while(co2Tick <= co2Max)
{
float yAxisTickPos = height - MARGIN - (co2Tick - CO2MIN) * yScale;

line(MARGIN, yAxisTickPos, MARGIN - 5.0, yAxisTickPos);
text(int(co2Tick), MARGIN - 15, yAxisTickPos+3);

co2Tick += 10.0;
}
}
void drawXAxis(int xAxisMaximum, float xScale)
{
int yPos = height - MARGIN;
float xAxisTick = 279.0 / 7.0;
int axisYear = 1959;

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);
}
}

Next week I’ll show you how to record videos and generate gifs along with hosting them on the internet.

More by Kevin Pluck

Topics of interest

More Related Stories