Tom Leo 's Blog

D3 is still Useful

Published on

Traditional D3 Use

Here's an example of a bar chart from d3-graph-gallery

D3 was the first framework I saw that made use of reactivity. Traditional d3 graphs usually took the form of'#app').data(data).enter(). In the example from above the bar graphs are created via:

    .attr("x", function (d) {
      return x(d.Country);
    .attr("y", function (d) {
      return y(d.Value);
    .attr("width", x.bandwidth())
    .attr("height", function (d) {
      return height - y(d.Value);
    .attr("fill", "#69b3a2");

Here a rectangle is created for each index of the data array. If the data changes, then all rectangles appended and their attributes will also update.

This is an extremely powerful framework from creating graphs, but it also wants to be in charge of manipulating and rendering part of the DOM.

Manipulating already existing SVGs

Imagine your design team has kicked over an SVG example of a graph they want on the page. To actually make the graph real you'll have to set the correct height of each rectangle. To complicate things further, the raw values from your dataset need to be mapped to your SVGs size & coordinate system.

This is where d3 still shines.

In this iteration:

I've set the rectangles heights all to 0, and instead let their heights be set via vanilla JS.

Parsing the CSV data

In an application you'll likely already have a way of fetching data. fetch or axios being the most common. For that reason, it doesn't really make sense to use a second one from d3. Instead, assuming the data has been fetched, you can parse it with d3.

import { csvParse } from "d3-dsv";

const csvData = `Country,Value
United States,12394
Germany (FRG),1653
United Kingdom,1214
const data = csvParse(csvData);

This results in [{Country: "United States", Value: 12394}, ...]

Mapping the data to the coordinate system

This can be done using scale functions. For the x axis, scaleBand is used. This will space out each bar evenly on the x axis.

// via npm install d3-scale@3
import { scaleBand } from "d3-scale";
var x = scaleBand()
  .range([0, width])
  .domain( => d.Country))

The result given a width of 400px is:

United States - X: 7.254901960784309px,
Russia - X: 43.52941176470588px,
Germany (FRG) - X: 79.80392156862744px,
France - X: 116.078431372549px,
United Kingdom - X: 152.35294117647058px,
China - X: 188.62745098039215px,
Spain - X: 224.9019607843137px,
Netherlands - X: 261.17647058823525px,
Italy - X: 297.45098039215685px,
Israel - X: 333.7254901960784px 

For the Y axis, scaleLinear is used. This will map the data points to positions relative to the height of the SVG.

// via npm install d3-scale@3
import { scaleLinear } from "d3-scale";
var y = scaleLinear().domain([0, 13000]).range([height, 0]);

The result given the height of 400px (minus 60px for margin) is:

United States - Y: 13.984615384615385,
Russia - Y: 158.1230769230769,
Germany (FRG) - Y: 261.8538461538461,
France - Y: 250.10769230769233,
United Kingdom - Y: 271.9846153846154,
China - Y: 273.90000000000003,
Spain - Y: 281.2153846153846,
Netherlands - Y: 273.0692307692307,
Italy - Y: 284.7692307692308,
Israel - Y: 270.8538461538462 

Manipulating the SVG data in vanilla JS

const yAttributes = => y(d.Value));
const xAttributes = => x(d.Country));

document.querySelectorAll("rect").forEach((elem, index) => {
  elem.setAttribute("x", xAttributes[index]);
  elem.setAttribute("y", yAttributes[index]);
  elem.setAttribute("height", height - yAttributes[index]);
  elem.setAttribute("width", 29.019607843137255);

The obvious caveat here is that this isn't responsive. But it's fairly strait forward to select all the elements and update them upon an event like user interaction.

Not all d3 components play nice

It's worth noting in the example I grabbed the axis were created using d3.axisBottom and d3.axisLeft. These functions unfortunately aren't decoupled from the d3's DOM manipulation logic.

Building axis is something you'd still be on the hook for most frontend stacks (i.e. vue.js for example).