Creating a line chart

Line chart is a common, basic chart type that is widely used in many fields. This chart consists of a series of data points connected by straight line segments. A line chart is also typically bordered by two perpendicular axes: the x axis and y axis. In this recipe, we will see how this basic chart can be implemented using D3 as a reusable JavaScript object that can be configured to display multiple data series on a different scale. Along with that we will also show the technique of implementing a dynamic multi-data-series update with animation.

Getting ready

Open your local copy of the following file in your web browser:

https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter8/line-chart.html

It is highly recommended to have the companion code example open while reading this recipe.

How to do it...

Let's take a look at the code that implements this chart type. Due the length of the recipe we will only show the outline of the code here while diving into the details in the following How it works... section.

<script type="text/javascript">
// First we define the chart object using a functional objectfunction lineChart() { // <-1A
...
    // main render function 
    _chart.render = function () { // <-2A
    ...
    };

    // axes rendering function
    function renderAxes(svg) {
    ...
    }
  ...
    

    // function to render chart body
    function renderBody(svg) { // <-2D        
    ...
    }

    // function to render lines
    function renderLines() {
    ...
    }

 // function to render data points
    function renderDots() {
        
    }

    return _chart; // <-1E
}

This recipe generates the following chart:

How to do it...

Line chart

How it works...

As we can see, this recipe is significantly more involved than anything we have encountered so far, so now I will break it into multiple detailed sections with different focuses.

Chart object and attributes: First, we will take a look at how this chart object is created and how associated attributes can be retrieved and set on the chart object.

