Skip to content

Instantly share code, notes, and snippets.

@asielen
Last active June 5, 2020 02:45
Show Gist options
  • Save asielen/44ffca2877d0132572cb to your computer and use it in GitHub Desktop.
Save asielen/44ffca2877d0132572cb to your computer and use it in GitHub Desktop.
Reusable Responsive Multiline Chart

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
@Misterfluff
Copy link

This is a great file. Very useful. My only question is, how do I add a threshold line? I have been struggling with this and I'm sure I'm missing something very obvious. Thanks for any assistance

@Paul760
Copy link

Paul760 commented Jun 4, 2020

Very nice multi line chart!
How can you change the x axis from "year" to "year-month-day" dates? Like 2020-06-04

@asielen
Copy link
Author

asielen commented Jun 5, 2020

@paul

Very nice multi line chart!
How can you change the x axis from "year" to "year-month-day" dates? Like 2020-06-04

I haven't tested it but the x axis is straight from the data so if you data is daily it should work fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment