Last updated: November 2019 👉 livestreamed every last Sunday of the month. Join live or subscribe by email 💌

"lol you can become a famous artist by just painting colorful squares"

Yeah turns out generative art is really hard. And Mondrian did it manually.

Piet Mondrian was a Dutch painter famous for his style of grids with basic squares and black lines. So famous in fact, Google finds him as "squares art guy".

The signature style grew out of his earlier cubist works seeking a universal beauty understood by a all humans.

I believe it is possible that, through horizontal and vertical lines constructed with awareness, but not with calculation, led by high intuition, and brought to harmony and rhythm, these basic forms of beauty, supplemented if necessary by other direct lines or curves, can become a work of art, as strong as it is true.

So I figured what better way to experiment with D3 treemaps than to pay an homage to this great artist.

You can watch the full live stream here:

GitHub link here 👉 Swizec/mondrian-generator

And try it out in your browser

It's not as good as Mondrian originals, but we learned a lot 👩‍🎨

Introduced by Ben Shneiderman in 1991, a treemap recursively subdivides area into rectangles according to each node’s associated value.

In other words, a treemap takes a rectangle and packs it with smaller rectangles based on a tiling algorithm. Each rectangle's area is proportional to the value it represents.

Treemaps are most often used for presenting budgets and other relative sizes. Like in this interactive example of a government budget from 2016.

You can see at a glance most of the money goes to social security, then health care, which is split between medicaid, children's health, etc.

Treemaps are great for recursive data like that.

We wanted to play with treemaps per a reader's request, but couldn't find an interesting dataset to visualize. Generating data was the solution. Parametrizing it with sliders, pure icing on the cake.

Also I was curious how close we can get :)

3 pieces have to work together to produce an interactive piece of art:

A recursive rendering component A function that generates treemappable data Sliders that control inputs to the function

Treemaps are recursive square subdivisions. They come with a bunch of tiling algorithms, the most visually stunning of which is the squarified treemaps algorithm described in 2000 by Dutch researchers.

What is it with Dutch people and neat squares 🤔

While beautiful, the squarified treemaps algorithm did not look like a Mondrian.

Subtle difference, I know, but squarified treemaps are based on the golden ratio and Piet Mondrian's art does not look like that. We used the d3.treemapBinary algorithm instead. It aims to create a balanced binary tree.

const Mondrian = ( { x , y , width , height , data } ) => { const treemap = d3 . treemap ( ) . size ( [ width , height ] ) . padding ( 5 ) . tile ( d3 . treemapBinary ) const root = treemap ( hierarchy ( data ) . sum ( d => d . value ) . sort ( ( a , b ) => 0.5 - Math . random ( ) ) ) return ( < g transform = { `translate( ${ x } , ${ y } )` } > < MondrianRectangle node = { root } / > < / g > ) }

The <Mondrian> component takes some props and instantiates a new treemap generator. We could've wrapped this in a useMemo call for better performance, but it seemed fast enough.

We set the treemap's size() from width and height props, a guessed padding() of 5 pixels, and a tiling algorithm.

This creates a treemap() generator – a method that takes data and returns that same data transformed with values used for rendering.

The generator takes a d3-hierarchy, which is a particular data format shared by all hierarchical rendering generators in the D3 suite. Rather than build it ourselves, we feed our source data into the hierarchy() method. That cleans it up for us ✌️

We need the sum() method to tell the hierarchy how to sum up the values of our squares, and we use random sorting because that produced nicer results.

We render each level of the resulting treemap with a <MondrianRectangle> component.

const MondrianRectangle = ( { node } ) => { const { x0 , y0 , x1 , y1 , children } = node , width = x1 - x0 , height = y1 - y0 ; return ( < > < rect x = { x0 } y = { y0 } width = { width } height = { height } style = { fill : node . data . color , stroke : "black" , strokeWidth : 5 } onClick = { ( ) => alert ( `This node is ${ node . data . color } ` ) } / > { children && children . map ( ( node , i ) => < MondrianRectangle node = { node } key = { i } / > ) } < / > ) ; } ;

Each rectangle gets a node prop with a bunch of useful properties.

x0, y0 defines the top left corner

defines the top left corner x1, y1 is the bottom right corner

is the bottom right corner children are the child nodes from our hierarchy

We render an SVG <rect> component as the square representing this node. Its children we render by looping through the children array and recursively rendering a <MondrianRectangle> component for each.

