Skip to content

Instantly share code, notes, and snippets.

@bartaelterman
Last active August 29, 2015 14:02
Show Gist options
  • Save bartaelterman/dfaa84ad8227a8bdffeb to your computer and use it in GitHub Desktop.
Save bartaelterman/dfaa84ad8227a8bdffeb to your computer and use it in GitHub Desktop.
Line chart example with sub domains using D3

Domain line chart example

This example explains how to create a line chart that consits of several domains (analogues to the domains defined in cal-heatmap).

For every domain, a separate svg element is added to the DOM. When pressing Previous or Next, new data is fetched and a new svg element with a line chart is created. This new svg element is placed before (in the case of Previous) or after (in the case of Next) the existing domain svgs. Next, all domain svgs are shifted either to the left or to the right, and finally the svg element on the opposide side is removed from the DOM.

The code also contains a nrOfPrefetchedDomains parameter. When set to 1, two additional domains are constructed and placed on either side of the chart area, but they remain invisible (using the class invisible and the display: none attribute in the style sheet). This is useful when you would replace the DataObjStub by something that performs API calls to a backend. By prefetching domains, the application becomes more responsive.

.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="previous">Previous</button>
<button id="next">Next</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>
</body>
// This DataObjStub is a mock object exposing a getNextDomain
// and getPreviousDomain method. When calling those methods,
// a dataset with x ranging from 0 to 29 is created with random
// y values between 0 and 1.
// In an application, this should be replaced by a layer that
// is querying a data backend.
function DataObjStub() {
var firstDomainIndex = 0;
var lastDomainIndex = 3;
var nrOfDomainsShown = 5;
var getDummyDomain = function() {
var data = [];
for (var i=0; i<10; i++) {
var y = Math.random();
data.push({"x": i, "y": y});
}
return data;
}
this.getNextDomain = function() {
var data = getDummyDomain();
return data;
}
this.getPreviousDomain = function() {
var data = getDummyDomain();
return data;
}
}
function lineChart() {
// Display options
// width of the overall chart area
var w = 500;
// height of the overall chart area
var h = 200;
// Distance between first domain and left svg border
leftPadding = 50;
// width of one domain svg
var domainWidth = 60;
// height of one domain svg
var domainHeight = 190;
// spacing between two domains
var domainSpacing = 10;
// Other options
var domains = [];
var nrOfDomains = 5;
var dataObj = new DataObjStub();
var nrOfPrefetchedDomains = 1; // will fetch data for additional domains at each end of the chart
var totalNrOfDomains = nrOfDomains + (2 * nrOfPrefetchedDomains);
// Add main svg element
var mainSvg = d3.select("#linechart")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("id", "linecontainer");
// Add clipPath
var devs = mainSvg.append("defs");
devs.append("clipPath")
.attr("id", "lineclip")
.append("rect")
.attr("x", leftPadding)
.attr("y", 0)
.attr("width", w-leftPadding)
.attr("height", h);
// Create domain objects
for (var i=0; i<totalNrOfDomains; i++) {
var domainData = dataObj.getNextDomain();
var type = "visible";
if (i<nrOfPrefetchedDomains || i > totalNrOfDomains - nrOfPrefetchedDomains - 1) {
type = "prefetched";
}
var start = leftPadding + (i-nrOfPrefetchedDomains) * (domainWidth + domainSpacing);
var domain = {"data": domainData, "type": type, "x": start};
domains.push(domain);
}
// Create overall y-scale
var allYValues = getAllYValues();
var yScale = d3.scale.linear().domain(d3.extent(allYValues)).range([domainHeight, 0]);
// Create domains svg g elements
for (var i=0; i<totalNrOfDomains; i++) {
domain = domains[i];
domain = createDomain(domain, mainSvg, yScale); // adds svg with line chart
domains[i] = domain;
}
// Create y axis
drawYAxis();
/* --------------------------
* LineChart exposed functions
* --------------------------
*/
// this method will add a domain to the left of the chart
this.previous = function() {
var new_data = dataObj.getPreviousDomain();
domain = {"data": new_data, "x": leftPadding - ((nrOfPrefetchedDomains + 1) * (domainWidth + domainSpacing)), "type": "prefetched"};
domain = createDomain(domain, mainSvg, yScale);
domains.unshift(domain);
// One domain becomes visible
domains[nrOfPrefetchedDomains].type = "visible";
// One domain becomes invisible
domains[totalNrOfDomains - nrOfPrefetchedDomains].type = "prefetched";
moveDomainsToRight();
dropLastDomain();
rescaleDomains();
}
// this method will add a domain to the right of the chart
this.next = function() {
var new_data = dataObj.getNextDomain();
var x = leftPadding + (nrOfDomains + nrOfPrefetchedDomains) * (domainWidth + domainSpacing);
domain = {"data": new_data, "x": x, "type": "prefetched"};
domain = createDomain(domain, mainSvg, yScale);
domains.push(domain);
// This domain becomes visible
domains[totalNrOfDomains - nrOfPrefetchedDomains].type = "visible";
// This domain becomes invisible
domains[nrOfPrefetchedDomains].type = "prefetched";
moveDomainsToLeft();
dropFirstDomain();
rescaleDomains();
}
/* --------------------------
* LineChart helper functions
* --------------------------
*/
// Fetch all y values from all visible domains. This
// is used to calculate an overall y scale
function getAllYValues() {
var values = [];
for (var i=0; i<domains.length; i++) {
var d = domains[i];
if (d.type == "visible") {
for (var k=0; k<d.data.length; k++) {
values.push(d.data[k].y);
}
}
}
return values;
}
// Get data from the dataObj, create scales and
// define a line function that will draw the data.
// Mind you that the domain element given to this
// function should already contain a `data` and a
// `x` property.
function createDomain(domain, mainSvg, yScale) {
var x_domain = d3.extent(domain.data, function(d) {return d.x});
var x = d3.scale.linear().domain(x_domain).range([domain.x, (domain.x + domainWidth)]);
domain.xscale = x;
domain.yscale = yScale;
domain = drawLine(domain);
return domain;
}
// The actual drawing function
function drawLine(domain) {
lineFunction = createLineFunction(domain.xscale, domain.yscale);
var line = mainSvg.append("g")
.attr("clip-path", "url(#lineclip)");
var path = line.append("path")
.attr("class", "domain")
.attr("d", lineFunction(domain.data))
.attr("stroke", "black")
.attr("stroke-width", 2)
.attr("fill", "none");
if (domain.type == "prefetched") {
line.attr("visibility", "hidden");
} else {
line.attr("visibility", "visible")
}
domain.g = line;
return domain;
}
// This function will create a line function which
// is used to draw the lines based on their domain data
function createLineFunction(xscale, yscale) {
lineFunction = d3.svg.line()
.x(function(d) {return xscale(d["x"])})
.y(function(d) {return yscale(d["y"])})
.interpolate("linear");
return lineFunction;
}
// Draw the y axis
function drawYAxis() {
var yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
try {
var g = mainSvg.selectAll(".axis");
g.remove();
}
catch(err) {
console.log(err);
}
mainSvg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + (leftPadding - 10) + ", 0)")
.call(yAxis);
}
// Function for redrawing (used by rescale)
function reDrawLine(domain) {
// create a new line function that will be used by the g element
lineFunction = createLineFunction(domain.xscale, domain.yscale);
// animate the line to the new position
domain.g.selectAll("path").transition().attr("d", lineFunction(domain.data));
// Toggle visibility
if (domain.type == "prefetched") {
domain.g.attr("visibility", "hidden");
} else {
domain.g.attr("visibility", "visible");
}
}
// General function to move all lines in the chart area
function moveDomains(xDiff) {
for (var i=0; i<domains.length; i++) {
// calculate new x position for the line
var new_x = domains[i].x + xDiff;
// find x_domain
var x_domain = d3.extent(domains[i].data, function(d) {return d.x});
// create a new x scale
var new_xscale = d3.scale.linear().domain(x_domain).range([new_x, (new_x + domainWidth)]);
domains[i].x = new_x;
domains[i].xscale = new_xscale;
reDrawLine(domains[i]);
}
}
// Shift all domains to the right by one domain
function moveDomainsToRight() {
var xdiff = domainWidth + domainSpacing;
moveDomains(xdiff);
}
// Shift all domains to the left by one domain
function moveDomainsToLeft() {
var xdiff = -(domainWidth + domainSpacing);
moveDomains(xdiff);
}
// after adding a new domain, the existing domains should be rescaled
// and the y axis should be redrawn
function rescaleDomains() {
var allYValues = getAllYValues();
yScale = d3.scale.linear().domain(d3.extent(allYValues)).range([domainHeight, 0]);
for (var i=0; i<domains.length; i++) {
var domain = domains[i];
domain.yscale = yScale;
reDrawLine(domain);
domains[i] = domain;
}
drawYAxis();
}
function dropLastDomain() {
var g = domains[domains.length - 1].g;
g.remove();
domains.pop();
}
function dropFirstDomain() {
var g = domains[0].g;
g.remove();
domains.shift();
}
}
var lineChart = new lineChart();
/* -----------------------
* Bind UI controls
*/
$("#previous").click(function() {
lineChart.previous();
});
$("#next").click(function() {
lineChart.next();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment