Learn to Create D3.js Data Visualizations by Example

Write HTML, SVG, and CSS to bring your data to life with D3.js, a JavaScript library based on the DOM of data manipulation.

In my opinion, the three most important JavaScript libraries every Web developer should learn are jQuery, Underscore, and D3. As you learn them, you’ll start thinking about how to write code in new ways: jQuery lets you manipulate the DOM as much as possible with as little code as possible; Underscore (or lodash) uses functional tools to change the way you write programs; D3 gives you a lot of tools to manipulate data, and the idea of graphical programming. If you are not familiar with D3, please take some time to look at its examples and get a taste of what D3 can do.

This is not your father’s chart library.

D3 is extremely flexible. It is a basic visual JS library with an API similar to jQuery that maps data to HTML structures or SVG documents. D3 has rich mathematical functions to handle data conversion and physical computation, and it is good at manipulating paths and geometric shapes in SVG (circle, Ellipse, rect…). .

This article aims to give the reader an overview of D3. In the following examples, you will see the input data, the data transformation, and the final output document. Instead of explaining what each function does, I’ll show you the code and hopefully give you a sense of how it works. Only the Scales and Selections section will be highlighted.

A histogram

(View the code in codepen)

As I said, there are far more charts you can use in D3 than Mr Playfair ever invented, but learn to run before you learn to run. Let’s start with a simple bar chart to see how D3 combines data and document flow:

d3.select('#chart')
  .selectAll("div")
  .data([4, 8, 15, 16, 23, 42])
  .enter()
  .append("div")
  .style("height", (d)=> d + "px")
Copy the code

The selectAll method returns a D3 selection. D3 Selection is an array, and the elements in selection are created when a div is created for a data point and then called with Enter () and Append ().

The input data in the above code is a set of arrays: [4, 8, 15, 16, 23, 42]. The corresponding output HTML structure is:

id="chart"> style="height: 4px;" > style="height: 8px;" > style="height: 15px;" > style="height: 16px;" > style="height: 23px;" > style="height: 42px;" >Copy the code

All visual layer content that does not need to be controlled by JS is written to CSS:

#chart div {
  display: inline-block;
  background: #4285F4;
  width: 20px;
  margin-right: 3px;
}
Copy the code

Making contribution to the table

We only need to change a few lines in the bar chart code to get a GitHub contribution table.

(View code in codepen)

Unlike the bar chart, the chart does not change the height of the element, but the background-color of the element.

const colorMap = d3.interpolateRgb(
  d3.rgb('#d6e685'),
  d3.rgb('#1e6823')
)

d3.select('#chart')
  .selectAll("div")
  .data([.2, .4, 0, 0, .13, .92])
  .enter()
  .append("div")
  .style("background-color", (d)=> {
    return d == 0 ? '#eee' : colorMap(d)
  })
Copy the code

The colorMap function receives input values between 0 and 1 and returns a color value that is the gradient between two pairs of the input values. Interpolation is the key to graphic programming and animation. We’ll see more examples of this later.

SVG

Perhaps the biggest attraction of D3 is its ability to be used in SVG, which means that flat graphics such as circles, polygons, paths, and text can be interacted with.

width="200" height="200"> fill="#3E5693" cx="50" cy="120" r="20" /> x="100" y="100">Hello SVG! D = "M100 approximately, the -l150 10, 70 l50, 70 z" fill = "# BEDBC3 stroke" = "# 539 e91 stroke -" width = "3" >Copy the code

The above code implements:

  • A circle with a center of (50,120) and a radius of 20
  • A piece of text at (100,100)
  • A 3px triangle with thick edges,dI’m referring to the direction, so I draw a line from point 100 to point 150 to point 170 to point 50 to 70

Arguably the most useful element in SVG.

Circles

circular

(View code in codepen)

The data structure given in the above example is a very simple set of data, and D3 is capable of much more complex data types.

const data = [{
  label: "7am",
  sales: 20
},{
  label: "8am",
  sales: 12
}, {
  label: "9am",
  sales: 8
}, {
  label: "10am",
  sales: 27
}]
Copy the code

For each data point, we will have a G (group) element in #chart, and there will be one element and one element in each group according to the attributes of the object.

The following code matches the input data to the SVG document. Can you see how it works?

 height="100" width="250" id="chart">
  
     cy="40" cx="50" r="20"/>
     y="90" x="50">7am
  
  
     cy="40" cx="100" r="12"/>
     y="90" x="100">8am
  
  
     cy="40" cx="150" r="8"/>
     y="90" x="150">9am
  
  
     cy="40" cx="200" r="27"/>
     y="90" x="200">10am
  

