An implementation of a reusable responsive multiline chart. Based on the concept outlined in Mike Bostocks blog post Towards Reusable Charts.
Open in new window to see the chart scale.
An implementation of a reusable responsive multiline chart. Based on the concept outlined in Mike Bostocks blog post Towards Reusable Charts.
Open in new window to see the chart scale.
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title></title> | |
<link rel="stylesheet" type="text/css" href="multiline.css"> | |
<script src="http://d3js.org/d3.v3.js" charset="utf-8"></script> | |
<!--<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>--> | |
</head> | |
<body> | |
<div class="chart-wrapper" id="chart-line1"></div> | |
<script type="text/javascript"> | |
d3.csv('multiline_data.csv', function(error, data) { | |
data.forEach(function (d) { | |
d.year = +d.year; | |
d.variableA = +d.variableA; | |
d.variableB = +d.variableB; | |
d.variableC = +d.variableC; | |
d.variableD = +d.variableD; | |
}); | |
var chart = makeLineChart(data, 'year', { | |
'Variable A': {column: 'variableA'}, | |
'Variable B': {column: 'variableB'}, | |
'Variable C': {column: 'variableC'}, | |
'Variable D': {column: 'variableD'} | |
}, {xAxis: 'Years', yAxis: 'Amount'}); | |
chart.bind("#chart-line1"); | |
chart.render(); | |
}); | |
</script> | |
<script src="multiline.js" charset="utf-8"></script> | |
</body> | |
</html> |
.chart-wrapper { | |
max-width: 950px; | |
min-width: 304px; | |
margin: 0 auto; | |
background-color: #FAF7F7; | |
} | |
.chart-wrapper .inner-wrapper { | |
position: relative; | |
padding-bottom: 50%; | |
width: 100%; | |
} | |
.chart-wrapper .outer-box { | |
position: absolute; | |
top: 0; bottom: 0; left: 0; right: 0; | |
} | |
.chart-wrapper .inner-box { | |
width: 100%; | |
height: 100%; | |
} | |
.chart-wrapper text { | |
font-family: sans-serif; | |
font-size: 11px; | |
} | |
.chart-wrapper p { | |
font-size: 16px; | |
margin-top:5px; | |
margin-bottom: 40px; | |
} | |
.chart-wrapper .axis path, | |
.chart-wrapper .axis line { | |
fill: none; | |
stroke: #1F1F2E; | |
stroke-opacity: 0.7; | |
shape-rendering: crispEdges; | |
} | |
.chart-wrapper .axis path { | |
stroke-width: 2px; | |
} | |
.chart-wrapper .line { | |
fill: none; | |
stroke: steelblue; | |
stroke-width: 5px; | |
} | |
.chart-wrapper .legend { | |
min-width: 200px; | |
display: flex; | |
justify-content: flex-start; | |
flex-wrap: wrap; | |
font-size: 16px; | |
padding: 10px 40px; | |
} | |
.chart-wrapper .legend > div { | |
margin: 0px 25px 10px 0px; | |
flex-grow: 0; | |
} | |
.chart-wrapper .legend p { | |
display:inline; | |
font-size: 0.8em; | |
font-family: sans-serif; | |
font-weight: 600; | |
} | |
.chart-wrapper .legend .series-marker { | |
height: 1em; | |
width: 1em; | |
border-radius: 35%; | |
background-color: crimson; | |
display: inline-block; | |
margin-right: 4px; | |
margin-bottom: -0.16rem; | |
} | |
.chart-wrapper .overlay { | |
fill: none; | |
pointer-events: all; | |
} | |
.chart-wrapper .focus circle { | |
fill: crimson; | |
stroke: crimson; | |
stroke-width: 2px; | |
fill-opacity: 15%; | |
} | |
.chart-wrapper .focus rect { | |
fill: lightblue; | |
opacity: 0.4; | |
border-radius: 2px; | |
} | |
.chart-wrapper .focus.line { | |
stroke: steelblue; | |
stroke-dasharray: 2,5; | |
stroke-width: 2; | |
opacity: 0.5; | |
} | |
@media (max-width:500px){ | |
.chart-wrapper .line {stroke-width: 3px;} | |
.chart-wrapper .legend {font-size: 14px;} | |
} |
function makeLineChart(dataset, xName, yObjs, axisLables) { | |
var chartObj = {}; | |
var color = d3.scale.category10(); | |
chartObj.xAxisLable = axisLables.xAxis; | |
chartObj.yAxisLable = axisLables.yAxis; | |
/* | |
yObjsects format: | |
{y1:{column:'',name:'name',color:'color'},y2} | |
*/ | |
chartObj.data = dataset; | |
chartObj.margin = {top: 15, right: 60, bottom: 30, left: 50}; | |
chartObj.width = 650 - chartObj.margin.left - chartObj.margin.right; | |
chartObj.height = 480 - chartObj.margin.top - chartObj.margin.bottom; | |
// So we can pass the x and y as strings when creating the function | |
chartObj.xFunct = function(d){return d[xName]}; | |
// For each yObjs argument, create a yFunction | |
function getYFn(column) { | |
return function (d) { | |
return d[column]; | |
}; | |
} | |
// Object instead of array | |
chartObj.yFuncts = []; | |
for (var y in yObjs) { | |
yObjs[y].name = y; | |
yObjs[y].yFunct = getYFn(yObjs[y].column); //Need this list for the ymax function | |
chartObj.yFuncts.push(yObjs[y].yFunct); | |
} | |
//Formatter functions for the axes | |
chartObj.formatAsNumber = d3.format(".0f"); | |
chartObj.formatAsDecimal = d3.format(".2f"); | |
chartObj.formatAsCurrency = d3.format("$.2f"); | |
chartObj.formatAsFloat = function (d) { | |
if (d % 1 !== 0) { | |
return d3.format(".2f")(d); | |
} else { | |
return d3.format(".0f")(d); | |
} | |
}; | |
chartObj.xFormatter = chartObj.formatAsNumber; | |
chartObj.yFormatter = chartObj.formatAsFloat; | |
chartObj.bisectYear = d3.bisector(chartObj.xFunct).left; //< Can be overridden in definition | |
//Create scale functions | |
chartObj.xScale = d3.scale.linear().range([0, chartObj.width]).domain(d3.extent(chartObj.data, chartObj.xFunct)); //< Can be overridden in definition | |
// Get the max of every yFunct | |
chartObj.max = function (fn) { | |
return d3.max(chartObj.data, fn); | |
}; | |
chartObj.yScale = d3.scale.linear().range([chartObj.height, 0]).domain([0, d3.max(chartObj.yFuncts.map(chartObj.max))]); | |
chartObj.formatAsYear = d3.format(""); | |
//Create axis | |
chartObj.xAxis = d3.svg.axis().scale(chartObj.xScale).orient("bottom").tickFormat(chartObj.xFormatter); //< Can be overridden in definition | |
chartObj.yAxis = d3.svg.axis().scale(chartObj.yScale).orient("left").tickFormat(chartObj.yFormatter); //< Can be overridden in definition | |
// Build line building functions | |
function getYScaleFn(yObj) { | |
return function (d) { | |
return chartObj.yScale(yObjs[yObj].yFunct(d)); | |
}; | |
} | |
for (var yObj in yObjs) { | |
yObjs[yObj].line = d3.svg.line().interpolate("cardinal").x(function (d) { | |
return chartObj.xScale(chartObj.xFunct(d)); | |
}).y(getYScaleFn(yObj)); | |
} | |
chartObj.svg; | |
// Change chart size according to window size | |
chartObj.update_svg_size = function () { | |
chartObj.width = parseInt(chartObj.chartDiv.style("width"), 10) - (chartObj.margin.left + chartObj.margin.right); | |
chartObj.height = parseInt(chartObj.chartDiv.style("height"), 10) - (chartObj.margin.top + chartObj.margin.bottom); | |
/* Update the range of the scale with new width/height */ | |
chartObj.xScale.range([0, chartObj.width]); | |
chartObj.yScale.range([chartObj.height, 0]); | |
if (!chartObj.svg) {return false;} | |
/* Else Update the axis with the new scale */ | |
chartObj.svg.select('.x.axis').attr("transform", "translate(0," + chartObj.height + ")").call(chartObj.xAxis); | |
chartObj.svg.select('.x.axis .label').attr("x", chartObj.width / 2); | |
chartObj.svg.select('.y.axis').call(chartObj.yAxis); | |
chartObj.svg.select('.y.axis .label').attr("x", -chartObj.height / 2); | |
/* Force D3 to recalculate and update the line */ | |
for (var y in yObjs) { | |
yObjs[y].path.attr("d", yObjs[y].line); | |
} | |
d3.selectAll(".focus.line").attr("y2", chartObj.height); | |
chartObj.chartDiv.select('svg').attr("width", chartObj.width + (chartObj.margin.left + chartObj.margin.right)).attr("height", chartObj.height + (chartObj.margin.top + chartObj.margin.bottom)); | |
chartObj.svg.select(".overlay").attr("width", chartObj.width).attr("height", chartObj.height); | |
return chartObj; | |
}; | |
chartObj.bind = function (selector) { | |
chartObj.mainDiv = d3.select(selector); | |
// Add all the divs to make it centered and responsive | |
chartObj.mainDiv.append("div").attr("class", "inner-wrapper").append("div").attr("class", "outer-box").append("div").attr("class", "inner-box"); | |
chartSelector = selector + " .inner-box"; | |
chartObj.chartDiv = d3.select(chartSelector); | |
d3.select(window).on('resize.' + chartSelector, chartObj.update_svg_size); | |
chartObj.update_svg_size(); | |
return chartObj; | |
}; | |
// Render the chart | |
chartObj.render = function () { | |
//Create SVG element | |
chartObj.svg = chartObj.chartDiv.append("svg").attr("class", "chart-area").attr("width", chartObj.width + (chartObj.margin.left + chartObj.margin.right)).attr("height", chartObj.height + (chartObj.margin.top + chartObj.margin.bottom)).append("g").attr("transform", "translate(" + chartObj.margin.left + "," + chartObj.margin.top + ")"); | |
// Draw Lines | |
for (var y in yObjs) { | |
yObjs[y].path = chartObj.svg.append("path").datum(chartObj.data).attr("class", "line").attr("d", yObjs[y].line).style("stroke", color(y)).attr("data-series", y).on("mouseover", function () { | |
focus.style("display", null); | |
}).on("mouseout", function () { | |
focus.transition().delay(700).style("display", "none"); | |
}).on("mousemove", mousemove); | |
} | |
// Draw Axis | |
chartObj.svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + chartObj.height + ")").call(chartObj.xAxis).append("text").attr("class", "label").attr("x", chartObj.width / 2).attr("y", 30).style("text-anchor", "middle").text(chartObj.xAxisLable); | |
chartObj.svg.append("g").attr("class", "y axis").call(chartObj.yAxis).append("text").attr("class", "label").attr("transform", "rotate(-90)").attr("y", -42).attr("x", -chartObj.height / 2).attr("dy", ".71em").style("text-anchor", "middle").text(chartObj.yAxisLable); | |
//Draw tooltips | |
var focus = chartObj.svg.append("g").attr("class", "focus").style("display", "none"); | |
for (var y in yObjs) { | |
yObjs[y].tooltip = focus.append("g"); | |
yObjs[y].tooltip.append("circle").attr("r", 5); | |
yObjs[y].tooltip.append("rect").attr("x", 8).attr("y","-5").attr("width",22).attr("height",'0.75em'); | |
yObjs[y].tooltip.append("text").attr("x", 9).attr("dy", ".35em"); | |
} | |
// Year label | |
focus.append("text").attr("class", "focus year").attr("x", 9).attr("y", 7); | |
// Focus line | |
focus.append("line").attr("class", "focus line").attr("y1", 0).attr("y2", chartObj.height); | |
//Draw legend | |
var legend = chartObj.mainDiv.append('div').attr("class", "legend"); | |
for (var y in yObjs) { | |
series = legend.append('div'); | |
series.append('div').attr("class", "series-marker").style("background-color", color(y)); | |
series.append('p').text(y); | |
yObjs[y].legend = series; | |
} | |
// Overlay to capture hover | |
chartObj.svg.append("rect").attr("class", "overlay").attr("width", chartObj.width).attr("height", chartObj.height).on("mouseover", function () { | |
focus.style("display", null); | |
}).on("mouseout", function () { | |
focus.style("display", "none"); | |
}).on("mousemove", mousemove); | |
return chartObj; | |
function mousemove() { | |
var x0 = chartObj.xScale.invert(d3.mouse(this)[0]), i = chartObj.bisectYear(dataset, x0, 1), d0 = chartObj.data[i - 1], d1 = chartObj.data[i]; | |
try { | |
var d = x0 - chartObj.xFunct(d0) > chartObj.xFunct(d1) - x0 ? d1 : d0; | |
} catch (e) { return;} | |
minY = chartObj.height; | |
for (var y in yObjs) { | |
yObjs[y].tooltip.attr("transform", "translate(" + chartObj.xScale(chartObj.xFunct(d)) + "," + chartObj.yScale(yObjs[y].yFunct(d)) + ")"); | |
yObjs[y].tooltip.select("text").text(chartObj.yFormatter(yObjs[y].yFunct(d))); | |
minY = Math.min(minY, chartObj.yScale(yObjs[y].yFunct(d))); | |
} | |
focus.select(".focus.line").attr("transform", "translate(" + chartObj.xScale(chartObj.xFunct(d)) + ")").attr("y1", minY); | |
focus.select(".focus.year").text("Year: " + chartObj.xFormatter(chartObj.xFunct(d))); | |
} | |
}; | |
return chartObj; | |
} |
year | variableA | variableB | variableC | variableD | |
---|---|---|---|---|---|
1980 | 70 | 52 | 145 | 75 | |
1981 | 77 | 51 | 156 | 80 | |
1982 | 81 | 55 | 169 | 79 | |
1983 | 78 | 55 | 171 | 91 | |
1984 | 80 | 55 | 187 | 102 | |
1985 | 79 | 53 | 199 | 103 | |
1986 | 79 | 54 | 204 | 102 | |
1987 | 78 | 53 | 218 | 104 | |
1988 | 75 | 51 | 232 | 105 | |
1989 | 78 | 48 | 233 | 106 | |
1990 | 76 | 51 | 233 | 112 | |
1991 | 73 | 55 | 232 | 111 | |
1992 | 70 | 52 | 240 | 122 | |
1993 | 69 | 50 | 256 | 122 | |
1994 | 74 | 50 | 273 | 131 | |
1995 | 71 | 51 | 286 | 128 | |
1996 | 71 | 53 | 283 | 129 | |
1997 | 76 | 51 | 292 | 126 | |
1998 | 81 | 49 | 298 | 132 | |
1999 | 80 | 53 | 313 | 142 | |
2000 | 77 | 59 | 321 | 152 | |
2001 | 82 | 63 | 338 | 162 | |
2002 | 88 | 67 | 337 | 171 | |
2003 | 90 | 69 | 338 | 177 | |
2004 | 90 | 75 | 338 | 183 | |
2005 | 92 | 80 | 351 | 180 | |
2006 | 93 | 87 | 367 | 188 | |
2007 | 91 | 91 | 375 | 186 | |
2008 | 90 | 96 | 374 | 195 | |
2009 | 97 | 97 | 385 | 207 | |
2010 | 104 | 101 | 401 | 206 | |
2011 | 111 | 106 | 403 | 205 | |
2012 | 115 | 105 | 417 | 204 | |
2013 | 117 | 108 | 420 | 211 | |
2014 | 121 | 107 | 436 | 217 | |
2015 | 121 | 104 | 449 | 216 |
@paul
I haven't tested it but the x axis is straight from the data so if you data is daily it should work fine.