Skip to content
This repository was archived by the owner on May 25, 2020. It is now read-only.
Thijs edited this page Jan 30, 2020 · 4 revisions

Bubbles with D3

Enter, Update, Exit

One of d3's core concepts are datajoins and selections or the enter, update, exit cycle.

This was a quite obscure concept, which took some time to understand.

Some caveats I came to understand:

  • Update is not a function in d3
  • The update selection doesn't exist: it's the overlap of the enter-exit selections which are virtual
  • The order isn't necessarily enter, update, exit

The update function

There are no bubbles when the page loads for the first time, so the first time all datapoints/bubbles are in the enter selection.

When a filter becomes active, de bubble chart needs to update: some need to be removed, some need to be updated, some need to be added.

I've written both the first time drawing and the updating in one function.

Filtering

The data is filtered within D3. Using D3-event, I know the value of the filter. Which is saved. The first time the page loads there is no d3-event however, so I've set a default.

	let key = d3.event ? d3.event.target.name : undefined; // (1)
	let value = d3.event ? d3.event.target.value : 'All'; //(2)

	if(value !== 'All') { // (3)
		data = data.filter((datum) => { // (4)
			if (value === 'undefined') return datum[key] === undefined; // (5)
			else return datum[key] && datum[key].has(value); // (6)
		});
	}
  1. Key is the type of filter. (in this case only one: academic titles)
  2. Value is the value of the filter (e.g doctor, professor)
  3. If the value is al, there is no need to filter.
  4. Open a filter on the data array
  5. Checks if the value is a string saying undefined and returns the undefined filter condition
  6. Returns the filter condition by checking if the key exists and it has the value requested.

Data packing and Hierarchy

Next step is to get D3 to calculate the size and position of the bubbles. I already discussed this during functional-programming, which can be found on its wiki

The more, the merrier

Next up is the data join. This joins the data with te bubbles (whether the bubbles exist or not).

let bubble = bubbles.selectAll('g')
		.data(pack(h).leaves(), // (1)
			d => d.data.id); // (2)
  1. After packing, the data has the size(radius) and position (x, y) calculated. A bubble chart can have multiple levels(which uses .nodes() method), but this one is flat and only has the lowest levels, which is called its leaves, and therefore uses the .leaves() method.
  2. Keying function, this requires a bit more explanation, see the next paragraph

That's the key

D3 is pretty smart. It binds data to the elements and therefore knows wich to update, which to remove and which to add.

It does this using a key(function). It binds the data using it's position in the data array(it's index). However this is not a reliable way in our chart. The data is filtered on update, which means the indexes won't match with the already existed bubbles.

When joining data, d3 allows a key function as argument. This function tells d3 which property from the data to use as key. This has to be something unique for each datapoint. In our case it's an ID.

d => d.data.id  // returns the ID of this datum and d3 uses this as key

Please exit the chart in an orderly fashion

The fist thing to do is remove the bubbles that aren't needed anymore.

This only happens if the chart is updated. During the first time drawing, the exit selection is empty and nothing happens with it.

let bubbleExit = bubble.exit(); // (1)
	bubbleExit.select('.bubbles').transition(t).attr('r', 0).attr('fill-opacity',0); // (2)
	bubbleExit.transition(h).delay(700).remove(); //(3)
  1. The exit selection is caputred here. This is for easy reference.
  2. First we wil animate/transition the removal. De opacity and size go to zero with a transition.
  3. When the animation is done, the .remove() method, removes the elements from the DOM

Get your updates

Next up are the bubbles that change and will have their values updated. It updates the attributes and uses a transition to do this

This again only happens if the chart is updated. During the first time drawing, there are no updates

	bubble.transition(t).attr('transform', d => `translate(${d.x},${d.y})`);
	bubble.select('.bubbles').transition(t).attr('r', d => d.r);

Come on in

Last is the enter selection. These are all new entries that need to be added to the DOM

let bubbleEnter = bubble.enter() // (1)
		.append('g')
		.classed('g', true) // (2)
		.attr('id', d => d.data.id) // (3)
		.attr('transform', d => `translate(${d.x} ${d.y})`); // (4)

	bubbleEnter.append('circle') // (5)
		.classed('bubbles', true) // (6)
		.attr('r', 0) // (7)
		.transition(t) 
		.attr('r', d => d.r);
  1. Here we capture the enter selection. This now holds all the entries that need a new DOM element.
  2. A new SVG group element with a class 'g' is added for every entry.
  3. The ID of the data entry is also added(this helps d3 with the key function)
  4. The position for the group is added.
  5. The group needs a circle SVG element as child, so we use the group enter caputure to append a circle
  6. We give a class bubbles to the new circle
  7. We set the size of the bubble to 0, this is it's starting size, we add a transition and then it's final size (calculated by D3)

The order

The order of my function is exit, update, enter. This makes more sense, since first the old bubbles need to be removed, using a transition. The second thing to do is update the remaining bubbles with a transition and at last transition the new bubbles in.

Doing this in any order order would look a bit messy.

Hey, listen up

After creating the update function in D3, it is now time to call it. This happens obviously on the first page load, but also when a filter becomes active. D3 makes it very easy to set event listeners on the chart.

let filters = d3.selectAll('.filter-radiobuttons');
	filters.on('change', () => update(data));

We select all filters and let it listen for the change event, when this happens the update function is called again. This time the data passes trough the filter so that there is only the data the user wanted to so. The chart is updated accordingly

Dragging vs Zooming

A user should be able to drag the bubble-chart around to view all the bubbles.

I assumed the d3-drag library was the one to use, since a user drags around the bubble-chart. However this is for dragging individual parts of a chart(eg the bubbles). While technically it could still be used for dragging the entire bubble-chart, it was not the best fit for this feature.

The d3-zoom library is a better fit. It is used to zoom and pan a chart. In the concept I called it drag the bubble-chart around, but it should have been pan the chart around. I also needed the zoom features from this library

Adding a zoom and pan

I need to create a listener from for the zoom event. Which I saved into a const called zoom. This way I can call it when needed.

let bubbleChart = d3.select('#bubble-chart');

const zoom = d3.zoom()
		.scaleExtent([1, 1.5]) // (1)
		.on('zoom', translate); // (2)

bubbleChart.call(zoom); // (3)
  1. This method sets the boundaries for the zoom. It tells d3 to not zoom in more then 1.5 times and less then 1 time.
  2. This is the event listener that calls the translate function when a zoom is detected.
  3. This 'binds' the zoom to the select DOM element which is the group with al the bubbles

This is the translate function that the event listener uses.

function translate() {
	let transform = d3.event.transform; // (1)
	let translate = transform.translate(0, 0); // (2)

	d3.select('#bubbles').attr('transform', translate); // (3)
}
  1. This is the event capture from d3. It contains al kinds of stuff we only need the transform property.
  2. This is the translate method. This method calculates the new translate properties using the scaleExtent from the listener. D3 is very clever and calculates the difference automagically, so the 0,0 in the method is always its current position.
  3. The new position is set on the chart.

The filters

I've used vanilla javascript to create the filters, but after figuring out the above, I became aware of the ability to also to this with D3. This became a extra task that I haven't done yet.

Clone this wiki locally