Copy the code

The line chart

(View code in codepen)

Implementing line charts in SVG could not be simpler, but we use the following data:

const data = [
  { x: 0, y: 30 },
  { x: 50, y: 20 },
  { x: 100, y: 40 },
  { x: 150, y: 80 },
  { x: 200, y: 95 }
]
Copy the code

Convert to the following SVG document:

Id = "chart" height = "100" width = "200" > stroke - width = "2" d = "M0, 70 l50, 80 l100, 60 the -l150, 20 l200, 5" >Copy the code

Note: the y value in SVG code is different from the y value in the input value, which is 100 minus the given y value. Since the top left corner of the screen in SVG is (0,0), this is required in coordinates with a maximum ordinate of 100.

A graph consisting of only one path can be implemented like this:

const path = "M" + data.map((d)=> {
  return d.x + ',' + (100 - d.y);
}).join('L');
const line = ``;
document.querySelector('#chart').innerHTML = line;
Copy the code

The above code looks cumbersome, but D3 actually provides a path generation function to simplify this step:

const line = d3.svg.line()
  .x((d)=> d.x)
  .y((d)=> 100 - d.y)
  .interpolate("linear")

d3.select('#chart')
  .append("path")
  .attr('stroke-width', 2)
  .attr('d', line(data))
Copy the code

It’s so refreshing! Interpolate functions will interpolate to accept different parameters and draw different shapes. Besides’ Linear ‘, you can also try ‘basis’,’ cardinal ‘…

Scales

Scales can map an input set to an output set.

View the code in codepen

The data used in the above examples is fake and does not exceed the range specified by the coordinate axes. When the data is changing dynamically, things are not so simple. You need to map the input to a fixed range of outputs, which are our coordinate axes.

Suppose we have a line chart area of 500px X 200px and the input data is:

const data = [
  { x: 0, y: 30 },
  { x: 25, y: 15 },
  { x: 50, y: 20 }
]
Copy the code

If the Y-axis is in the range of [0,30] and the X-axis is in the range of [0,50], the data will appear nicely on the screen. But the reality is that the Y-axis ranges from 0 to 200, and the X-axis ranges from 0 to 500.

We can use d3. Max to obtain the maximum X and Y values in the input data, and then create the corresponding scales.

Scale is similar to the color difference function used above in that it maps input values to a fixed output range.

xScale(0) -> 0
xScale(10) -> 100
xScale(50) -> 500
Copy the code

The same applies to input values that are outside the range of output values:

xScale(-10) -> -100
xScale(60) -> 600
Copy the code

Scale can be used in line chart generation code like this:

const line = d3.svg.line()
  .x((d)=> xScale(d.x))
  .y((d)=> yScale(d.y))
  .interpolate("linear")
Copy the code

Scales also allows the graphics to appear more elegantly, for example by adding a bit of space:

const padding = 20;
const xScale = d3.scale.linear()
  .domain([0, xMax])
  .range([padding, width - padding])

const yScale = d3.scale.linear()
  .domain([0, yMax])
  .range([height - padding, padding])
Copy the code

It is now possible to generate a line graph of a dynamic dataset within the range of 500px X 200px, with a spacing of 20px on each side of the region.

Linear scales are the most common, but there are also poW for exponents, ordinal scales for non-numerical data (such as classification, naming, etc.), quantitative scales, ordinal scales, and time scales.

For example, taking my life as an input value, map it to the field [0,500] :

const life = d3.time.scale() .domain([new Date(1986, 1, 18), new Date()]) .range([0, 500]) // Which point between 0 and 500 is my 18th birthday? life(new Date(2004, 1, 18))Copy the code

Visualization of route data

So far, all we’ve seen are still images. Next, take the Melbourne-Sydney route as an example, and try moving images.

Codepen to view the effect

The above effect is based on SVG text, line, and circle.

 id="chart" width="600" height="500">
   class="time" x="300" y="50" text-anchor="middle">6:00
   class="origin-text" x="90" y="75" text-anchor="end">MEL
   class="dest-text" x="510" y="75" text-anchor="start">SYD
   class="origin-dot" r="5" cx="100" cy="75" />
   class="dest-dot" r="5" cx="500" cy="75" />
   class="origin-dest-line" x1="110" y1="75" x2="490" y2="75" />

  
   class="flight">
     class="flight-id" x="160" y="100">JQ 500
     class="flight-line" x1="100" y1="100" x2="150" y2="100" />
     class="flight-dot" cx="150" cy="100" r="5" />
  

