Update Bound Data Without Replacing It in D3
I wanted to amend data I had bound to dom elements using D3.js.11 The general idea was to first render a world map by binding the geography data of countries to svg paths, and then visualise some measure (like average height) by colouring the countries based on the value (a visualisation type known as a choropleth, by binding the value and fill colour to the paths in addition to the original geography data.) Either I suck at searching, or I’m one of very few people who want to do this, because I tried a number of search queries, including how to join two datasets (think of a database join), how to zip two datasets together, and so on. Nothing.
I did, however, come across something close: the D3 nest
datatype (which is
sort of a GROUP BY
type thing, to people coming from sql) and it gave me an
idea.
The objects which you want to merge share some similarity. 22 Otherwise how would you know which objects to merge? If we can create a large array of all objects, and then group it by that very same similarity measure, then we can merge all groups into a single object, and we get what we want.
Each individual component is reasonably simple:
- Assume the user supplies some sort of key function that will extract the parts that should be the same for two objects to be merged.
- Create a D3
nest
object that will group by this key function:d3.nest().key(keyfn)
- Combine33 Optionally, you can let the user specify a combination function,
which tells you how to create new objects from the ones grouped together.
all objects that are in the same group:
objs => Object.assign.apply({}, objs)
. - Create an array containing both the currently bound data and the new data,
assuming this function is called on the selection we’re interested in:
this.data().concat(data)
. - The result of this will still be grouped by the original key, except all
groups will only have a single member in it: the combined object. In order to
lift this up to the top level again, we’ll simply
map(d => d.value)
, replacing each group with its contained object.
All in all, the following can be put into a JavaScript file that is included
after D3. It will add the updateData
function to the D3 selection
prototype,
so you can use it as a drop-in replacement for the data()
binding method, on
any of your selections. Quite neat!
(function() { 'use strict'; /* Take whatever is currently bound to selection and * update it with more data without replacing whatever * was already bound. */ d3.selection.prototype.updateData = updateBinding; function updateBinding(data, keyfn, zipfn) { if (data === undefined) return this; if (keyfn === undefined) keyfn = obj => obj.toString(); if (zipfn === undefined) zipfn = os => Object.assign.apply({}, os); return this.data( d3.nest() /* Group by id attribute */ .key(keyfn) /* Then recombine by zipping children together */ .rollup(zipfn) /* Use currently bound and new data */ .entries(this.data().concat(data)) /* Ungroup again */ .map(d => d.value) ); } })();
If you don’t want to modify the selection
prototype for political reasons, you
can always updateBinding.bind(yourSelection, data, keyfn)
instead.