Money for nothing, treemaps for free (maps)

Despite the similar name, treemaps bear little visual resemblance to the tree layout we used earlier; instead, they divide a tree into rectangular regions. This requires us to have a dimension to our data; in this case, we'll size the treemap regions based on screen time, nesting each region into its parent. As such, the size of each parent will be the sum of its children, plus its own value.

This is all going to start looking really similar, so we will start writing all of our common functions now. In common/index, add this function, which will give us increasingly deeper gradiations based on a hierarchy:

 export const descendantsDarker =  (d, color, invert = false, dk = 5) => 
d3.color(
color(
d.ancestors()[d.ancestors().length - 2].id.split(' ').pop()
) )[invert ? 'brighter' : 'darker'](d.depth / dk);

This takes a datum, a color scale, and then three options: a Boolean to make the scale go brighter instead of darker, a numerical multiplier for the strength of the effect, and a string denoting the property that contains the ID field, defaulting it to itemLabel.

Brilliant! Let's do the standard set up we did in the last few charts. Go to lib/main.js and comment out the last westerosChart.init() call while adding this new one:

westerosChart.init('treemap', 'data/GoT-lineages-screentimes.json');

Navigate back to chapter6/index.js and add this:

westerosChart.treemap = function Treemap(_data) { 
const data = getMajorHouses(_data);
const stratify = d3.stratify()
.parentId(d => d.fatherLabel)
.id(d => d.itemLabel);

const root = stratify(data)
.sum(d => d.screentime)
.sort(heightOrValueComparator);

const cellPadding = 10;
const houseColors = color.copy().domain(houseNames(root));
}

This does everything we did before, except now we sort by the heightOrValue comparator we created when I had you paste those empty functions into common/index. If you need a refresher, all it does is this:

(a, b) => b.height - a.height || b.value - a.value

This means that it works regardless of whether a height or a value is supplied. We also get the sum of each person's screentime and also all of their children's screentime. You must sort and sum your root before passing it to your treemap layout generator; otherwise, it won't know how to calculate the size of the regions.

We also define a constant for our cell padding (which we set to 10 because I think it helps to have really big region borders, given this dataset) and create a new color scale just for house names. We need to have two color scales here because otherwise our legend will get really big and unwieldy -- we just want it to display the categorical colors associated with each house.

Next, we add our treemap layout generator:

  const layout = d3.treemap() 
.size([
this.innerWidth - 100,
this.innerHeight,
])
.padding(cellPadding);

This should look pretty similar to the other ones. We just add our cell padding and make it a hundred pixels less wide so that we have room for the legend.

We now call our layout on our data, which adds the relevant properties to it; this mutates our hierarchy, so we don't need to assign the result to anything:

layout(root);

I know, the functional programmer in me is crying on the inside too.

Now, we draw all the nodes, which are just rectangles:

  const nodes = this.container.selectAll('.node') 
.data(root.descendants().slice(1))
.enter()
.append('g')
.attr('class', 'node');

nodes.append('rect')
.attr('x', d => d.x0)
.attr('y', d => d.y0)
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => descendantsDarker(d, color, true, 3));

Since we don't need the root node in this instance, we slice it off when we call root.descendents(). We then append a new group for each node. Then, to each of those, we append a rectangle, setting its positioning and sizing accordingly. We then use our newly created descendantsDarker function to give us a sequence of similar colors.

Here's both the legend and tooltip in one go:

  this.container 
.append('g')
.attr('id', 'legend')
.attr('transform',
`translate(${this.innerWidth - 100}, ${cellPadding})`)
.call(legend.legendColor().scale(houseColors));

nodes.call(tooltip(d => d.data.itemLabel, this.container));

Click on save and we're done with our third chart. We're now halfway there!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset