Creating a Custom D3 Visualization

This course barely scratches the surface of what D3 is capable of. A more complicated example will show how you can make custom visualizations with D3 that are impossible with standard charting libraries. This is where D3 really shines. The goal is to make a chart of day lengths throughout the year, like this one drawn by a child from M&M child care.

This chart has several interesting aspects that are worth noting. It is centred on noon and the length of the day is shown with two lines over the central axis, which forms a shape which is coloured in and the background is blue to represent night. There are four labeled axes, top and bottom and left and right. Building a visualization like it is not likely to come from a pre-packaged library.

Planning the Visualization

To plan the attack, a diagram of the desired visualization is helpful.

This brings several points to the foreground:

• The visualization will be inset with a margin of 40 pixels to print the axes. Using transform will make the math for this simple.
• Axes will go above and below and left and right of the graphic. Since the axes are all time-related, it makes sense to use time scales.
• Helpfully, the y axis counts up the same way as the SVG coordinate system so you do not need to reverse the axis.
• The sunrise section will be yellow and will be made up of what is left of a rect after drawing shapes with a svg:path for sunrise time and sunset time. d3.svg.area makes this easy.
• The points on the path need to be connected into a smooth curve. There are different kinds of interpolation for this.

New Concepts

For this example, some new concepts will be introduced:

• Time scales, an extension of linear scales tailored for time values.
• The svg:g element which groups other elements like a div and can be transformed as a group.
• Drawing complex shapes with svg:path, which is complicated, but made easy with the d3.svg.area function.
• Using d3.range to create an array of numbers.
• Using scale.ticks to generate tick marks for the axes.
• Using SVG translation to simplify positioning.
• Styling text with CSS.
• Adjusting pixel positions to avoid anti-aliasing

Digression: Anti-aliasing

Anti-aliasing helps make text and curved lines look smoother, but can make straight lines appear fuzzy. If you are using a WebKit-based browser, the lines on the left should be fuzzy, while those on the right should be crisp.

Anti-aliasing often happens when drawing straight lines with D3. If the line is 1 pixel tall and positioned exactly on a pixel, it will straddle the pixel and become fuzzy. To avoid it, add or subtract 0.5 to the pixel position. The example below uses this technique to prevent fuzziness.

However, rendering differs depending on the SVG layout engine. In the example above, Firefox 5 renders fuzzy vertical lines when positioned exactly on a pixel, while the horizontal lines are not.

Setting the SVG shape-rendering property to crispEdges disables anti-aliasing for some or all lines. However, this may have unwanted side-effects if the visualization has curved lines, making them look jagged.

svg {
shape-rendering: crispEdges;
}

A better solution may be to target the crispEdges property at certain (vertical or horizontal) lines with a CSS class.

line.crisp {
shape-rendering: crispEdges;
}

Using this solution without pixel munging seems to work better across different browsers, but you should test it yourself and see what works best for your audience.

Getting the Data

Having game-planned the graphic, the first step is to collect the data. You can find sunrise and sunset times for cities around the world at timeanddate.com. I could not find a source of sunrise and sunset times in a computer-readable format and didn’t feel like writing a scraper, so I selected a few days throughout the year for the selected location (Minneapolis, Minnesota).

The data format is like this:

{
date: new Date(2011, 4, 15), // day of year
sunrise: [7, 51],            // sunrise time as array of hours and minutes
sunset: [16, 42]             // likewise, sunset time (24 hour clock)
}

Sunrise and sunset times are stored as an array of hour and minute values. These are used to construct a Date to represent the time of day when plotting.

Building the Visualization

The JavaScript code for the visualization is below, with comments in line.
Here’s my codePen:

var width = 700;
var height = 525;

// the vertical axis is a time scale that runs from 00:00 - 23:59
// the horizontal axis is a time scale that runs from the 2011-01-01 to 2011-12-31

var y = d3.time.scale().domain([new Date(2011, 0, 1), new Date(2011, 0, 1, 23, 59)]).range([0, height]);
var x = d3.time.scale().domain([new Date(2011, 0, 1), new Date(2011, 11, 31)]).range([0, width]);

var monthNames = ["Jan", "Feb", "Mar", "April", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

// Sunrise and sun set times for dates in 2011. I have picked the 1st
// and 15th day of every month, plus other important dates like equinoxes
// and solstices and dates around the standard time/DST transition.

var data = [
{date: new Date(2011, 0, 1), sunrise: [7, 51], sunset: [16, 42]},
{date: new Date(2011, 0, 15), sunrise: [7, 48], sunset: [16, 58]},
{date: new Date(2011, 1, 1), sunrise: [7, 33], sunset: [17, 21]},
{date: new Date(2011, 1, 15), sunrise: [7, 14], sunset: [17, 41]},
{date: new Date(2011, 2, 1), sunrise: [6, 51], sunset: [18, 0]},
{date: new Date(2011, 2, 12), sunrise: [6, 32], sunset: [18, 15]}, // dst - 1 day
{date: new Date(2011, 2, 13), sunrise: [7, 30], sunset: [19, 16]}, // dst
{date: new Date(2011, 2, 14), sunrise: [7, 28], sunset: [19, 18]}, // dst + 1 day
{date: new Date(2011, 2, 14), sunrise: [7, 26], sunset: [19, 19]},
{date: new Date(2011, 2, 20), sunrise: [07, 17], sunset: [19, 25]}, // equinox
{date: new Date(2011, 3, 1), sunrise: [6, 54], sunset: [19, 41]},
{date: new Date(2011, 3, 15), sunrise: [6, 29], sunset: [19, 58]},
{date: new Date(2011, 4, 1), sunrise: [6, 3], sunset: [20, 18]},
{date: new Date(2011, 4, 15), sunrise: [5, 44], sunset: [20, 35]},
{date: new Date(2011, 5, 1), sunrise: [5, 30], sunset: [20, 52]},
{date: new Date(2011, 5, 15), sunrise: [5, 26], sunset: [21, 1]},
{date: new Date(2011, 5, 21), sunrise: [5, 26], sunset: [21, 3]}, // solstice
{date: new Date(2011, 6, 1), sunrise: [5, 30], sunset: [21, 3]},
{date: new Date(2011, 6, 15), sunrise: [5, 41], sunset: [20, 57]},
{date: new Date(2011, 7, 1), sunrise: [5, 58], sunset: [20, 40]},
{date: new Date(2011, 7, 15), sunrise: [6, 15], sunset: [20, 20]},
{date: new Date(2011, 8, 1), sunrise: [6, 35], sunset: [19, 51]},
{date: new Date(2011, 8, 15), sunrise: [6, 51], sunset: [19, 24]},
{date: new Date(2011, 8, 23), sunrise: [7, 1], sunset: [19, 9]}, // equinox
{date: new Date(2011, 9, 1), sunrise: [7, 11], sunset: [18, 54]},
{date: new Date(2011, 9, 15), sunrise: [7, 28], sunset: [18, 29]},
{date: new Date(2011, 10, 1), sunrise: [7, 51], sunset: [18, 2]},
{date: new Date(2011, 10, 5), sunrise: [7, 57], sunset: [17, 56]}, // last day of dst
{date: new Date(2011, 10, 6), sunrise: [6, 58], sunset: [16, 55]}, // standard time
{date: new Date(2011, 10, 7), sunrise: [6, 59], sunset: [16, 54]}, // standard time + 1
{date: new Date(2011, 10, 15), sunrise: [7, 10], sunset: [16, 44]},
{date: new Date(2011, 11, 1), sunrise: [7, 31], sunset: [16, 33]},
{date: new Date(2011, 11, 15), sunrise: [7, 44], sunset: [16, 32]},
{date: new Date(2011, 11, 22), sunrise: [7, 49], sunset: [16, 35]}, // solstice
{date: new Date(2011, 11, 31), sunrise: [7, 51], sunset: [16, 41]}
];

function yAxisLabel(d) {
if (d == 12) { return "noon"; }
if (d < 12) { return d; }
return (d - 12);
}

// The labels along the x axis will be positioned on the 15th of the
// month

function midMonthDates() {
return d3.range(0, 12).map(function(i) { return new Date(2011, i, 15) });
}

var dayLength = d3.select("#day-length").
append("svg:svg").
attr("width", width + padding * 2).
attr("height", height + padding * 2);

// create a group to hold the axis-related elements
var axisGroup = dayLength.append("svg:g").

// draw the x and y tick marks. Since they are behind the visualization, they
// can be drawn all the way across it. Because the  has been
// translated, they stick out the left side by going negative.

axisGroup.selectAll(".yTicks").
data(d3.range(5, 22)).
enter().append("svg:line").
attr("x1", -5).
// Round and add 0.5 to fix anti-aliasing effects (see above)
attr("y1", function(d) { return d3.round(y(new Date(2011, 0, 1, d))) + 0.5; }).
attr("x2", width+5).
attr("y2", function(d) { return d3.round(y(new Date(2011, 0, 1, d))) + 0.5; }).
attr("stroke", "lightgray").
attr("class", "yTicks");

axisGroup.selectAll(".xTicks").
data(midMonthDates).
enter().append("svg:line").
attr("x1", x).
attr("y1", -5).
attr("x2", x).
attr("y2", height+5).
attr("stroke", "lightgray").
attr("class", "yTicks");

// draw the text for the labels. Since it is the same on top and
// bottom, there is probably a cleaner way to do this by copying the
// result and translating it to the opposite side

axisGroup.selectAll("text.xAxisTop").
data(midMonthDates).
enter().
append("svg:text").
text(function(d, i) { return monthNames[i]; }).
attr("x", x).
attr("y", -8).
attr("text-anchor", "middle").
attr("class", "axis xAxisTop");

axisGroup.selectAll("text.xAxisBottom").
data(midMonthDates).
enter().
append("svg:text").
text(function(d, i) { return monthNames[i]; }).
attr("x", x).
attr("y", height+15).
attr("text-anchor", "middle").
attr("class", "xAxisBottom");

axisGroup.selectAll("text.yAxisLeft").
data(d3.range(5, 22)).
enter().
append("svg:text").
text(yAxisLabel).
attr("x", -7).
attr("y", function(d) { return y(new Date(2011, 0, 1, d)); }).
attr("dy", "3").
attr("class", "yAxisLeft").
attr("text-anchor", "end");

axisGroup.selectAll("text.yAxisRight").
data(d3.range(5, 22)).
enter().
append("svg:text").
text(yAxisLabel).
attr("x", width+7).
attr("y", function(d) { return y(new Date(2011, 0, 1, d)); }).
attr("dy", "3").
attr("class", "yAxisRight").
attr("text-anchor", "start");

// create a group for the sunrise and sunset paths

var lineGroup = dayLength.append("svg:g").

// draw the background. The part of this that remains uncovered will
// represent the daylight hours.

lineGroup.append("svg:rect").
attr("x", 0).
attr("y", 0).
attr("height", height).
attr("width", width).
attr("fill", "lightyellow");

// The meat of the visualization is surprisingly simple. sunriseLine
// and sunsetLine are areas (closed svg:path elements) that use the date
// for the x coordinate and sunrise and sunset (respectively) for the y
// coordinate. The sunrise shape is anchored at the top of the chart, and
// sunset area is anchored at the bottom of the chart.

var sunriseLine = d3.svg.area().
x(function(d) { return x(d.date); }).
y1(function(d) { return y(new Date(2011, 0, 1, d.sunrise[0], d.sunrise[1])); }).
interpolate("linear");

lineGroup.
append("svg:path").
attr("d", sunriseLine(data)).
attr("fill", "steelblue");

var sunsetLine = d3.svg.area().
x(function(d) { return x(d.date); }).
y0(height).
y1(function(d) { return y(new Date(2011, 0, 1, d.sunset[0], d.sunset[1])); }).
interpolate("linear");

lineGroup.append("svg:path").
attr("d", sunsetLine(data)).
attr("fill", "steelblue");

// finally, draw a line representing 12:00 across the entire
// visualization

lineGroup.append("svg:line").
attr("x1", 0).
attr("y1", d3.round(y(new Date(2011, 0, 1, 12))) + 0.5).
attr("x2", width).
attr("y2", d3.round(y(new Date(2011, 0, 1, 12))) + 0.5).
attr("stroke", "lightgray");

Using CSS helps remove duplication from the text formatting attributes:

div#day-length text {
fill: gray;
font-family: Helvetica, sans-serif;
font-size: 10px;
}

And the final result:

JanFebMarAprilMayJunJulAugSepOctNovDecJanFebMarAprilMayJunJulAugSepOctNovDec567891011noon123456789567891011noon123456789

Next Steps

Now that the visualization is complete, think of what could be done to improve it. You could label the solstices and equinoxes with exact times, or plot different sunset and sunrise times for different locations against each other, or make a label that shows exact times for the day the user is are mousing over…

The possibilities are endless.

Debugging D3

Lest you assume these examples were coded straight through, let me assure you this was not the case. There was a significant amount of trial-and-error coding needed to get them working.

One of the advantages of D3 using SVG as its native graphical representation is that it is easier to debug the WebKit Inspector or Firebug.

This is incredibly useful. You can try it on this page to see how the SVG code for these examples is generated.

Another technique that has been helpful for me is to use console.log in positioning functions. This lets you inspect the datum and index of an element as well as the calculated position for the x or y value. For example:

attr("x", function(d, i) {
console.log(d);
console.log(i);

return x(d.whatever);
})

Now you know about as much about D3 as I do. Obviously, this barely scratches the surface. For more tutorials and resources about D3, read the following articles:

D3 Refresh

This article aims to give you a high level overview of D3’s capabilities, in each example you’ll be able to see the input data, transformation and the output document. Rather than explaining what every function does I’ll show you the code and you should be able to get a rough understanding of how things work. I’ll only dig into details for the most important concepts, Scales and Selections.