Copy the code

The dynamic part is time and different flights, and the data source looks like this:

let data = [
  { departs: '06:00 am', arrives: '07:25 am', id: 'Jetstar 500' },
  { departs: '06:00 am', arrives: '07:25 am', id: 'Qantas 400' },
  { departs: '06:00 am', arrives: '07:25 am', id: 'Virgin 803' }
]
Copy the code

We need to scale the departure and arrival times of each route to the X-axis, and these data will change dynamically. At the beginning of the code, set these data as Date objects and scale them for later use. For dates, I use moment.js.

data.forEach((d)=> {
  d.departureDate = moment(d.departs, "hh-mm a").toDate();
  d.arrivalDate = moment(d.arrives, "hh-mm a").toDate();
  d.xScale = d3.time.scale()
    .domain([departureDate, arrivalDate])
    .range([100, 500])
});
Copy the code

Now you can pass the data into xScale() to get the x coordinates of each route.

Render loop

Departure and arrival times are rounded to 5 minutes, so the time between the first flight departure time and the last flight arrival time increases by 5 minutes.

let now = moment(data[0].departs, "hh:mm a"); const end = moment(data[data.length - 1].arrives, "hh:mm a"); const loop = function() { const time = now.toDate(); Const currentData = data.filter((d)=> {return D.parturedate &&time D.arrdate}); render(currentData, time); If (now end) {// Increment 5m and call loop again in 500ms // now = now. Add (5, 'minutes'); setTimeout(loop, 500); }}Copy the code

Create, update, and expire

Developers can specify the data transformation and element transition methods to use in the following scenarios:

  • When a new data point appears (creation time)
  • Existing data changes (when updated)
  • When data is no longer in use (expired)
const render = function(data, Select ('.time').text(moment(time).format("hh:mm a")) // Create selection, Select ('#chart').selectall (' g.light ').data(data, // Create node for new data const newFlight = flight.enter().append("g").attr('class', 'flight') const xPoint = (d)=> d.xScale(time); const yPoint = (d, i)=> 100 + i * 25; newFlight.append("circle") .attr('class',"flight-dot") .attr('cx', xPoint) .attr('cy', yPoint) .attr('r', Flight.select ('.flight-dot').attr('cx', xPoint).attr('cy', Const oldFlight = flight.exit().remove()}Copy the code

The transition

The code above renders the image in 5-minute increments every 500ms:

  • Updated the time
  • Each time there is a new flight chart, it is created with a circle identifier
  • Update the x/ Y axes of the current flight
  • When the plane arrives at its destination, the data is removed

We’ve achieved what we set out to do, but every time new data comes in and old data is destroyed, it’s brutal. This process can be smoothed out by adding transitions to D3 Selection.

For example, to add new data to a DOM structure, add an accretion animation by changing opacity:

const newFlight = flight.enter()
  .append("g")
  .attr('class', 'flight')
  .attr('opacity', 0)

newFlight.transition()
  .duration(500)
  .attr('opacity', 1)
Copy the code

Data removal can be added with fade animation:

flight.exit()
  .transition()
  .duration(500)
  .attr('opacity', 0)
  .remove()
Copy the code

We can also add:

flight.select('.flight-dot')
  .transition()
  .duration(500)
  .ease('linear')
  .attr('cx', xPoint)
  .attr('cy', yPoint)
Copy the code

We can also transition the 5-minute increments by using the tween function so that the time will appear every minute instead of every 5 minutes.

const inFiveMinutes = moment(time).add(5, 'minutes').toDate();
const i = d3.interpolate(time, inFiveMinutes);
d3.select('.time')
  .transition()
  .duration(500)
  .ease('linear')
  .tween("text", ()=> {
    return function(t) {
      this.textContent = moment(i(t)).format("hh:mm a");
    };
  });
Copy the code

T is an increment between 0 and 1 for the transformation.

Be creative

D3 can do a lot of things, although I would love to dig a little deeper, but I won’t go through them here.

You can find more examples in the D3 Gallery. I highly recommend Scott Murray’s D3 tutorial and the official D3 documentation.

Hopefully by the end of this article you have a general idea of how to use Selections, Scales, and Transitions to come up with a unique solution for seamlessly visualizing data. Be sure to let me know in the comments section once you’ve done it!