function lineChart() { // <-1A
  var _chart = {};

  var _width = 600, _height = 300, // <-1B
    _margins = {top: 30, left: 30, right: 30, bottom: 30},
    _x, _y,
    _data = [],
    _colors = d3.scale.category10(),
    _svg,
    _bodyG,
    _line;
  ...
  _chart.height = function (h) {// <-1C
    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 (series) { // <-1D
    _data.push(series);
    return _chart;
  };
...
   return _chart; // <-1E
}

...

var chart = lineChart()
  .x(d3.scale.linear().domain([0, 10]))
  .y(d3.scale.linear().domain([0, 10]));

data.forEach(function (series) {
  chart.addSeries(series);
});

chart.render();

As we can see, the chart object is defined using a function called lineChart on line 1A following the functional object pattern we have discussed in the Understanding D3-Style JavaScript recipe in Chapter 1, Getting Started with D3.js. Leveraging the greater flexibility with information hiding offered by the functional object pattern, we have defined a series of internal attributes all named starting with an underscore (line 1B). Some of these attributes are made public by offering accessor function (line 1C). Publically accessible attributes are:

  • width: Chart SVG total width in pixels
  • height: Chart SVG total height in pixels
  • margins: Chart margins
  • colors: Chart ordinal color scale used to differentiate different data series
  • x: x axis scale
  • y: y axis scale

    The accessor functions are implemented using the technique we introduced in Chapter 1, Getting Started with D3.js, effectively combining both getter and setter functions in one function, which behaves as a getter when no argument is given and a setter when an argument is present (line 1C). Additionally, both lineChart function and its accessors, return a chart instance thus allowing function chaining. Finally, the chart object also offers an addSeries function which simply pushes a data array (series) into its internal data storage array (_data), see line 1D.

    Chart body frame rendering: After covering the basic chart object and its attributes, the next aspect of this reusable chart implementation is the chart body svg:g element rendering and its clip path generation.

    _chart.render = function () { // <-2A
      if (!_svg) {
        _svg = d3.select("body").append("svg") // <-2B
          .attr("height", _height)
          .attr("width", _width);
    
        renderAxes(_svg);
    
        defineBodyClip(_svg);
      }
    
      renderBody(_svg);
    };
    ...
    function defineBodyClip(svg) { // <-2C
      var padding = 5;
    
      svg.append("defs")
        .append("clipPath")
        .attr("id", "body-clip")
        .append("rect")
        .attr("x", 0 - padding)
        .attr("y", 0)
        .attr("width", quadrantWidth() + 2 * padding)
        .attr("height", quadrantHeight());
      }
    
    function renderBody(svg) { // <-2D
      if (!_bodyG)
        _bodyG = svg.append("g")
          .attr("class", "body")
          .attr("transform", "translate(" 
            + xStart() + "," 
            + yEnd() + ")") // <-2E
          .attr("clip-path", "url(#body-clip)");        
    
      renderLines();
    
      renderDots();
    }
    ...

    The render function defined on line 2A is responsible for creating the svg:svg element and setting its width and height (line 2B). After that, it creates an svg:clipPath element that covers the entire chart body area. The svg:clipPath element is used to restrict the region where paint can be applied. In our case we use it to restrict where the line and dots can be painted (only within the chart body area). This code generates the following SVG element structure that defines the chart body:

    How it works...

    Note

    For more information on clipping and masking please visit http://www.w3.org/TR/SVG/masking.html

    The renderBody function defined on line 2D generates the svg:g element which wraps all the chart body content with a translation set according to the chart margin convention we have discussed in the previous section (line 2E).

    Render axes: Axes are rendered in the function renderAxes (line 3A).

    function renderAxes(svg) { // <-3A
      var axesG = svg.append("g")
        .attr("class", "axes");
    
      renderXAxis(axesG);
    
      renderYAxis(axesG);
    }

    As discussed in the previous chapter, both x and y axes are rendered inside the chart margin area. We are not going into details about axes rendering since we have discussed this topic in much detail in Chapter 5, Playing with Axes.

    Render data series: Everything we discussed so far in this recipe is not unique to this chart type alone but rather it is a shared framework among other Cartesian coordinates based chart types. Finally, now we will discuss how the line segments and dots are created for multiple data series. Let's take a look at the following code fragments that are responsible for data series rendering.

    function renderLines() { 
      _line = d3.svg.line() // <-4A
        .x(function (d) { return _x(d.x); })
        .y(function (d) { return _y(d.y); });
        
      _bodyG.selectAll("path.line")
        .data(_data)
        .enter() // <-4B
        .append("path")                
        .style("stroke", function (d, i) { 
          return _colors(i); // <-4C
        })
        .attr("class", "line");
    
      _bodyG.selectAll("path.line")
        .data(_data)
        .transition() // <-4D
        .attr("d", function (d) { return _line(d); });
    }
    
    function renderDots() {
      _data.forEach(function (list, i) {
        _bodyG.selectAll("circle._" + i) // <-4E
          .data(list)
          .enter()
          .append("circle")
          .attr("class", "dot _" + i);
    
        _bodyG.selectAll("circle._" + i)
          .data(list)                    
          .style("stroke", function (d, i) { 
            return _colors(i); // <-4F
          })
          .transition() // <-4G
          .attr("cx", function (d) { return _x(d.x); })
          .attr("cy", function (d) { return _y(d.y); })
          .attr("r", 4.5);
        });
    }

    The line segments and dots are generated using techniques we introduced in Chapter 7, Getting into Shape. The d3.svg.line generator was created on line 4A to create svg:path that maps the data series. The Enter-and-Update pattern is used to create the data line (line 4B). Line 4C sets a different color for each data line based on its index. Lastly, line 4E sets the transition in the update mode to move the data line smoothly on each update. The renderDots function performs a similar rendering logic that generates a set of svg:circle elements representing each data point (line 4E), coordinating its color based on the data series index (line 4F), and finally also initiates a transition on line 4G, so the dots can move with the line whenever the data is updated.

    As illustrated by this recipe, creating a reusable chart component involves actually quite a bit of work. However, more than two-thirds of the code is required in creating peripheral graphical elements and accessors methods. Therefore in a real-world project you can extract this logic and reuse a large part of this implementation for other charts; though we did not do this in our recipes in order to reduce the complexity, so you can quickly grasp all aspects of chart rendering. Due to limited scope in this book, in later recipes we will omit all peripheral rendering logic while only focusing on the core logic related to each chart type.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset