Working with D3 - lennartdeknikker/frontend-data GitHub Wiki

What is D3?

The D3 (or D3.js) library is described in it's readme] as:

... a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data.

source: D3 Readme

Installation

The easiest way to use D3 is to include it in your HTML file like this:

<script src="https://d3js.org/d3.v5.min.js"></script>

Understanding D3

I started to work with D3 by looking for a good tutorial. I found one on LinkedIn Learning and used it to learn the basics. I coded along as the videos showed how to make the visualisations below. This helped me a lot in understanding the core concept and the basics of pushing data into svg element properties.

experiment 1

View code
  var dataArray = [5, 11, 18];
var dataDays = ['mon','wed','fri'];

var rainbow = d3.scaleSequential(d3.interpolateRainbow).domain([0,10]);
var rainbow2 = d3.scaleSequential(d3.interpolateRainbow).domain([0,3]);

var x = d3.scaleBand()
  .domain(dataDays)
  .range([0,170])
  .paddingInner(0.1176);
var xAxis = d3.axisBottom(x);

var svg = d3.select("body").append("svg").attr("height","100%").attr("width","100%");

var cat20 = d3.schemeCategory20;

svg.selectAll("rect")
  .data(dataArray)
  .enter().append("rect")
      .attr("height",(d) => { return d*15 })
      .attr("width", 50)
      .attr("fill",(d,i) => { return rainbow(i); })
      .attr("x",(d, i) => { return 60*i; })
      .attr("y",(d, i) => { return 300-(d*15); });

svg.append("g")
  .attr("class","x axis hidden")
  .attr('transform', 'translate(' + 0 + ',' + 300 + ')')
  .call(xAxis);

var newX = 300;
svg.selectAll("circle")
  .data(dataArray)
  .enter().append("circle")
  .attr('fill', (d,i) => { return rainbow2(i); } )
      .attr("cx",(d, i) => { return newX+=(d*3)+(i*20); })
      .attr("cy", 100)
      .attr("r",(d) => { return d*3; });

var newX = 600;
svg.selectAll("ellipse")
  .data(dataArray)
  .enter().append("ellipse")
      .attr("fill", (d,i) => { return cat20[i]; } )
      .attr("cx",(d, i) => { return newX+=(d*3)+(i*20); })
      .attr("cy", 100)
      .attr("rx",(d) => { return d*3; })
      .attr("ry", 30);

var newX = 900;
svg.selectAll("line")
  .data(dataArray)
  .enter().append("line")
      .attr("x1",newX)
      .attr("y1",(d,i) => { return 80+(i*20); })
      .attr("x2",(d,i) => { return newX+(d*15); })
      .attr("y2",(d,i) => { return 80+(i*20); });

var textArray = ['start','middle','end'];
svg.append("text").selectAll("tspan")
  .data(textArray)
  .enter().append("tspan")
      .text((d,i) => { return d })
      .attr("x", newX)
      .attr("y", (d,i) => { return (i*30)+150 })
      .attr("text-anchor", "start")
      .attr("dominant-baseline", "middle")
      .attr("font-size", 30);

experiment 2

View code
  
var dataArray = [12,23,25,31,34,45,56,67,78,89,90,122,123,124,130,150,170];
var dataYears = ['2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016'];

var parseDate = d3.timeParse("%Y");

var height = 200;
var width = 500;

var margin = {left:50,right:50,top:40,bottom:0};

var y = d3.scaleLinear()
  .domain([0,d3.max(dataArray)])
  .range([height,0]);

var x = d3.scaleTime()
  .domain(d3.extent(dataYears, (d,i) => { return parseDate(d); }))
  .range([0, width]);

var yAxis = d3.axisLeft(y).ticks(6).tickPadding(10).tickSize(10);
var xAxis = d3.axisBottom(x);

var area = d3
  .area()
  .x((d,i) => { return x(parseDate(dataYears[i])); })
  .y0(height)
  .y1((d,i) => { return y(d); })
;

var svg = d3
  .select("body")
  .append("svg")
  .attr("height","100%")
  .attr("width","100%");


  
var chartGroup = svg
  .append("g")
  .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')

chartGroup
  .append("path")
  .attr("d",area(dataArray));

chartGroup
  .append("g")
  .attr("class", "axis y")
  .call(yAxis);

chartGroup
  .append("g")
  .attr("class", "axis x")
  .attr('transform', 'translate(0,' + height + ')')
  .call(xAxis);

experiment 3

View code
  var dataArray = [
  {x:5,y:5},
  {x:10,y:15},
  {x:20,y:7},
  {x:30,y:18},
  {x:40,y:10},
];

var interpolateTypes = [d3.curveLinear, d3.curveNatural, d3. curveStep, d3.curveBasis, d3.curveBundle,d3.curveCardinal];

var svg = d3.select("body").append("svg").attr("height", 500).attr("width", "100%");

for (var p=0; p < dataArray.length; p++) {
  var line = d3
  .line()
  .x((d, i) => { return d.x*6; })
  .y((d, i) => { return d.y*4; })
  .curve(interpolateTypes[p]);

var chartGroup = svg.append("g").attr("class","group"+p).attr("transform","translate(0,"+100*p+")");

chartGroup
  .append("path")
  .attr("d",line(dataArray))
  .attr("fill","none")
  .attr("stroke","red");
;

chartGroup
  .selectAll("circle.grp"+p)
  .data(dataArray)
  .enter().append("circle")
      .attr("class", (d,i) => { return "grp"+i; })
      .attr("cx",(d, i) => { return d.x*6; })
      .attr("cy",(d, i) => { return d.y*4; })
      .attr("r",2)
;

}
Laurens explained these concepts more rigorously and taught us more about working with SVG and canvas elements in general. He also introduced some features of D3 that I wasn't aware of. Those came in really handy.

Applying D3 using examples

When I understood the main idea behind D3, it was easy to tweak code from examples or implement small bits myself. Most examples are well documented and easy to transform, because d3 is a very functional library which makes it easy to change variables and see results right away.

A handy tool, I used sometimes, to start visualisations and try things out is vizhub. This website makes it possible to just copy code from examples and start tweaking those to find out if you can use those.

Implementing D3 without examples

Later on, when I tweaked one of the examples to work with my own data, I got some ideas for improvements and new features that would be nice to add. From that moment on, work progressed a bit slower, since it became harder to find the right d3 functions to achieve what I wanted. I did have more fun working out things myself though, because there's more to learn working this way and it's nice to get things working without too much help.

Additional Findings

D3 is an amazing library with an insane amount of functionalities. It takes time to get to know what functions are available, but the documentation is really clear about all functionalities and there's a big community that shares their findings and methods. I'd like to work more often with this library, because when you get the hang of it, it becomes really easy to visualise bigger data sets exactly like you want to.

Updating D3 elements

To update data points in D3, there's an update pattern that's commonly used to get your data in, delete superfluous elements or create new elements if there's more data.

Binding data

To bind your data to elements in d3, you can use d3.data(). In my case, the update pattern then starts like this.

	const datapoints = d3
		.select('.g-datapoints')
		.selectAll('.datapoint')
		.data(objects)
		.attr('r', 0);
``
First, I select the group that contains all data points. Then, the elements that need to be updated are selected, using `.selectAll()` and the new data is connected with `data`. 
If the array of data is longer than the selection of elements, new elements need to be appended. If the new array is shorter, the surplus of elements need to be deleted.

## .enter()
By using `enter()` the possible surplus of data is selected. Then new elements need to be appended to this selection. In my code this is accomplished by `.append(circle)`. 
```js
datapoints.enter()
          .append('circle')

Because I need to change the cx and cy properties for all elements, both the new ones as the old ones, I use .merge() to combine the current selection of new elements with the old elements. Then I have the connected data define the properties for all elements in this selection like this:

datapoints		
	  .merge(datapoints)
	  .attr('class', 'datapoint')
	  .attr('data-place', d => d.placeName)
	  .attr('data-long', d => d.long)
	  .attr('data-lat', d => d.lat)
	  .attr('cx', d => projection([d.long, d.lat])[0])
	  .attr('cy', d => projection([d.long, d.lat])[1])
	  .attr('fill', '#00827b')

To bind interaction to all elements, I then add some event handlers:

datapoints
	  .on('mouseover', objectMouseoverHandler)
	  .on('mouseout', objectMouseoutHandler)
	  .on('click', objectClickHandler);

As said before, it's important to remove a possible surplus of elements. That part is accomplished using this last line of code. exit() selects the surplus and .remove() removes these elements.

	datapoints.exit().remove();

At D3InDepth, a general update pattern is defined. Alltogether, the pattern on that page with examples looks like this:

function update(data) {
  var u = d3.select('#content')
    .selectAll('div')
    .data(data);

  u.enter()
    .append('div')
    .merge(u)
    .text(function(d) {
      return d;
    });

  u.exit().remove();
}

Source

.join()

The pattern I used, found on d3InDepth is a bit outdated. The new pattern proposed by Mike Bostock uses data join. This new pattern makes it possible to add separate transitions to both the enter and the exit selection as well as the update selection. This pattern is a bit more complicated though and I didn't need to add different transitions to each selection, which is why I didn't use it.

.join combines both adding elements to the enter selection as removing elements from the exit selection. Within this function, it's possible to manipulate enter, update and exit selections separately. The syntax for this is as follows:

.join(
      enter => enter.attr(),
      update => update.attr(),
      exit => exit.attr()

The first question I would ask, was: why would you want to manipulate the exit selection if you remove it anyways. It's there because it enables you to add different transitions on this selection, so the elements don't just disappear anymore. You can make those disappear gradually now.

Source: https://observablehq.com/@d3/selection-join

⚠️ **GitHub.com Fallback** ⚠️