Sol LeWitt’s Wall Drawings 852 and 853 with D3.js

I had an insight while going through the D3.js documentation to find a solution to another piece: d3.area() with a highly curved fit line isn’t that different from wall drawings 852 and 853! By the way, 852 and 853 are actually distinct pieces, though they are often grouped together into one display like the one at MASSMoCA. I worked on the solution to 852 first, then modified it to make 853:

Wall Drawing 852: A wall divided from the upper left to the lower right by a curvy line; left: glossy yellow; right: glossy purple.

Wall Drawing 853: A wall bordered and divided vertically into two parts by a flat black band. Left part: a square is divided vertically by a curvy line. Left: glossy red; right: glossy green; Right part: a square is divided horizontally by a curvy line. Top: glossy blue; bottom: glossy orange.

Here are previews of 852 and 853 respectively:

Open my D3 implementation of Wall Drawing 852 in a new window →

Open my D3 implementation of Wall Drawing 853 in a new window →

They are also available as blocks: 852 853

The underlying data set

So much of implementing these drawings with D3.js rely on making a solid, usable data set to join elements with. (After all, D3 stands for data-driven-documents.) Creating these data sets is where I spend most of my time. Once I’ve created them, everything else follows pretty quickly.

Since d3.area() makes an area graph with given points, I had to create a data set of points. I wanted the start and end points to be defined with random (but constrained) points in the middle. Here is what I came up with for 852:

var ww = window.innerWidth, wh = window.innerHeight; function getRandomArbitrary(min, max) { return Math.random() * (max - min) + min; } function lineData() { var data = new Array(); for (var point = 0; point < 5; point++) { var x = getRandomArbitrary(ww/10, ww-ww/10); // Constrains within the middle 80% var y = getRandomArbitrary(wh/10, wh-wh/10); data.push({ x: x, y: y }) } // Starting point upper left data.push({ x: 0, y: 0 }); // End point lower right data.push({ x: ww, y: wh }) return data; }

For 853, things were a little trickier since the left is oriented vertically:

var ww = window.innerWidth, wh = window.innerHeight - 80, halfwidth = ww/2 - 60; function getRandomArbitrary(min, max) { return Math.random() * (max - min) + min; } function fillLeft() { var data = new Array(); for (var point = 0; point < 3; point++) { var x = getRandomArbitrary(halfwidth/10, halfwidth-halfwidth/10); // Keeps within the middle 80% var y = getRandomArbitrary(wh/10, wh-wh/10); data.push({ x: x, y: y }) } data.push({ x: halfwidth/2, y: 0 }); data.push({ x: halfwidth/2, y: wh }) return data; } function fillRight() { var data = new Array(); for (var point = 0; point < 3; point++) { var x = getRandomArbitrary(halfwidth/10, halfwidth-halfwidth/10); // Keeps within the middle 80% var y = getRandomArbitrary(wh/10, wh-wh/10); data.push({ x: x, y: y }) } data.push({ x: 0, y: wh/2 }); data.push({ x: halfwidth, y: wh/2 }) return data; }

d3.area()

One of the most important things to note about d3.area() is that you must sort your data sets (by x if the chart is horizonal, y if vertical) before passing them to the function. Otherwise you end up with something like this because the values are out of order:

D3 has selection.sort() built in, but you also need to pass a function telling it how to sort:

.sort(function(a,b) { return +a.x - +b.x; }) // Sorting by x ascending .sort(function(a,b) { return +a.y - +b.y; }) // Sorting by y ascending

Most area charts tend to be horizontal, and nearly all examples you can find are structured in this way. You need to pass .x, .y0, and .y1:

var area = d3.area() .x(function(d) { return d.x; }) .y0(window.innerHeight) .y1(function(d) { return d.y; }) .curve(d3.curveBasis);

If you are switching to a vertical orientation, you instead need to pass .y, .x0, and .x1:

var areaLeft = d3.area() .x0(0) .x1(function(d) { return d.x; }) .y(function(d) { return d.y; }) .curve(d3.curveBasis);

Curves and Points

I played with a number of curve functions and a number of points to see how they compared to traditional interpretations of Sol LeWitt’s pieces:

curveCatmullRom

curveCardinal

curveBasis

curveNatural

curveLinear

I eventually settled on curveBasis for both pieces. It creates the smoothest curving line with the given random points, which produces results similar to the curved lines in Sol’s earlier work. curveCatmullRom was my second choice, but occasionally produced harsh concave regions.

For the larger standalone piece (852) I used 7 total points, including the end points. For 853, which consists of two different curves, I used 5 points each since the widths were smaller.

Rebuilding on screen resize

When you resize the screen, the divs gets destroyed and then rebuilt based on the new screen size. Here is the function I use to handle that. It is the same thing I used on 86 and on my Jekyll posts heatmap calendar. I wrapped the D3 instructions in its own function, called that function on first load, then wrote a function to destroy the node divs when the screen is resized and reexecute the D3 instructions after the screen resize ends. I’m sure it can be done with regular javascript, but jQuery makes this kind of thing fast and easy:

// run on first load sol852(); $(window).resize(function() { if(this.resizeTO) clearTimeout(this.resizeTO); this.resizeTO = setTimeout(function() { $(this).trigger('resizeEnd'); }, 500); }); //resize on resizeEnd function $(window).bind('resizeEnd', function() { d3.selectAll("div.node").remove(); sol852(); });

Tools Used

D3.js – The D3.js library is well-suited to Sol LeWitt’s works. D3’s foundational principle of joining data to elements is easy to apply to LeWitt’s drawings.

jQuery – I’m using jQuery to detect changes in the window size and trigger removing & rebuilding the visualization.

Inspiration and References

Wall Drawings 852 and 853 at MASSMoCA

Page 210-211 in Sol LeWitt: 100 Views