Skip to content
Irene Ros edited this page Feb 13, 2014 · 13 revisions

In this guide, we will walk through a standard way create d3.js chart, and convert it into a d3.chart reusable chart.

For an in depth overview of the reasoning behind d3.chart see the following article by @jugglinmike: Exploring Reusability with D3.js

Basic d3 chart structure

d3.js charts have a very predictable structure, regardless of their complexity. d3.chart was built on top of those predictable patterns in order to separate the chart into logical units.

Take for example the following basic d3 chart. This chart renders red circles along the x axis, where the value of each datum dictates where the circle falls along a spectrum.

You can see it in action in this jsBin:

var data = [1,3,5,10,11,12,50];

var width = 500,
  height = 100,
  r = 5;

var xScale = d3.scale.linear()
  .range([5, width-r])
  .domain([1,50]);

var chart = d3.select("#vis")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

var circles = chart.append("g")
  .classed("circles", true);

circles.selectAll("circle")
  .data(data)
  .enter()
    .append("circle")
    .classed("circle", true)
    .style("fill", "red")
    .attr("r", r)
    .attr("cy", height/2)
    .attr("cx", function(d) {
      return xScale(d);
    });

Let's break down some of the common patterns we've just encountered:

  1. We setup some common values/functions that are used throughout the chart:
var width = 500,
  height = 100,
  r = 5;
  
var xScale = d3.scale.linear()
  .range([5, width-r])
  .domain([1,50]);
  1. We define a selection that will function as our main chart selection.
var chart = d3.select("#vis")
  .append("svg")
  .attr("width", width)
  .attr("height", height);
  1. We define a selection off of our chart that will constitute some portion of our chart (in our case, a container of circles:)
var circles = chart.append("g")
  .classed("circles", true);  
  1. We select the elements we wish to create based on the data, and bind the data to them:
circles.selectAll("circle")
  .data(data)
  1. We define what should happen for each element when it enters. This breaks into two parts:
  • The portion that is not data dependent:
.enter()
  .append("circle")
  .classed("circle", true)
  .style("fill", "red")
  .attr("r", r);
  • The portion that is data dependent:
.attr("cx", function(d) {
    return xScale(d);
  });

Naturally this is a very basic example. There are facilities we haven't explored, such as other lifecycle events or the reusable chart pattern. Having said that, this structure is the backbone of most d3-based charts.

Using d3.chart By Example

Conveniently d3.chart offers facilities that force that separation. Let's see how we would define this chart:

  1. Create a new chart constructor for our Circles chart. This will be used to create one or more instances of the Circles chart.
d3.chart("Circles", {

});
  1. Define some getters/setters for values that will be used throughout the chart. Note that we return this at the end of each method so that we can chain them. Seeing as chaining is a common d3 pattern, this is a good practice to follow in your own charts.
d3.chart("Circles", {

  // configures the width of the chart.
  // when called without arguments, returns the
  // current width.
  width: function(newWidth) {
    if (arguments.length === 0) {
      return this.w;
    }
    this.w = newWidth;
    return this;
  },
  
  // configures the height of the chart.
  // when called without arguments, returns the
  // current height.
  height: function(newHeight) {
    if (arguments.length === 0) {
      return this.h;
    }
    this.h = newHeight;
    return this;
  },
  
  // configures the radius of the circles in the chart.
  // when called without arguments, returns the
  // current radius.
  radius: function(newRadius) {
   if (arguments.length === 0) {
      return this.r;
    }
    this.r = newRadius;
    return this;
  }
});
  1. Initialize our basic chart structure
d3.chart("Circles", {

  initialize: function() {

    // initialize height and width from parent
    // as backup if the user doesn't configure
    // the chart parameters.
    this.h = this.base.attr("height");
    this.w = this.base.attr("width");

    // create a base scale we will use later.
    this.xScale = d3.scale.linear();
  },
  
  // configures the width of the chart.
  // when called without arguments, returns the
  // current width.
  width: function(newWidth) {
    if (arguments.length === 0) {
      return this.w;
    }
    this.w = newWidth;
    return this;
  },
  
  // configures the height of the chart.
  // when called without arguments, returns the
  // current height.
  height: function(newHeight) {
    if (arguments.length === 0) {
      return this.h;
    }
    this.h = newHeight;
    return this;
  },
  
  // configures the radius of the circles in the chart.
  // when called without arguments, returns the
  // current radius.
  radius: function(newRadius) {
   if (arguments.length === 0) {
      return this.r;
    }
    this.r = newRadius;
    return this;
  }
});
  1. Create a layer that will render the circles based on the data
d3.chart("Circles", {

  initialize: function() {
    // create a base scale we will use later.
    this.xScale = d3.scale.linear();
    
    var circlesBase = this.base.append("g")
        .classed("circles", true)
        .attr("height", this.h)
        .attr("width", this.w);

    this.layer("circles", circlesBase, {
      dataBind: function(data) {
        var chart = this.chart();

        // update the domain of the xScale since it depends on the data
        chart.xScale.domain(d3.extent(data));
        
        // return a data bound selection for the passed in data.
        return this.selectAll("circle")
          .data(data);
        
      },
      insert: function() {
        var chart = this.chart();

        // update the range of the xScale (account for radius width)
        // on either side
        chart.xScale.range([chart.r, chart.w - chart.r]);
        
        // setup the elements that were just created
        return this.append("circle")
          .classed("circle", true)
          .style("fill", "red")
          .attr("cy", chart.h/2)
          .attr("r", chart.r);
      },
      
      // setup an enter event for the data as it comes in:
      events: {
        "enter" : function() {
          var chart = this.chart();

          // position newly entering elements
          return this.attr("cx", function(d) {
            return chart.xScale(d);
          });
        }
      }
    });
  },
  
  // configures the width of the chart.
  // when called without arguments, returns the
  // current width.
  width: function(newWidth) {
    if (arguments.length === 0) {
      return this.w;
    }
    this.w = newWidth;
    return this;
  },
  
  // configures the height of the chart.
  // when called without arguments, returns the
  // current height.
  height: function(newHeight) {
    if (arguments.length === 0) {
      return this.h;
    }
    this.h = newHeight;
    return this;
  },
  
  // configures the radius of the circles in the chart.
  // when called without arguments, returns the
  // current radius.
  radius: function(newRadius) {
   if (arguments.length === 0) {
      return this.r;
    }
    this.r = newRadius;
    return this;
  }
});
  1. Your reusable chart is now ready. You can initialize your chart!
var data = [1,3,4,6,10];

var chart = d3.select("#vis")
  .append("svg")
  .chart("Circles")
    .width(100)
    .height(50)
    .radius(5);

chart.draw(data);