The onClick is there just to show how you might make these interactive :)

You can use this same principle to render any data with a treemap.

We moved the generative art piece into a custom useMondrianGenerator hook. Keeps our code cleaner 😌

let mondrian = useMondrianGenerator ( { redRatio , yellowRatio , blueRatio , blackRatio , subdivisions , maxDepth , } )

The method takes a bunch of arguments that act as weights on randomly generated parameters. Ideally we'd create a stable method that always produces the same result for the same inputs, but that proved difficult.

As mentioned earlier, generative art is hard so this method is gnarly. 😇

We start with a weighed random generator.

const createColor = ( { redRatio , blueRatio , yellowRatio , blackRatio } ) => { const probabilitySpace = [ ... new Array ( redRatio * 10 ) . fill ( 'red' ) , ... new Array ( blueRatio * 10 ) . fill ( 'blue' ) , ... new Array ( yellowRatio * 10 ) . fill ( 'yellow' ) , ... new Array ( blackRatio * 10 ) . fill ( 'black' ) , ... new Array ( redRatio * 10 + blueRatio * 10 + yellowRatio * 10 + blackRatio * 10 ) . fill ( '#fffaf1' ) , ] return d3 . shuffle ( probabilitySpace ) [ 0 ] }

createColor picks a color to use for each square. It takes desired ratios of different colors and uses a trick I discovered in college. There are probably better ways to create a [weighed random method, but this works well and is something I can understand.

You create an array with the amount of values proportional to the probabilities you want. If you want red to be twice as likely as blue , you'd use an array like [red, red, blue] .

Pick a random element from that array and you get values based on probabilities.

The result is a createColor method that returns colors in the correct ratio without knowing context of what's already been picked and what hasn't. ✌️

The useMondrianGenerator hook itself is pretty long. I'll explain in code comments so it's easier to follow along :)

function useMondrianGenerator ( { redRatio , yellowRatio , blueRatio , blackRatio , subdivisions , maxDepth , } ) { let mondrian = useMemo ( ( ) => { const generateMondrian = ( { value , depth = 0 } ) => { const N = Math . round ( 1 + Math . random ( ) * ( subdivisions * 10 - depth ) ) return { value , color : createColor ( { redRatio , yellowRatio , blueRatio , blackRatio , } ) , children : depth < maxDepth * 5 ? d3 . range ( N ) . map ( _ => generateMondrian ( { value : value / N , depth : depth + 1 , } ) ) : null , } } return generateMondrian ( { value : 100 , } ) } , [ maxDepth , subdivisions ] ) const updateColors = node => ( { ... node , color : createColor ( { redRatio , yellowRatio , blueRatio , blackRatio , } ) , children : node . children ? node . children . map ( updateColors ) : null , } ) mondrian = useMemo ( ( ) => updateColors ( mondrian ) , [ redRatio , yellowRatio , blueRatio , blackRatio , subdivisions , maxDepth , ] ) return mondrian }

And that creates mondrian datasets based on inputs. Now we just need the inputs.

Thanks to React Hooks, our sliders were pretty easy to implement. Each controls a ratio for a certain value fed into a random data generation method.

Take the slider that controls the ratio of red squares for example.

It starts life as a piece of state in the <App> component.

const [ redRatio , setRedRatio ] = useState ( 0.2 )

redRatio is the value, setRedRatio is the value setter, 0.2 is the initial value.

Render the slider as a <Range> component.

< Range name = "red" value = { redRatio } onChange = { setRedRatio } / >

Value comes from our state, update state on change.

The <Range> component itself looks like this:

const Range = ( { name , value , onChange } ) => { return ( < div style = { display : "inline-block" } > { name } < br / > < input type = "range" name = { name } min = { 0 } max = { 1 } step = { 0.1 } value = { value } onChange = { event => onChange ( Number ( event . target . value ) ) } / > < / div > ) ; } ;

HTML has built-in sliders so we don't have to reinvent the wheel. Render an input, give it a type="range" , set value from our prop, and parse the event value in onChange before feeding it back to setRedRange with our callback.

Now each time you move that slider, it triggers a re-render, which generates a new Mondrian from the data.

In conclusion: generative art is hard, Piet Mondrian was brillianter than he seems, and D3 treemaps are great fun.

Hope you enjoyed this as much as I did :)

Cheers,

~Swizec