Skip to content

Instantly share code, notes, and snippets.

@bartaelterman
Last active August 29, 2015 14:00
Show Gist options
  • Save bartaelterman/11284618 to your computer and use it in GitHub Desktop.
Save bartaelterman/11284618 to your computer and use it in GitHub Desktop.
line-chart-example

Line chart example

This code demonstrates how to create a line chart in d3 and how to let the user navigate it.

Code explanation

1. Draw the line chart

Drawing a basic line chart is explained here. Some adjustments to this example were made:

  • The line chart is created as an object, which makes it easier to interact with it.
  • Data is given as a parameter to the LineChart object, and is not read from a file. A separate DataObj object is defined for convenience but this is not stricly necessary.
  • Defining data dependent variables (most notably, the x axis scale) is done in a separate function (calculateChartParameters) to allow reuse.
  • Drawing the chart the first time is done in the _init function, which is called at the end of the LineChart function.

2. Zoom to a certain x domain

The function updateXDomain shows how to zoom to a given x domain. The setXDomain function of the DataObj object takes care of setting the x domain on the data, while the rest of the updateXDomain function is simply recalling calculateChartParameters followed by redrawing the axes and line.

3. Shift the x domain

The x domain can be shifted left or right, by calling the shiftBy function with a shift argument. The shift argument is the value that you want to shift the x domain (not the range). Shift can be negative (shift left) or positive (shift right). The shiftBy function is based on the principle explained in this blogpost. This example has two drawbacks that are solved in the code here: 1) the data is shifted by 1 data point. Not by an arbitrary x-value. The code won't work if the x values in your data are not evenly distributed and it is not very flexible. Very nice for time-series data though. 2) The example with the moving x-axis did not work when I tried it locally. And 3), The y axis is static. The code in this repository will reset the y axis after shifting.

