Skip to content

Instantly share code, notes, and snippets.

@sunole
Created February 20, 2018 22:43
Show Gist options
  • Save sunole/699156684769b6bb82d4ac609591b855 to your computer and use it in GitHub Desktop.
Save sunole/699156684769b6bb82d4ac609591b855 to your computer and use it in GitHub Desktop.
Reusable Line Chart v2
license: mit

An implementation of a reusable responsive multiline chart. Based on the concept outlined in Mike Bostocks blog post Towards Reusable Charts.

Features:

  • Reusable modular design
  • Responsive design, chart size adjusts with screen size
  • Customizable options
    • Chart Size
    • Margins
    • Div Selector
    • Chart Colors
    • Axis labels
  • Toggleable series (click on the legend to toggle series)

Previous version: Reusable Responsive Multiline Chart

forked from asielen's block: Reusable Line Chart v2

<!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 src="multiline.js" charset="utf-8"></script>
<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'}
});
chart.bind({selector:"#chart-line1",chartSize:{height:452, width:960}, axisLabels: {xAxis:'Years', yAxis: 'Values'}});
chart.render();
});
</script>
</body>
</html>
.chart-wrapper {
max-width: 650px;
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;
cursor: pointer;
}
.chart-wrapper .legend .series-marker:hover {
opacity: 0.8;
}
.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 .tooltip circle {
fill: black;
stroke: crimson;
stroke-width: 2px;
fill-opacity: 25%;
}
.chart-wrapper .tooltip rect {
fill: #ecf0f4;
opacity: 0.7;
border-radius: 2px;
}
.chart-wrapper .tooltip text {
font-size: 14px;
}
.chart-wrapper .tooltip .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, yNames) {
/*
* dataset = the csv file
* xName = the name of the column to use as the x axes
* yNames = the columns to use for y values
*
* */
var chart = {};
chart.data = dataset;
chart.xName = xName;
chart.yNames = yNames;
chart.groupObjs = {}; //The data organized by grouping and sorted as well as any metadata for the groups
chart.objs = {mainDiv: null, chartDiv: null, g: null, xAxis: null, yAxis: null, tooltip:null, legend:null};
var colorFunct = d3.scale.category10();
function updateColorFunction(colorOptions) {
/*
* Takes either a list of colors, a function or an object with the mapping already in place
* */
if (typeof colorOptions == 'function') {
return colorOptions
} else if (Array.isArray(colorOptions)) {
// If an array is provided, map it to the domain
var colorMap = {}, cColor = 0;
for (var cName in chart.groupObjs) {
colorMap[cName] = colorOptions[cColor];
cColor = (cColor + 1) % colorOptions.length;
}
return function (group) {
return colorMap[group];
}
} else if (typeof colorOptions == 'object') {
// if an object is provided, assume it maps to the colors
return function (group) {
return colorOptions[group];
}
}
}
//Formatter functions for the axes
chart.formatAsNumber = d3.format(".0f");
chart.formatAsDecimal = d3.format(".2f");
chart.formatAsCurrency = d3.format("$.2f");
chart.formatAsFloat = function(d) {if(d%1!==0){return d3.format(".2f")(d);}else{return d3.format(".0f")(d);}};
chart.formatAsYear = d3.format("");
chart.xFormatter = chart.formatAsNumber;
chart.yFormatter = chart.formatAsFloat;
function getYFuncts() {
// Return a list of all *visible* y functions
var yFuncts = [];
for (var yName in chart.groupObjs) {
currentGroup = chart.groupObjs[yName];
if (currentGroup.visible == true) {
yFuncts.push(currentGroup.yFunct);
}
}
return yFuncts
}
function getYMax () {
// Get the max y value of all *visible* y lines
return d3.max(getYFuncts().map(function(fn){
return d3.max(chart.data, fn);
}))
}
function prepareData() {
chart.xFunct = function(d){return d[xName]};
chart.bisectYear = d3.bisector(chart.xFunct).left;
var yName, cY;
for (yName in chart.yNames) {
chart.groupObjs[yName] = {yFunct:null, visible:null, objs:{}};
}
// For each yName argument, create a yFunction
function getYFn(column) {
return function (d) {
return d[column];
};
}
// Object instead of array
chart.yFuncts = [];
for (yName in chart.yNames) {
cY = chart.groupObjs[yName];
cY.visible = true;
cY.yFunct = getYFn(chart.yNames[yName].column);
}
}
prepareData();
chart.update = function () {
chart.width = parseInt(chart.objs.chartDiv.style("width"), 10) - (chart.margin.left + chart.margin.right);
chart.height = parseInt(chart.objs.chartDiv.style("height"), 10) - (chart.margin.top + chart.margin.bottom);
/* Update the range of the scale with new width/height */
chart.xScale.range([0, chart.width]);
chart.yScale.range([chart.height, 0]).domain([0, getYMax()]);
if (!chart.objs.g) {return false;}
/* Else Update the axis with the new scale */
chart.objs.axes.g.select('.x.axis').attr("transform", "translate(0," + chart.height + ")").call(chart.objs.xAxis);
chart.objs.axes.g.select('.x.axis .label').attr("x", chart.width / 2);
chart.objs.axes.g.select('.y.axis').call(chart.objs.yAxis);
chart.objs.axes.g.select('.y.axis .label').attr("x", -chart.height / 2);
/* Force D3 to recalculate and update the line */
for (var yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
if (cY.visible==true) {
cY.objs.line.g.attr("d", cY.objs.line.series).style("display",null);
cY.objs.tooltip.style("display",null);
} else {
cY.objs.line.g.style("display","none");
cY.objs.tooltip.style("display","none");
}
}
chart.objs.tooltip.select('.line').attr("y2", chart.height);
chart.objs.chartDiv.select('svg').attr("width", chart.width + (chart.margin.left + chart.margin.right)).attr("height", chart.height + (chart.margin.top + chart.margin.bottom));
chart.objs.g.select(".overlay").attr("width", chart.width).attr("height", chart.height);
return chart;
};
chart.bind = function (bindOptions) {
function getOptions() {
if (!bindOptions) throw "Missing Bind Options";
if (bindOptions.selector) {
chart.objs.mainDiv = d3.select(bindOptions.selector);
// Capture the inner div for the chart (where the chart actually is)
chart.selector = bindOptions.selector + " .inner-box";
} else {throw "No Selector Provided"}
if (bindOptions.margin) {
chart.margin = margin;
} else {
chart.margin = {top: 15, right: 60, bottom: 30, left: 50};
}
if (bindOptions.chartSize) {
chart.divWidth = bindOptions.chartSize.width;
chart.divHeight = bindOptions.chartSize.height;
} else {
chart.divWidth = 800;
chart.divHeight = 400;
}
chart.width = chart.divWidth - chart.margin.left - chart.margin.right;
chart.height = chart.divHeight - chart.margin.top - chart.margin.bottom;
if (bindOptions.axisLabels) {
chart.xAxisLable = bindOptions.axisLabels.xAxis;
chart.yAxisLable = bindOptions.axisLabels.yAxis;
} else {
chart.xAxisLable = chart.xName;
chart.yAxisLable = chart.yNames[0];
}
if (bindOptions.colors) {
colorFunct = updateColorFunction(bindOptions.colors);
}
}
getOptions();
chart.xScale = d3.scale.linear().range([0, chart.width]).domain(d3.extent(chart.data, chart.xFunct));
chart.yScale = d3.scale.linear().range([chart.height, 0]).domain([0, getYMax()]);
//Create axis
chart.objs.xAxis = d3.svg.axis()
.scale(chart.xScale)
.orient("bottom")
.tickFormat(chart.xFormatter);
chart.objs.yAxis = d3.svg.axis()
.scale(chart.yScale)
.orient("left")
.tickFormat(chart.yFormatter);
// Build line building functions
function getYScaleFn(yName) {
return function (d) {
return chart.yScale(chart.groupObjs[yName].yFunct(d));
};
}
// Create lines (as series)
for (var yName in yNames) {
var cY = chart.groupObjs[yName];
cY.objs.line = {g:null, series:null};
cY.objs.line.series = d3.svg.line()
.interpolate("cardinal")
.x(function (d) {return chart.xScale(chart.xFunct(d));})
.y(getYScaleFn(yName));
}
chart.objs.mainDiv.style("max-width", chart.divWidth + "px");
// Add all the divs to make it centered and responsive
chart.objs.mainDiv.append("div")
.attr("class", "inner-wrapper")
.style("padding-bottom", (chart.divHeight / chart.divWidth) * 100 + "%")
.append("div").attr("class", "outer-box")
.append("div").attr("class", "inner-box");
chart.objs.chartDiv = d3.select(chart.selector);
d3.select(window).on('resize.' + chart.selector, chart.update);
// Create the svg
chart.objs.g = chart.objs.chartDiv.append("svg")
.attr("class", "chart-area")
.attr("width", chart.width + (chart.margin.left + chart.margin.right))
.attr("height", chart.height + (chart.margin.top + chart.margin.bottom))
.append("g")
.attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")");
chart.objs.axes = {};
chart.objs.axes.g = chart.objs.g.append("g").attr("class", "axis");
// Show axis
chart.objs.axes.x = chart.objs.axes.g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + chart.height + ")")
.call(chart.objs.xAxis)
.append("text")
.attr("class", "label")
.attr("x", chart.width / 2)
.attr("y", 30)
.style("text-anchor", "middle")
.text(chart.xAxisLable);
chart.objs.axes.y = chart.objs.axes.g.append("g")
.attr("class", "y axis")
.call(chart.objs.yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", -42)
.attr("x", -chart.height / 2)
.attr("dy", ".71em")
.style("text-anchor", "middle")
.text(chart.yAxisLable);
return chart;
};
chart.render = function () {
var yName,
cY=null;
chart.objs.legend = chart.objs.mainDiv.append('div').attr("class", "legend");
function toggleSeries(yName) {
cY = chart.groupObjs[yName];
cY.visible = !cY.visible;
if (cY.visible==false) {cY.objs.legend.div.style("opacity","0.3")} else {cY.objs.legend.div.style("opacity","1")}
chart.update()
}
function getToggleFn(series) {
return function () {
return toggleSeries(series);
};
}
for (yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
cY.objs.g = chart.objs.g.append("g");
cY.objs.line.g = cY.objs.g.append("path")
.datum(chart.data)
.attr("class", "line")
.attr("d", cY.objs.line.series)
.style("stroke", colorFunct(yName))
.attr("data-series", yName)
.on("mouseover", function () {
tooltip.style("display", null);
}).on("mouseout", function () {
tooltip.transition().delay(700).style("display", "none");
}).on("mousemove", mouseHover);
cY.objs.legend = {};
cY.objs.legend.div = chart.objs.legend.append('div').on("click",getToggleFn(yName));
cY.objs.legend.icon = cY.objs.legend.div.append('div')
.attr("class", "series-marker")
.style("background-color", colorFunct(yName));
cY.objs.legend.text = cY.objs.legend.div.append('p').text(yName);
}
//Draw tooltips
//Themust be a better way so we don't need a second loop. Issue is draw order so tool tips are on top
chart.objs.tooltip = chart.objs.g.append("g").attr("class", "tooltip").style("display", "none");
// Year label
chart.objs.tooltip.append("text").attr("class", "year").attr("x", 9).attr("y", 7);
// Focus line
chart.objs.tooltip.append("line").attr("class", "line").attr("y1", 0).attr("y2", chart.height);
for (yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
//Add tooltip elements
var tooltip = chart.objs.tooltip.append("g");
cY.objs.circle = tooltip.append("circle").attr("r", 5);
cY.objs.rect = tooltip.append("rect").attr("x", 8).attr("y","-5").attr("width",22).attr("height",'0.75em');
cY.objs.text = tooltip.append("text").attr("x", 9).attr("dy", ".35em").attr("class","value");
cY.objs.tooltip = tooltip;
}
// Overlay to capture hover
chart.objs.g.append("rect")
.attr("class", "overlay")
.attr("width", chart.width)
.attr("height", chart.height)
.on("mouseover", function () {
chart.objs.tooltip.style("display", null);
}).on("mouseout", function () {
chart.objs.tooltip.style("display", "none");
}).on("mousemove", mouseHover);
return chart;
function mouseHover() {
var x0 = chart.xScale.invert(d3.mouse(this)[0]), i = chart.bisectYear(dataset, x0, 1), d0 = chart.data[i - 1], d1 = chart.data[i];
try {
var d = x0 - chart.xFunct(d0) > chart.xFunct(d1) - x0 ? d1 : d0;
} catch (e) { return;}
var minY = chart.height;
var yName, cY;
for (yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
if (cY.visible==false) {continue}
//Move the tooltip
cY.objs.tooltip.attr("transform", "translate(" + chart.xScale(chart.xFunct(d)) + "," + chart.yScale(cY.yFunct(d)) + ")");
//Change the text
cY.objs.tooltip.select("text").text(chart.yFormatter(cY.yFunct(d)));
minY = Math.min(minY, chart.yScale(cY.yFunct(d)));
}
chart.objs.tooltip.select(".tooltip .line").attr("transform", "translate(" + chart.xScale(chart.xFunct(d)) + ")").attr("y1", minY);
chart.objs.tooltip.select(".tooltip .year").text("Year: " + chart.xFormatter(chart.xFunct(d)));
}
};
return chart;
}
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment