Published: 11-08-2016
Getting to grips with D3. Learning the basics and understanding the Enter, Update and Exit model.
Overview
D3 is a really cool JavaScript library for "manipulating documents based on data". It essentially lets you modify the DOM in a functional, powerful way. You can bind data to HTML and SVG elements, making them 'data driven'. SVG is the graphical powerhouse of the web and can be used to display pretty much anything, from logos to physics simulations. D3 helps take it to another level. Let the fun commence.
Pre-stuff
You can get it a multitude of ways; NPM, Bower, Github. The quickest way is to just use a direct link:

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

Quick Example
As a quick example here is the code to produce fairly boring chart.
The Process
This section outlines the general process or workflow for making stuff with D3. It's the important bit to understand. The following four sub-sections make up the 'binding API', where you interact with the DOM. The rest of the D3 library really just aids in data manipulation and making your life easier (the 'non-binding API'). Things like scaling and line/axis/brush generators etc. The key part being they don't manipulate the DOM directly, they just help with the data or styling.
1) Select DOM element/s
To work with the DOM you need to be able to grab DOM elements. In D3, select and selectAll are used to do this. The select method is similar to JQuery's $. Both methods accept standard W3C Selectors so:
d3.select( selector )

d3.select( "#foo" )      // Gets id foo          => <div id="foo">   </div>
d3.select( ".foo" )      // Gets class foo       => <div id="foo">   </div>
d3.select( "foo" )       // Gets element foo     => <foo> </foo>
d3.select( ["foo=bar"] ) // Gets attribute       => <div foo="bar">  </div>
d3.select( "foo.bar" )   // Get class bar in foo => <foo class="bar"></foo>

d3.selectAll( selector )
This does exactly the same thing as select except it returns all the matches, not just the first. The returned object is called a selection. The selection object has transformation methods attached which can be used to modify document content. These could be used now but to do anything useful first bind the selection to some data. The full list of selector methods can be found here.
2) Bind Data
Once a collection of elements is selected it's time to bind a data collection to it. In JavaScript, arrays are used as collections. The selection.data method is called to bind data. This is where some D3 magic happens. Well, it's not magic at all, but it is pretty neat.

var data = [4, 8, 15, 16, 23, 42];

d3.select(".chart")
  	.selectAll("div")
  		.data(data)

The data array you bind doesn't have to have the same number of items as the collection of elements. D3 will apply all the data it can to the existing elements (one to one mapping), this is the Update selection. If the collections aren't equal in size, there will be either left over elements or not enough elements. Left over elements are added to the Exit selection. Extra elements are added to the Enter selection. If nothing is added to either of these selections they remains empty. Below is a visual representation.
Data Binding: Enter, Update and Exit
The .data() method returns these selections. By default the Update selection is returned straight away. Enter and Exit hang of the Update selection like this:

var data = [4, 8, 15, 16, 23, 42];

// Update selection
d3.select(".chart")
  	.selectAll("div")
  		.data(data)

// Enter selection
d3.select(".chart")
  	.selectAll("div")
  		.data(data)
  		.enter()

// Exit selectino
d3.select(".chart")
  	.selectAll("div")
  		.data(data)
  		.exit()

The selection arrays can now used be to apply DOM changes and styling to elements. When starting a visualisation often there is data but no elements, so the Enter selection is operated on. When the data is changed a combination of all three are likely to be used. When data is removed, the Exit selection is used to remove elements. Another explanation of the data binding can be found here.
3) Change/style Dom element/s
Using any selection, element/s can be modified. Modifiers act on all the elements in the selection array. The full list of modifying methods can be found here. Here are a few examples:

selection.attr(name[, value])
// Sets the attribute with the specified name to the specified value

selection.classed(names[, value])
// Assigns or unassigns the specified CSS class names

selection.style(name[, value[, priority]])
//  sets the style property with the specified name to the specified value

selection.append(type)
// If the specified type is a string, appends a new element of this type (tag name)

selection.text([value])
// If a value is specified, sets the text content to the specified value

selection.html([value])
// If a value is specified, sets the inner HTML to the specified value

The above methods can all be invoked, passing in a value which is a constant of a function. If called with a constant value, the value is applied to all elements in the selection. If a function is used, it is called for every element, and the returned value is used. This is useful because if a function is supplied, the item of data bound to that element is made available in the functions parameters. Precisely the function is passed the current datum (d), the current index (i) and the current group (nodes), with this as the as the current Dom element. Below is an example:

d3.selectAll( "div" )
	.data( [10, 20, 30, 40] )
	.enter()
	.style( "width", "10px" ) // set for all
	.style( "width", function (d, i, nodes) {
		return d + "px"
	});

As can be seen the methods for changing the DOM follow the standard DOM functionality very closely making it easy to figure out what they do. Being able to apply changes to all the elements in a selection with one line of code is very powerful.
4) Transitions
A selection also exposes a .transition() method. A transition is a section-like interface for animating changes to the DOM. Instead of applying changes instantaneously, transitions smoothly interpolate the DOM from its current state to the desired target state over a given duration. Transition returns a transition object. Transition supports some of the selection methods such as selection.attr and selection.styl. Check here for all available methods. Here is that quick example chart with a simple transition.
Now
These four processes: selecting elements, binding data, modifying elements and transitions make up the core functionality of D3. With that knowledge it's time to start making some more interesting visualisations.
Practical - Line Chart
Creating a line chart, step by step. The final goal is to make a reusable line chart that can be used for plotting multi-series data.
Plot line data

var selection = d3.select("body")

lineChart(selection);