William Playfair invented the bar, line and area charts in 1786 and the pie chart in 1801. Today, these are still the primary ways that most data sets are presented. Now, these charts are excellent but D3 gives you the tools and the flexibility to make unique data visualizations for the web, your creativity is the only limiting factor.

A Bar Chart

Although we want to get to more than William Playfair’s charts we’ll begin by making the humble bar chart with HTML – one of the easiest ways to understand how D3 transforms data into a document. Here’s what that looks like:

See code in codePen

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


The selectAll function returns a D3 “selection”: an array of elements that get created when we enter and append a div for each data point.

This code maps the input data [4, 8, 15, 16, 23, 42] to this output HTML.

<div id="chart">
<div style="height: 4px;"></div>
<div style="height: 8px;"></div>
<div style="height: 15px;"></div>
<div style="height: 16px;"></div>
<div style="height: 23px;"></div>
<div style="height: 42px;"></div>
</div>

All of the style properties that don’t change can go in the CSS.

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


GitHub’s Contribution Chart

With a few lines of extra code we can convert the bar chart above to a contribution chart similar to Github’s.

See code in codepen

Rather than setting a height based on the data’s value we can set a background-color instead.

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)
})

The colorMap function takes an input value between 0 and 1 and returns a colour along the gradient of colours between the two we provide. Interpolation is a key tool in graphics programming and animation, we’ll see more examples of it later.

An SVG Primer

Much of D3’s power comes from the fact that it works with SVG, which contains tags for drawing 2D graphics like circles, polygons, paths and text.

<svg width="200" height="200">
<circle fill="#3E5693" cx="50" cy="120" r="20" />
<text x="100" y="100">Hello SVG!</text>
<path d="M100,10L150,70L50,70Z" fill="#BEDBC3" stroke="#539E91" stroke-width="3">
</svg>

The code above draws:

• A circle at 50,120 with a radius of 20
• The text “Hello SVG!” at 100,100
• A triangle with a 3px border, the d attribute has the following instructions
• Move to 100,10
• Line to 150,70
• Line to 50,70
• Close path(Z)

<path> is the most powerful element in SVG.

Circles

See code in codepen

The data sets in the previous examples have been a simple array of numbers, D3 can work with more complex types too.

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

For each point of data we will append a <g>(group) element to the #chart and append <circle> and <text>elements to each with properties from our objects.

const g = d3.select('#chart')
.selectAll("g")
.data(data)
.enter()
.append('g')
g.append("circle")
.attr('cy', 40)
.attr('cx', (d, i)=> (i+1) * 50)
.attr('r', (d)=> d.sales)
g.append("text")
.attr('y', 90)
.attr('x', (d, i)=> (i+1) * 50)
.text((d)=> d.label)

The variable g holds a d3 “selection” containing an array of <g> nodes, operations like append() append a new element to each item in the selection.

This code maps the input data into this SVG document, can you see how it works?

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

Line Chart

See the codepen

Drawing a line chart in SVG is quite simple, we want to transform data like this:

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


Into this document:

<svg id="chart" height="100" width="200">
<path stroke-width="2" d="M0,70L50,80L100,60L150,20L200,5">
</svg>


Note: The y values are subtracted from the height of the chart (100) because we want a y value of 100 to be at the top of the svg (0 from the top).

Given it’s only a single path element, we could do it ourselves with code like this:

const path = "M" + data.map((d)=> {
return d.x + ',' + (100 - d.y);
}).join('L');
const line = <path stroke-width="2" d="\${ path }"/>;
document.querySelector('#chart').innerHTML = line;


D3 has path generating functions to make this much simpler though, here’s what it looks like.

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))


Much better! The interpolate function has a few different ways it can draw the line around the x, y coordinates too. See how it looks with “linear”, “step-before”, “basis” and “cardinal”.

Scales

Scales are functions that map an input domain to an output range.

See the codepen

In the examples we’ve looked at so far we’ve been able to get away with using “magic numbers” to position things within the charts bounds, when the data is dynamic you need to do some math to scale the data appropriately.

Imagine we want to render a line chart that is 500px wide and 200px high with the following data:

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


Ideally we want the y axis values to go from 0 to 30 (max y value) and the x axis values to go from 0 to 50 (max x value) so that the data takes up the full dimensions of the chart.

We can use d3.max to find the max values in our data set and create scales for transforming our x, y input values into x, y output coordinates for our SVG paths.

const width = 500;
const height = 200;
const xMax = d3.max(data, (d)=> d.x)
const yMax = d3.max(data, (d)=> d.y)

const xScale = d3.scale.linear()
.domain([0, xMax]) // input domain
.range([0, width]) // output range

const yScale = d3.scale.linear()
.domain([0, yMax]) // input domain
.range([height, 0]) // output range

These scales are similar to the colour interpolation function we created earlier, they are simply functions which map input values to a value somewhere on the output range.

xScale(0) -> 0
xScale(10) -> 100
xScale(50) -> 500


They also work with values outside of the input domain as well:

xScale(-10) -> -100
xScale(60) -> 600


We can use these scales in our line generating function like this:

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


Another thing you can easily do with scales is to specify padding around the output range:

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

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

Now we can render a dynamic data set and our line chart will always fit inside our 500px / 200px bounds with 20px padding on all sides.

Linear scales are the most common type but there’s others like pow for exponential scales and ordinal scales for representing non-numeric data like names or categories. In addition to Quantitative Scales and Ordinal Scales there are also Time Scales for mapping date ranges.

For example, we can create a scale that maps my lifespan to a number between 0 and 500:

const life = d3.time.scale()
.domain([new Date(1986, 1, 18), new Date()])
.range([0, 500])

// At which point between 0 and 500 was my 18th birthday?
life(new Date(2004, 1, 18))

If you’d like to go further with this, try the Animated Flight Visualization

Animated Flight Visualization

So far we’ve only looked at static lifeless graphics with a few rollovers for additional information. Let’s make an animated visualization that shows the active flights over time between Melbourne and Sydney in Australia.

See the Pen D3 – scales by Haig Armen (@haigarmen) on CodePen.

The SVG document for this type of graphic is made up of text, lines and circles.

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

<!-- for each flight in the current time -->
<g class="flight">
<text class="flight-id" x="160" y="100">JQ 500</text>
<line class="flight-line" x1="100" y1="100" x2="150" y2="100" />
<circle class="flight-dot" cx="150" cy="100" r="5" />
</g>

</svg>


The dynamic parts are the time and the elements within the flight group and the data might look something 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' }
]


To get an x position for a dynamic time we’ll need to create a time scale for each flight that maps its departure and arrival times to an x position on our chart. We can loop through our data at the start adding Date objects and scales so they’re easier to work with. Moment.js helps a lot here with date parsing and manipulation.

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])
});


We can now pass our changing Date to xScale() to get an x coordinate for each flight.

Render Loop

Departure and arrival times are rounded to 5 minutes so we can step through our data in 5m increments from the first departure to the last arrival.

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();

// Filter data set to active flights in the current time
const currentData = data.filter((d)=> {
return d.departureDate <= time && time <= d.arrivalDate
});

render(currentData, time);

if (now <= end) {
// Increment 5m and call loop again in 500ms
setTimeout(loop, 500);
}
}


Enter, Update and Exit

D3 allows you to specify transformations and transitions of elements when:

• New data points come in (Enter)
• Existing data points change (Update)
• Existing data points are removed (Exit)
const render = function(data, time) {
// render the time
d3.select('.time')
.text(moment(time).format("hh:mm a"))

// Make a d3 selection and apply our data set
const flight = d3.select('#chart')
.selectAll('g.flight')
.data(data, (d)=> d.id)

// Enter new nodes for any data point with an id not in the DOM
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', "5")

// Update existing nodes in selection with id's that are in the data
flight.select('.flight-dot')
.attr('cx', xPoint)
.attr('cy', yPoint)

// Exit old nodes in selection with id's that are not in the data
const oldFlight = flight.exit()
.remove()
}


Transitions

The code above renders a frame every 500ms with a 5 minute time increment:

• Creates a new flight group with a circle for every flight
• Updates the x/y coordinates of current flights
• Removes the flight groups when they’ve arrived

This works but what we really want is a smooth transition between each of these frames. We can achieve this by creating a transition on any D3 selection and providing a duration and easing function before setting attributes or style properties.

For example, let’s fade in the opacity of entering flight groups.

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

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


Let’s fade out exiting flight groups.

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


Add a smooth transition between the x and y points.

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


We can also transition the time between the 5 minute increments so that it displays every minute rather than every five minutes using the tween function.

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");
};
});


t is a progress value between 0 and 1 for the transition.