Hours of Dark

This post is from the talented brunoimbrizi, if you're interested in posting, you can open up a proposal, just like he did!

In this tutorial, we are going to recreate the look of a print by Accept & Proceed called Hours of Dark 2011. All the strokes in the grid represent a day, their width proportional to the hours of darkness and their orientation defined by the angle of the sunset.

Here is our initial setup with a single <canvas> element, using window.devicePixelRatio to scale it on retina screens.

1
2
3
4
5
6
7
8
9
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');

var size = window.innerWidth;
var dpr = window.devicePixelRatio;
canvas.width = size * dpr;
canvas.height = size * dpr;
context.scale(dpr, dpr);

We need a few variables to describe the grid. First, the number of strokes which is the same as the number of days in a non-leap year. Then the number of columns and rows, just enough to pack 365 cells in the grid. In this case 23×16 = 368, so the last 3 cells will be blank.

10
11
12
13
var cols = 23;
var rows = 16;
var days = 365;

We also need variables to define the dimensions of the grid, which has a landscape aspect. And from that we can calculate the dimensions of each cell and the top and left margins.

14
15
16
17
18
19
20
var gridw = size * 0.9;
var gridh = size * 0.7;
var cellw = gridw / cols;
var cellh = gridh / rows;
var margx = (size - gridw) * 0.5;
var margy = (size - gridh) * 0.5;

Now we have enough to start drawing the strokes. We are going to loop over days column by column, so first, all the rows in the first column, then all the rows in the second column and so on.

x and y depend on the margins and on the cell dimensions. The width and height of each stroke (w, h) are arbitrary values which look right for the size of our canvas on the page. The rectangles are drawn from the centre, so this will be their anchor point when we rotate them later.

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
for (let i = 0; i < days; i++) {
  var col = Math.floor(i / rows);
  var row = i % rows;

  var x = margx + col * cellw;
  var y = margy + row * cellh;
  var w = 2;
  var h = 30;
  
  context.save();
  context.translate(x, y);

  context.beginPath();
  context.rect(w * -0.5, h * -0.5, w, h);
  context.fill();

  context.restore();
}

At this point, the grid is a bit offset to the left and to the top, and this is because x and y are the top-left of each cell. So we need one more call to translate to place the origin at the centre of the cell. These separate calls will come in handy later.

32
33
context.translate(cellw * 0.5, cellh * 0.5);

Now we need to calculate the rotation. In the original calendar, the authors used data from the angle of the sunset (known as the Azimuth) to determine the orientation of the lines. Luckily for us, we can get pretty close with a sine curve, thanks to the smooth wobble of our planet.

The trick here is to use two angles.

The first one phi determines the range of rotation in a year, which is between 0 and Π.

The second one theta modulates the first one with a sine curve, so instead of rotating a full 180°, it eases in and out of the first half of that angle and then eases back into 0°.

34
35
36
37
38
39
var phi = (i / days) * Math.PI;
var theta = Math.sin(phi) * Math.PI * 0.5;

context.rotate(theta);
 

To match the look, we need to nudge the inital angle. We also need to adjust the rotation range so it’s a little bit less than 90°.

35
36
var theta = Math.sin(phi) * Math.PI * 0.45 + 0.85;

The thickness of the strokes is proportional to the hours of darkness in each day. Again, we can get away with a good approximation using a cosine curve.

39
40
41
42
43
var scale = Math.abs(Math.cos(phi)) * 2 + 1;

context.scale(scale, 1);
 

The last step is to apply a clipping mask so each stroke is only drawn inside its cell. This is where our separate calls to translate come in handy, so we need to insert the next chunk of code in between those calls.

32
33
34
35
36
37
 
context.beginPath();
context.rect(0, 0, cellw, cellh);
context.clip();
 

And there we have it! Simple and elegant. A beautiful piece of data visualisation from Accept & Proceed which we managed to recreate with some sine and cosine curves.

↪ You can edit the code above, and have it run by pressing the arrow between the code and demo, but if you like, you can also play around a little with this code