.axis path {
fill: none;
stroke-width: 1;
stroke: black;
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div id="linechart"></div>
<button id="update-but">Zoom to -20;20</button>
<button id="shift-left-but">Shift Left</button>
<button id="shift-right-but">Shift Right</button>
<button id="zoom-out">Zoom out</button>
<!-- Scripts -->
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="linechart.js"></script>
<script type="text/javascript">
$(document).ready(function () {
testdata = [{'x': -20, 'y': 12}, {'x': 0, 'y': 5}, {'x': 20, 'y': 21}, {'x': 40, 'y': 1}, {'x': 60, 'y': 13}, {'x': 80, 'y': 10}, {'x': 100, 'y': 4}];
parameters = {
data: testdata,
xlab: "x-label",
ylab: "y-label"
};
window.lc = new LineChart(parameters);
});
$("#update-but").click(function() {
window.lc.updateXDomain([-20, 20]);
});
$("#zoom-out").click(function() {
window.lc.updateXDomain([-20, 100]);
});
$("#shift-right-but").click(function() {
window.lc.shiftBy(20);
});
$("#shift-left-but").click(function() {
window.lc.shiftBy(-20);
});
</script>
</body>
function LineChart(parameters) {
// Svg parameters
var w = 900;
var h = 350;
var pad = 40;
var left_pad = 150;
// Chart elements. Defined here so they can be accessed later in other functions.
var data; // will contain the DataObj element containing data
var x; // the x scale
var y; // the y scale
var xmin; // the minimum value of the x domain
var xmax; // the maximum value of the x domain
var xAxis; // d3.svg.axis for x
var yAxis; // d3.svg.axis for y
var xaxis_el; // the x axis element on the screen
var yaxis_el; // the y axis element on the screen
var xlab; // label for the x axis
var ylab; // label for the y axis
var xlab_el; // the element on the screen containing the x axis label
var ylab_el; // the element on the screen containing the y axis label
var lineFunction; // the function to draw the line
var line; // the line element
// Add main svg element
var svg = d3.select("#linechart").append("svg").attr("width", w).attr("height", h);
// Add clipPath for both the line element and the x axis element
var devs = svg.append("defs");
devs.append("clipPath")
.attr("id", "lineclip")
.append("rect")
.attr("x", left_pad)
.attr("y", pad)
.attr("width", w - left_pad - pad)
.attr("height", h - 3*pad);
devs.append("clipPath")
.attr("id", "axisclip")
.append("rect")
.attr("x", left_pad - 15)
.attr("y", h - pad - 10)
.attr("width", w - left_pad - pad + 30)
.attr("height", 30);
// note that the margins defined for the x axis clip Path are arbitrary
// ------------------------
// calculateChartParameters will calculate all required parameters
// that are needed to draw and redraw the chart.
// ------------------------
var calculateChartParameters = function (data, xdomain) {
// Find data metrics
if (xdomain.length == 2) {
xmin = xdomain[0];
xmax = xdomain[1];
} else {
var xmin_xmax = d3.extent(data, function(d) {return d.x});
xmin = xmin_xmax[0];
xmax = xmin_xmax[1];
}
var ymin_ymax = d3.extent(data, function(d) {return d.y});
ymin = ymin_ymax[0];
ymax = ymin_ymax[1];
// Define x and y scale and domain
x = d3.scale.linear().domain([xmin,xmax]).range([left_pad, w - pad]);
y = d3.scale.linear().domain([ymin,ymax]).range([h - 2*pad, pad]);
// Define axes
xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(5);
yAxis = d3.svg.axis().scale(y).orient("left").ticks(5);
// The accessor function for the line
lineFunction = d3.svg.line()
.x(function(d) {return x(d["x"])})
.y(function(d) {return y(d["y"])})
.interpolate("linear");
}
// ------------------------
// init function: this function will draw the initial chart.
// It is this function that will first populate the Chart element
// variables that are declared above.
// ------------------------
_init = function () {
// User defined parameters, with default values
data = new DataObj(parameters.data.sort(compareByX))
xlab = parameters.xlab || "no xlabel given";
ylab = parameters.ylab || "no ylabel given";
view = parameters.view || [];
data.setXDomain(view);
calculateChartParameters(data.domain_data, view);
// define xLabel
xlab_el = svg.append("text")
.attr("class", "axis-label")
.attr("x", w / 2)
.attr("y", h - 6)
.text(xlab);
// define yLabel
ylab_el = svg.append("text")
.attr("class", "axis-label")
.attr("transform", "translate(" + left_pad / 3 + ", " + h / 2 + ") rotate(-90)")
.text(ylab);
// Draw xAxis
xaxis_el = svg.append("g")
.attr("clip-path", "url(#axisclip)")
.append("g")
.attr("class", "axis")
.attr("transform", "translate(0, " + (h - pad) + ")")
.call(xAxis);
// Draw yAxis
yaxis_el = svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + (left_pad - pad) + ", 0)")
.call(yAxis);
// Add the line to the chart
line = svg.append("g")
.attr("clip-path", "url(#lineclip)")
.append("path")
.attr("d", lineFunction(data.domain_data))
.attr("stroke", "black")
.attr("stroke-width", 2)
.attr("fill", "none");
}
this.updateXDomain = function (xdomain) {
data.setXDomain(xdomain);
calculateChartParameters(data.domain_data, xdomain);
xaxis_el.call(xAxis);
yaxis_el.call(yAxis);
line.transition().duration(200).attr("d", lineFunction(data.domain_data));
}
this.shiftBy = function (shift) {
data.addShiftPoints(shift);
var rangeDiff = x(0) - x(0 + shift);
if (shift > 0) {
tmpx = d3.scale.linear().domain([xmin,xmax + shift]).range([left_pad, w - pad - rangeDiff]);
} else {
tmpx = d3.scale.linear().domain([xmin + shift,xmax]).range([left_pad - rangeDiff, w - pad]);
}
tmpxAxis = d3.svg.axis().scale(tmpx).orient("bottom").ticks(5);
xaxis_el.transition().duration(350).attr("transform", "translate(" + rangeDiff + ", " + (h - pad) + ")").call(tmpxAxis);
line.attr("d", lineFunction(data.domain_data)).attr("transform", null)
.transition().duration(350).attr("transform", "translate(" + rangeDiff + ")").each("end", function() {
data.removeShiftPoints(shift);
calculateChartParameters(data.domain_data, [xmin + shift, xmax + shift]);
yaxis_el.call(yAxis);
xaxis_el.attr("transform", "translate(0, " + (h - pad) + ")").call(xAxis);
line.attr("transform", null).attr("d", lineFunction(data.domain_data));
});
}
// Run init when constructing a new LineChart object
_init();
/* ----------------
* Helper functions
* ----------------
*/
function compareByX(a,b) {
if (a.x < b.x)
return -1;
if (a.x > b.x)
return 1;
return 0;
}
}
function DataObj(data) {
this.data = data;
this.setXDomain = function(xdomain) {
if (xdomain.length == 0) {
this.minX = this.data[0].x;
this.maxX = this.data[this.data.length - 1].x;
} else {
this.minX = xdomain[0];
this.maxX = xdomain[1];
}
this.start_index = this.getStartIndex(this.data, this.minX);
this.stop_index = this.getStopIndex(this.data, this.maxX);
this.domain_data = [];
for (var i=this.start_index; i<= this.stop_index; i++) {
this.domain_data.push(this.data[i]);
}
return this.domain_data;
}
this.getStartIndex = function(data, minXValue) {
for (var i=0; i < data.length; i++) {
var x = data[i].x;
if (x >= minXValue) {
return i;
}
}
return data.length - 1;
}
this.getStopIndex = function(data, maxXValue) {
for (var i=0; i < data.length; i++) {
index = data.length - 1 - i;
var x = data[index].x;
if (x <= maxXValue) {
return index;
}
}
return 0;
}
this.addShiftPoints = function(shift) {
if (shift < 0) {
var new_minX = this.minX + shift;
var new_start_index = this.getStartIndex(this.data, new_minX);
for (var i=this.start_index - 1; i >= new_start_index; i=i-1) {
this.domain_data.unshift(this.data[i]);
}
this.minX = new_minX;
this.start_index = new_start_index;
}
if (shift > 0) {
var new_maxX = this.maxX + shift;
var new_stop_index = this.getStopIndex(this.data, new_maxX);
for (var i=this.stop_index + 1; i <= new_stop_index; i++) {
this.domain_data.push(this.data[i]);
}
this.maxX = new_maxX;
this.stop_index = new_stop_index;
}
return this.domain_data;
}
this.removeShiftPoints = function(shift) {
if (shift < 0) {
var new_maxX = this.maxX + shift;
var new_stop_index = this.getStopIndex(this.data, new_maxX);
var diff = this.stop_index - new_stop_index;
for (var i=0; i<diff; i++) {
this.domain_data.pop();
}
this.maxX = new_maxX;
this.stop_index = new_stop_index;
}
if (shift > 0) {
var new_minX = this.minX + shift;
var new_start_index = this.getStartIndex(this.data, new_minX);
var diff = new_start_index - this.start_index;
for (var i=0; i<diff; i++) {
this.domain_data.shift();
}
this.minX = new_minX;
this.start_index = new_start_index;
}
return this.domain_data;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment