In this section, we will create an area chart to display stock prices and use brushing to allow the user to select an interval and get additional information about that time interval.
We will use the time series of the prices of the AAPL stock, available as a TSV file in the D3 examples gallery. The file contains the date and closing price for the date, covering almost 5 years of activity, as shown in the following code:
date close 1-May-12 582.13 30-Apr-12 583.98 27-Apr-12 603.00 26-Apr-12 607.70 ...
We begin by creating the structure of a reusable chart; we will add the width, height, and margin as the chart attributes, and add their corresponding accessors. The complete code is available in the chapter05/02-brushing.html
file. In the charting
function, we initialize and set the size of the svg
element as follows:
// Chart Creation function chart(selection) { selection.each(function(data) { // Select the container element and create the svg selection var div = d3.select(this), svg = div.selectAll('svg').data([data]); // Initialize the svg element svg.enter().append('svg') .call(svgInit); // Initialize the svg element svg.attr('width', width).attr('height', height); // Creation of the inner elements... }); }
The svgInit
function will be called only on enter, and it will create groups for the axis and the chart content. We will parse the input data, so we don't have to transform each item later. To parse the date, we will use the d3.time.format
D3 method:
// Configure the time parser var parseDate = d3.time.format(timeFormat).parse; // Parse the data data.forEach(function(d) { d.date = parseDate(d.date); d.close = +d.close; });
The
timeFormat
variable is defined as a chart attribute; we also added an accessor function so that the user can use the chart for other datasets. In this case, the input date format is %d-%b-%y
, that is, the day, abbreviated month name, and year. We can now create the x and y axis:
// Create the scales and axis var xScale = d3.time.scale() .domain(d3.extent(data, function(d) { return d.date; })) .range([0, width - margin.left - margin.right]); // Create the x axis var xAxis = d3.svg.axis() .scale(xScale) .orient('bottom'), // Invoke the xAxis function on the corresponding group svg.select('g.xaxis').call(xAxis);
We do the same with the y axis, but we will use a linear scale instead of the time scale and orient the axis to the left side. We can now create the chart content; we create and configure an area generator that will compute the path and then append the path to the chart group, as follows:
// Create and configure the area generator var area = d3.svg.area() .x(function(d) { return xScale(d.date); }) .y0(height - margin.top - margin.bottom) .y1(function(d) { return yScale(d.close); }); // Create the area path svg.select('g.chart').append("path") .datum(data) .attr("class", "area") .attr("d", area);
We will modify the styles for the classes of the axis groups and the area to have a better-looking chart, as follows:
<style> .axis path, line{ fill: none; stroke: #222; shape-rendering: crispEdges; } .axis text { font-size: 11px; } .area { fill: #ddd; } </style>
We load the dataset using the d3.tsv
function, which retrieves and parses tabular delimited data. Next, we will configure the chart, select the container element, and bind the dataset to the selection as follows:
// Load the TSV Stock Data d3.tsv('/chapter05/aapl.tsv', function(error, data) { // Handle errors getting or parsing the data if (error) { console.error(error); throw error; } // Create and configure the area chart var chart = areaChart(); // Bind the chart to the container div d3.select('div#chart') .datum(data) .call(chart); });
A brush is a control that allows you to select a range in a chart. D3 provides built-in support for brushing. We will use brushing to select time intervals in our area chart, and use it to show the price and date of the edges of the selected interval. We will also add a label that shows the relative price variation in the interval. We will create an SVG group to contain the brush elements. We will add this group in the svgInit
method, as follows:
// Create and translate the brush container group svg.append('g') .attr('class', 'brush') .attr('transform', function() { var dx = margin.left, dy = margin.top; return 'translate(' + [dx, dy] + ')'; });
The group should be added at the end of the svg
element to avoid getting it hidden by the other elements. With the group created, we can add the brush control in the charting function as follows:
function chart(selection) { selection.each(function(data) { // Charting function contents... // Create and configure the brush var brush = d3.svg.brush() .x(xScale) .on('brush', brushListener); }); }
We set the scale of the brush in the horizontal axis, and add a listener for the brush event. The brush can be configured to select a vertical interval by setting the y attribute with an appropriate scale and even be used to select areas by setting both the x and y attributes.
The brushListener
function will be invoked if the brush extent changes. The
brushstart
and brushend
events are also available, but we don't need to use them at the moment. In the following code, we apply the brush
function to the brush group using the call
method of the selection:
var gBrush = svg.select('g.brush').call(brush);
When we invoke the brush
function in a group, a series of elements are created. A background rectangle will capture the brush events. There will also be a rectangle of the extent class, which will resize as the user changes the brush area. Also, there are two invisible vertical rectangles at the brush edges; so, it's easier for the user to select the brush boundary. The rectangles will initially have zero height; we will set the height to cover the chart area:
// Change the height of the brushing rectangle gBrush.selectAll('rect') .attr('height', height - margin.top - margin.bottom);
We will modify the extent class, so the selected region is visible. We will set its color to gray
and set the fill opacity to 0.05
.
We will add lines to mark the prices at the beginning and end of the selected period, and add a label to display the price variation in the interval. We begin by adding the elements in the
chart.svgInit
function and set some of its attributes. We will create groups for the line markers and for the text elements that will display the price and date. We also add a text element for the price variation. We create the brushListener
function in the charting function scope, as shown in the following code:
// Brush Listener function function brushListener() { var s = d3.event.target.extent(); // Filter the items within the brush extent var items = data.filter(function(d) { return (s[0] <= d.date) && (d.date<= s[1]); }); }
When the brush event is triggered, the brush listener will have access to the event attributes through the d3.event
object. Here, we get the brush extent and use it to filter the dates that lie within the selected interval. Note that the selection is an approximation, because there are a limited number of pixels in the screen. At the beginning of the brush event, the time interval might be too small to contain data items. We will compute the prices only when at least two items have been selected. We then select the first and last elements of the item array, as follows:
// Compute the percentual variation of the period if (items.length > 2) { // Get the prices in the period priceB = items[0].close; priceA = Math.max(items[n - 1].close, 1e-8); // Set the lines and text position... }
Having the first and last elements of the selected period, we can compute the relative price variation and set the position of the marker lines and labels. We will also set the color of the variation label to blue
if the variation is positive and to red
if it's negative. As the configuration of the positions and labels is rather large, we won't include the code here. However, the code is available in the chapter05/02-brushing.html
file for reference.