function lineChart(selection) {
  var width = 600,
  height = 400

  // Generate some data
  data = [3,4,5.5,4,8,3,4.5,5,7,2].map(function (y, i) {
    return {x: i, y: y};
  })

  // Scale the data according the the height
  var scaleY = d3.scaleLinear()
    .range([height-40, 0])
    .domain(d3.extent(data, function (d) { return d.y }));

  // Scale the data according the the width
  var scaleX = d3.scaleLinear()
    .range([0, width-60])
    .domain([0, numberOfDataPoints])

  // Create a line generator for the data set
  var line = d3.line()
    .x(function (d) {
      return scaleX(d.x)
    })
    .y(function (d) {
      return scaleY(d.y)
    })

  // Create SVG element
  var svg = selection.append("svg")
    .attr("width", width)
    .attr("height", height)

  // Move the inner grouping element so chart 
  // doesn't overlap the boundaries
  var chart = svg.append("g")
  .attr("class", "chart")
  .attr("transform", "translate(30,20)");

  // Add SVG line to plot using line generator
  chart.append("path")
      .data([data])
      .attr("class", "line")
      .attr("d", line)
      .attr("fill", "none")
      .attr("stroke", "#ccc")
      .attr("stroke-width", "2")

  // Add data points
  chart.selectAll(".datum")
    .data(data)
    .enter()
      .append("circle")
        .attr("class", ".datum")
        .attr("cx", function (d, i) {
          return scaleX(d.x);
        })
        .attr("cy", function (d) {
          return scaleY(d.y);
        })
        .attr("r", 3)
        .attr("fill", "#000")

Plotting data
Add Axes
The line chart currently has no axes. Lucky, D3 makes rendering axes a doddle. Just call the d3-axis component and pass in the axis scaling function. By default all returned axes are positioned at (0,0) and need to be translated to required position. The code below show how the axes are generated and added to the SVG.

// Inner grouping element
var chart = svg.append("g")
  .attr("class", "chart")
  .attr("transform", "translate("30","20")");

// Add x-axis using scaling function from earlier.
// Has to be moved to the bottom of the graph
// because origin has been moved to (0, height)
// from (0, 0). (the y-axis is reversed)
svg
  .append("g")
  .attr("transform", "translate(30,380)")
  .call(d3.axisBottom(scaleX))

// Add y-axis using scaling function from earlier
svg
  .append("g")
  .attr("transform", "translate(30,20)")
  .call(d3.axisLeft(scaleY))

Adding axes with d3-axis
The resulting line chart:
Line chart with axes
Add Margins
Currently the chart has no concept of margins. It has been translated by (20, 30) to avoid clipping but the margins are not formalised. It's simple to do, all that's needed is to replace the hard coded translate coordinates with a variable or a margin object in this case.

// Use margin when calculating overal width and height
var margins = { top: 30, right: 30, bottom: 40, left: 50 };
var width = 600 - margins.left - margins.right;
var height = 400 - margins.top - margins.bottom;

// Margin used to offset inner grouping element
var chart = svg.append("g")
.attr("class", "chart")
.attr("transform", "translate(" + margins.left  + "," + margins.top + ")");

Adding margins
Now it's easy to control the chart's margins
Line chart with adding margins
Support multiple data series
To plot multiple data series, first there needs to be more data:

// Adding data for two series instead of one
data.push([3,4,5.5,4,8,3,4.5,5,7,2].map(function (y, i) {
return {x: i, y: y};
}));

data.push([4,5,6.7,3,7,4,5.5,6,3,3].map(function (y, i) {
return {x: i, y: y};
}));

Multiple data series - adding more data
The x and y-axis scaling needs to take into account the new data as the domain might not be the same. You could just set the domain manually but it makes sense to dynamically set it based on the data. To do that the data array needs to be flattened so the maximum and minimums can be found:

// Flattening the arrays for scaling
var all_data = []
data.forEach(function (series) {
	all_data = all_data.concat(series);
});

// Scaling y-axis using flattened array
var scaleY = d3.scaleLinear()
	.range([height, 0])
	.domain(d3.extent(all_data, function (d) { return d.y }));

// Scaling x-axis using flattened array
var scaleX = d3.scaleLinear()
  .range([0, width])
  .domain(d3.extent(all_data, function (d) { return d.x }));

Multiple data series - scaling data
The next challenge is to change the rendering so that all the data points and both lines are plotted. To distinguish between the lines multiple colours are needed. D3 has a neat little palette helper for that:

// Add colours
var colours = d3.schemeCategory10;

// Can call colours[index] for up tp 10 different colours
colours[0] // #1f77b4
colours[1] // #ff7f0e

Multiple data series - adding colours
Using the new data array, lines can now be plotted for each series. Line are created using the same line generator as before. To plot the individual data points the all_data array is simply switched plugged in. This flatten array can be used because the order of the points does not matter. They are decoupled from the line plotting.

// Add multiple lines
chart.selectAll("path.line")
  .data(data)
  .enter()
  .append("path")
  .attr("class", "line")
  .attr("d", line)
  .attr("fill", "none")
  .attr("stroke", function (d, i) {
    return colours[i];
  })
  .attr("stroke-width", "2")

// Loop through series and add data points
chart.selectAll(".datum")
	.data(all_data)
	.enter()
	  .append("circle")
	    .attr("class", ".datum")
	    .attr("cx", function (d, i) {
	      return scaleX(d.x);
	    })
	    .attr("cy", function (d) {
	      return scaleY(d.y);
	    })
	    .attr("r", 3)
	    .attr("fill", "#000")

Multiple data series - rendering
The end result is a multi-series line chart that looks like this:
Supporting multiple data series
Refactoring - make it reusable
Currently the chart isn't particularly useful. The data is hard coded and the script can only produce one graph. To make it useful requires some refactoring. The script should be able to render multiple charts and accept data generated elsewhere. Here, the module pattern will be used to achieve the required functionality. The chart will be configurable using getter/setter methods with chaining. This follows the technique laid out by D3 author Mike Bostock for resuable charts. The outline looks like:

function lineChart(selection) {
  // Private variables
  var selection = selection;

  //- Private methods ---------------------------------------------//
  function dimension() {
  }

  function scale() {
  }

  function renderBody() {
  }

  function renderLines() {
  }

  function renderPoints() {
  }

  function renderAxes() {
  }

  //- Public API --------------------------------------------------//
  var chart = {};

  // Getter/setter methods for configuring chart
  chart.width = function (w) {
  }

  chart.height = function (h) {
  }

  chart.margins = function (m) {
  }

  chart.addSeries = function (s) {
  }

  chart.selection = function (sel) {
  }

  chart.render = function () {
  }

  return chart;
}

Refactoring chart - module pattern template
All the charts config and data are held in 'private variables'. The private methods encapsulate all the functionality created in the previous snippets.

// Private variables
var selection = selection;
var data = [];
var all_data = [];
var colours = d3.schemeCategory10;

var svg, chart_area;
var scaleX, scaleY, line;

var margins = { top: 40, right: 30, bottom: 50, left: 50 };
var width = 600;
var height = 400;
var chart_width, chart_height;

dimension();

Refactoring chart - new private variables
The chart is configurable through getter/setters which modify these private variables. As an example the width getter/setter looks like:

// Width getter/setter
chart.width = function (w) {
  if (!arguments.length) return width;
  width = w;
  return chart;
}

Refactoring chart - getter/setter example
Returning the chart object at the end of the setter/getter makes the methods chainable, just like D3 itself. Using the refactored code to create a chart now looks like this:

// Create chart data
var data1 = [3,4,5.5,4,8,3,4.5,5,7,2].map(function (y, i) {
  return {x: i, y: y};
});

var data2 = [4,5,6.7,3,7,4,5.5,6,3,3].map(function (y, i) {
  return {x: i, y: y};
})

var selection = d3.select("body")

// Create and render chart
lineChart(selection)
  .addSeries(data1)
  .addSeries(data2)
  .render()

Refactoring chart - example use
And with all that refactoring the chart looks... the same, good. The full code can be found here. Or just keep scroll for the full code with added labels.
Re-usable chart
Adding labels
Almost forgot, the chart has no labels. Without labels the data doesn't mean anything. Adding labels requires a few updates. First, some private variables need to be added along with getter/setter methods. Providing fixed axes labels is easy, but not a good idea. If the user changes, say, the font size, the label might be positioned wrongly. The user should be able to modify the label positions as well. Label position are dynamically set, depending on the chart size. The best way to let the user change the positioning is through a callback. They can then set and return the position with access to variables such the width, height and margins. Their callback is stored internally and overrides the original function. The private variables introduced to control the label look like:

// New private variables and properties
var title = { text:"" };
var x_label = { text:"" };
var y_label = { text:"" };

// Generates the titles xy position
title.position = function (width, height, margins) {
  var x_pos = (width)/2;
  var y_pos = margins.top/1.4;
  return {x: x_pos, y: y_pos}
}

// Generates the x-axis label position
x_label.position = function (width, height, margins) {
  var x_pos = (width)/2;
  var y_pos = height - (margins.bottom/5);
  return {x: x_pos, y: y_pos}
}

// Generates the y-axis label position
y_label.position = function (width, height, margins) {
  var x_pos = (-height)/2;
  var y_pos = margins.left/3;
  return {x: x_pos, y: y_pos}
}

Label implementation - private variables
These new private variables require getter and setter method on the main chart object so that chart users can configure them:

// New getter/setters
chart.title = function (text, pos) {
  if (!arguments.length) return title;
  title.text = text;

  if(pos) {
    title.position = pos;
  }
  return chart;
}

chart.xLabel = function (text, pos) {
  if (!arguments.length) return x_label;
  x_label.text = text;

  if(pos) {
    x_label.position = pos;
  }
  return chart;
}

chart.yLabel = function (text, pos) {
  if (!arguments.length) return y_label;
  y_label.text = text;

  if(pos) {
    y_label.position = pos;
  }
  return chart;
}

Label implementation - new getter/setters
Now the functionality to render the axes labels needs to be implemented:

function renderLabels() {
  var position
  if (x_label.text) {
    position = x_label.position(width, height, margins);
    svg.append("text")
      .attr("class", "x-label")
      .text(x_label.text)
      .attr("text-anchor", "middle")
      .attr("transform", "translate(" + position.x + "," + position.y + ")")
  }

  if (y_label.text) {
    position = y_label.position(width, height, margins);
    svg.append("text")
      .attr("class", "y-label")
      .text(y_label.text)
      .attr("text-anchor", "middle")
      .attr("transform", "rotate(-90) translate(" + position.x + "," + position.y + ")")
  }

  if (title.text) {
    position = title.position(width, height, margins);
    svg.append("text")
      .attr("class", "title")
      .text(title.text)
      .attr("text-anchor", "middle")
      .attr("transform", "translate(" + position.x + "," + position.y + ")")
  }
}

Label implementation - rendering labels
Configurable labels are now part of the charts implementation. The code shows how they are configured.

lineChart(selection)
  .addSeries(data1)
  .addSeries(data2)
  // Add x-axis label supplying callback that generate position
  .xLabel('X-Axis', function (width, height, margins) {
    return {x:50, y:(height-20)}
  })
  // Add y-axis label in default position
  .yLabel('Y-Axis')
  // Add title
  .title('Title')
  .render()

Label implementation - example use
And the result:
Chart with labels
End Result
Getting lost in all those code snippets is very easy so below is the full code in one block. Big blocks just don't make for good code narration, hence the splitting up! Notice that throughout no raw CSS has been used on the chart. Elements have, however, been appropriately tagged with classes so that whoever uses lineChart can easily add in styling.

'use strict'

var selection = d3.select("body")

// Setting up data series
var data1 = [3,4,5.5,4,8,3,4.5,5,7,2].map(function (y, i) {
  return {x: i, y: y};
});

var data2 = [4,5,6.7,3,7,4,5.5,6,3,3].map(function (y, i) {
  return {x: i, y: y};
})

// How to use lineChart
lineChart(selection)
  .addSeries(data1)
  .addSeries(data2)
  .xLabel('X-Axis', function (width, height, margins) {
    return {x:50, y:(height-20)}
  })
  .yLabel('Y-Axis')
  .title('Title')
  .render()

//- Implementation -------------------------------------------

function lineChart(selection) {
  // Private variables
  var selection = selection;
  var data = [];
  var all_data = [];
  var colours = d3.schemeCategory10;

  var svg, chart_area;
  var scaleX, scaleY, line;

  var margins = { top: 40, right: 30, bottom: 50, left: 50 };
  var width = 600;
  var height = 400;
  var chart_width, chart_height;

  var title = { text:"" };
  var x_label = { text:"" };
  var y_label = { text:"" };

  // Generates the titles xy position
  title.position = function (width, height, margins) {
    var x_pos = (width)/2;
    var y_pos = margins.top/1.4;
    return {x: x_pos, y: y_pos}
  }

  // Generates the x-axis label position
  x_label.position = function (width, height, margins) {
    var x_pos = (width)/2;
    var y_pos = height - (margins.bottom/5);
    return {x: x_pos, y: y_pos}
  }

  // Generates the y-axis label position
  y_label.position = function (width, height, margins) {
    var x_pos = (-height)/2;
    var y_pos = margins.left/3;
    return {x: x_pos, y: y_pos}
  }

  function scale() {
    all_data = []
    data.forEach(function (series) {
      all_data = all_data.concat(series);
    });

    scaleY = d3.scaleLinear()
      .range([height - margins.top - margins.bottom, 0])
      .domain(d3.extent(all_data, function (d) { return d.y }));

    scaleX = d3.scaleLinear()
      .range([0, width - margins.left - margins.right])
      .domain(d3.extent(all_data, function (d) { return d.x }));
  }

  function renderBody() {
    console.log(height + margins.top + margins.bottom)
    svg = selection.append("svg")
      .attr("width", width)
      .attr("height", height)
      .attr("class", "chart-container")
      .attr("xmlns", "http://www.w3.org/2000/svg")
      .attr("viewBox", "0 0 " + (width) + " " + (height))
      .style("background", "#eee")

    chart_area = svg.append("g")
      .attr("class", "chart")
      .attr("transform", "translate(" + margins.left  + "," + margins.top + ")")
  }

  function renderLines() {
    // Create line generator
    line = d3.line()
      .x(function (d) {
        return scaleX(d.x)
      })
      .y(function (d) {
        return scaleY(d.y)
      });

    // Add line for each data series
    chart_area.selectAll("path.line")
      .data(data)
      .enter()
      .append("path")
      .attr("class", "line")
      .attr("d", line)
      .attr("fill", "none")
      .attr("stroke", function (d, i) {
        return colours[i];
      })
      .attr("stroke-width", "2");
  }

  function renderPoints() {
    // Render data points using flattened array
    chart_area.selectAll(".datum")
    .data(all_data)
    .enter()
      .append("circle")
        .attr("class", ".datum")
        .attr("cx", function (d, i) {
          return scaleX(d.x);
        })
        .attr("cy", function (d) {
          return scaleY(d.y);
        })
        .attr("r", 3)
        .attr("fill", "#000")
  }

  function renderAxes() {
    svg.append("g")
      .attr("class", "x-axis")
      .attr("transform", "translate(" + margins.left + "," + (height - margins.bottom) + ")")
      .call(d3.axisBottom(scaleX))

    svg.append("g")
      .attr("class", "y-axis")
      .attr("transform", "translate(" + margins.left + "," + margins.top + ")")
      .call(d3.axisLeft(scaleY))
  }

  function renderLabels() {
    var position
    if (x_label.text) {
      position = x_label.position(width, height, margins);
      svg.append("text")
        .attr("class", "x-label")
        .text(x_label.text)
        .attr("text-anchor", "middle")
        .attr("transform", "translate(" + position.x + "," + position.y + ")")
    }

    if (y_label.text) {
      position = y_label.position(width, height, margins);
      svg.append("text")
        .attr("class", "y-label")
        .text(y_label.text)
        .attr("text-anchor", "middle")
        .attr("transform", "rotate(-90) translate(" + position.x + "," + position.y + ")")
    }

    if (title.text) {
      position = title.position(width, height, margins);
      svg.append("text")
        .attr("class", "title")
        .text(title.text)
        .attr("text-anchor", "middle")
        .attr("transform", "translate(" + position.x + "," + position.y + ")")
    }
  }

  // Returned object containing public API 
  var chart = {};

  // Getter/setter methods for configuring chart
  chart.width = function (w) {
    if (!arguments.length) return width;
    width = w;
    return chart;
  }

  chart.height = function (h) {
    if (!arguments.length) return height;
    height = h;
    return chart;
  }

  chart.margins = function (m) {
    if (!arguments.length) return margins;
    margins = m;
    return chart;
  }

  chart.addSeries = function (s) {
    data.push(s);
    return chart;
  }

  chart.selection = function (sel) {
    if (!arguments.length) return selection;
    selection = sel;
  }

  chart.title = function (text, pos) {
    if (!arguments.length) return title;
    title.text = text;

    if(pos) {
      title.position = pos;
    }
    return chart;
  }

  chart.xLabel = function (text, pos) {
    if (!arguments.length) return x_label;
    x_label.text = text;

    if(pos) {
      x_label.position = pos;
    }
    return chart;
  }

  chart.yLabel = function (text, pos) {
    if (!arguments.length) return y_label;
    y_label.text = text;

    if(pos) {
      y_label.position = pos;
    }
    return chart;
  }

  // Renders the chart to screen
  chart.render = function () {
    if (data.length) {
      scale();
      renderBody();
      renderPoints();
      renderLines();
      renderAxes();
      renderLabels();
    }

  }

  return chart;
}

Line chart - full code