D3.js Crash Course¶
D3 is a low level visualization library.
Lots of libraries build on top of it: Vega, Highcharts, Chart.js, and more all use D3 to build their charts out of SVG primitives. These higher level libraries focus on providing an easier to use interface, tailored to a limited number of use cases. Most offer a limited selection of supported charts: bar charts, scatterplots, etc.
D3 likely won't feel familiar to you coming from those or other visualization librareis. It operates on a completely different paradigm and will require substantially more code to accomplish similar results. Its usefulness comes from the fact that it allows total control every single item placed on the screen, so there is no real limit to waht can be accomplished with its primitives.
This chapter serves as a crash course on the core ideas required to become productive with D3:
- Element Modification via chained functions/function passing
- Data Binding
- Scale Functions, as a means of introducing the rest of D3's extensive library
If you'd like to dive deeper, I recommend D3 in Depth which expands upon all of these concepts and others not introduced here.
Element Modification¶
At the lowest level, D3 is a library for modifying elements in SVG and/or HTML.
Example
Passing Functions¶
The second parameter to functions like style and attr can be literal values (like "red" above) or functions.
It is common to use anonymous/arrow functions, but named functions work just as well, and are preferred if a function is being re-used.
The function will be passed two arguments, commonly known as d and i so I'm following convention here and using those names.
dis the bound data. We'll return to that concept later.iis the index of the element in the group.
So the above code sets each circle to red, and assigns each an increasing radius based on the position in the list.
Other Modification Methods¶
In addition to style and attr, there are methods to set classes, content, and various other properties.
Example
Appending Elements¶
In practice, instead of having pre-defined SVG elements, we'll use d3 to add them.
This time we'll start with an empty div:
Example
Data Binding¶
If we had static data, we could use a combination of built-in JavaScript methods and d3.append and build any SVG we want. It may be tedious, but the tools we have are enough to build any kind of static graphic.
In practice, especially with interactivity, we often want to "bind" specific SVG elements to the data they created, so that future modifications can leverage that data.
Remember that d parameter that our functions took?
We will use .data and .join to bind data to a given selection. We can then access the bound data via the d parameter.
Let's take a look:
Example
Data Joins Explained¶
The first line creates an array of hues. We'll see in the next example that we use any data, here we just create a list of evenly-spaced numbers. (d3.range works much like Python's range here.)
The format for a data join is
One step at a time:
- We grab the element we're appending to.
- Select all existing elements of this type (this seems redundant but is important for when we come back and animate things).
- Create a data element.
- Join that data element to the type we're creating, effectively creating
array.lengthinstances ofelement_type.
Resources
- https://bost.ocks.org/mike/join/ - The quintessential essay on this idea from the author of D3. Uses slightly older d3 syntax (
enter,exit) but core ideas are the same.
With this in mind, let's look at another example:
Example
Exercise¶
- First, move the text above the baseline by 2px or so.
- Then, change the code so that the label is at the end of the line instead of the beginning.
Scale Functions¶
In the above code we had the function
This is a common pattern: taking a ratio of some values, and then multiplying them by the size of an axis (or other range such as 360 degrees for a pie chart or hue).
For this reason, d3 gives us a set of functions that return this kind of scale functions.
Let's take a look:
Example
Now you can replace:
with:
// because of the way JS functions handle arguments, the i is optional & can be omitted
.attr("x2", d => riverScale(d.lengthKm))
Scale Types¶
While that only saved us a bit of typing, there are many more scale function factories, appropriate for different types of data:
Continuous -> Continuous
- scaleLinear
- scaleSqrt
- scalePow
- scaleLog
- scaleTime
- scaleSequential
clamp=true*
If you are outside of the specified domain, the function will extrapolate by default.
If you'd like it to instead clamp, add .clamp(true)
Continuous -> Discrete
- scaleQuantize (TODO: demo)
- scaleQuantile
- scaleThreshold
Discrete -> Discrete
- scaleOrdinal
- scalePoint
- scaleBand
Resources: - https://www.d3indepth.com/scales/ - https://d3js.org/d3-scale
Interaction¶
D3 has helpers for attaching event handlers.
Unlike addEventListener, d3 will replace existing event listeners if called twice with the same event type.
function eventDebug(e) {
// log event when fired, helpful for debugging
console.log(`event fired => ${e.type} on ${e.target.tagName.toLowerCase()}#${e.target.id}`, e);
}
d3.select("#the-button").on("click", eventDebug);
d3.select("#the-select").on("change", eventDebug)
Animation in D3¶
In D3 we will achieve animation by:
- moving our data join logic into a method we can call (e.g.
update()) - applying
transition()anddelay()calls as appropriate to ease/slow down transitions - using enter/update/exit parameters to
join()to treat different elements differently - using
ease()andtween()for more precise control of the animation
Then, interaction can be achieved by:
- binding a function to an event (timer, button click, etc.)
- using that function to update the data associated with a data join
- calling our
update()function as defined above
Demo: https://www.d3indepth.com/transitions/
Putting it all Together
Common D3 Modules¶
In addition to d3.scale, there are a few other modules you are likely to use that provide helper functions that reduce the amount of boilerplate code you'll need to write:
Fetch¶
D3 also has an async form of its data loading methods: d3-fetch.
// await needs to be in a script type=module
const planetData = await d3.csv("example.csv");
d3.select("#planets")
.selectAll("li.planet")
.data(planetData)
.join("li")
.text((d) => `${d.planet} has ${d.moons} moons`);
If you aren't using modules, there is a callback-based version instead:
d3.csv("example.csv").then(
(data) => {
console.log("LOADED", data);
// callback function executes once data is loaded
d3.select("#planets2").selectAll("li.planet").data(data).join("li")
.text((d) => `${d.planet} weighs as much as ${d.mass_earths} earths`);
}
);
Axes¶
Generates SVG for common axes.
Example
Color¶
Utilities for working with various color spaces.
- https://d3js.org/d3-color
- https://d3js.org/d3-scale-chromatic - Similar to d3.scale but supports different types of color scales appropriate for visualization.
Interpolation¶
Useful for animations, blend between two values (numbers, colors, shapes, etc.) to provide a smooth transition:
Shape¶
Various functions for converting data to shapes, such as converting from percentages to slices of a pie. Since SVG does not have a <pie-slice> element, you will use these to create <path> elements that are the appropriate shape. (Recall that `
Includes: arc, area, curve, line, link, pie, stack, symbol, radial line/area/link, and more.
Less Common D3 Modules¶
See https://d3js.org/api and https://observablehq.com/@d3/gallery for more.
Highlights:
- Chord - Chord-based visualizations.
- Contour - Contour-line visualizations.
- Delaunay - Space partitioning algorithms.
- Force - Tools for (node/edge) graph layouts.
- Geo - Tools for working with GIS data and maps.
- Hierarchy - Tools for building hierarchical visualizations like treemaps.