Skip to content

Instantly share code, notes, and snippets.

@HamishWoodrow
Last active August 1, 2017 17:18
Show Gist options
  • Save HamishWoodrow/9ac43df1252502dbec7ca5c9ccb34ea8 to your computer and use it in GitHub Desktop.
Save HamishWoodrow/9ac43df1252502dbec7ca5c9ccb34ea8 to your computer and use it in GitHub Desktop.
Viewership_Cult
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', {
'Breaking Bad': {column: 'variableA'},
'Game of Thrones': {column: 'variableB'},
'Lost': {column: 'variableC'},
'South Park': {column: 'variableD'}
});
chart.bind({selector:"#chart-line1",chartSize:{height:452, width:960}, axisLabels: {xAxis:'Years', yAxis: 'Relative % of Hits'}});
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
2004.083333 0 0 29 10
2004.166667 0 0 33 8
2004.25 0 0 28 11
2004.333333 0 0 26 13
2004.416667 0 0 26 12
2004.5 0 0 25 9
2004.583333 0 0 26 10
2004.666667 0 0 26 9
2004.75 0 0 30 9
2004.833333 0 0 35 11
2004.916667 0 0 32 11
2005 0 0 34 11
2005.083333 0 0 36 9
2005.166667 0 0 33 8
2005.25 0 0 34 10
2005.333333 0 0 37 12
2005.416667 0 0 42 11
2005.5 0 0 33 10
2005.583333 0 0 29 9
2005.666667 0 0 31 8
2005.75 0 0 54 8
2005.833333 0 0 54 11
2005.916667 0 0 48 13
2006 0 0 44 13
2006.083333 0 0 47 11
2006.166667 0 0 47 10
2006.25 0 0 46 17
2006.333333 0 0 48 17
2006.416667 0 0 69 13
2006.5 0 0 36 11
2006.583333 0 0 35 11
2006.666667 0 0 35 10
2006.75 0 0 41 11
2006.833333 0 0 57 19
2006.916667 0 0 44 16
2007 0 0 36 13
2007.083333 0 0 41 10
2007.166667 0 0 51 11
2007.25 0 0 49 18
2007.333333 0 0 48 18
2007.416667 0 0 66 13
2007.5 0 0 36 12
2007.583333 0 0 34 13
2007.666667 0 0 33 13
2007.75 0 0 36 12
2007.833333 0 0 35 17
2007.916667 0 0 31 19
2008 0 0 34 15
2008.083333 1 0 45 11
2008.166667 2 0 70 11
2008.25 2 0 58 17
2008.333333 1 0 44 22
2008.416667 0 0 58 16
2008.5 0 0 41 14
2008.583333 0 0 37 14
2008.666667 0 0 35 13
2008.75 1 0 35 12
2008.833333 1 0 33 19
2008.916667 0 0 34 20
2009 0 0 37 14
2009.083333 0 0 57 12
2009.166667 1 0 59 11
2009.25 3 0 54 17
2009.333333 2 0 52 22
2009.416667 3 0 58 15
2009.5 2 0 42 13
2009.583333 1 0 37 13
2009.666667 1 0 37 12
2009.75 1 0 39 12
2009.833333 1 0 37 19
2009.916667 1 0 37 19
2010 1 0 37 14
2010.083333 1 0 46 12
2010.166667 1 0 68 11
2010.25 4 0 57 16
2010.333333 4 0 59 29
2010.416667 4 0 100 17
2010.5 5 0 42 13
2010.583333 2 0 36 12
2010.666667 2 0 36 12
2010.75 2 0 33 11
2010.833333 2 0 33 19
2010.916667 2 0 31 17
2011 2 1 32 13
2011.083333 2 1 36 12
2011.166667 2 1 33 11
2011.25 2 2 33 11
2011.333333 2 11 33 14
2011.416667 2 13 33 18
2011.5 3 19 36 19
2011.583333 8 10 35 14
2011.666667 7 7 34 12
2011.75 10 7 33 11
2011.833333 14 5 34 16
2011.916667 8 5 33 15
2012 7 6 33 12
2012.083333 7 6 35 10
2012.166667 6 6 33 10
2012.25 5 12 33 13
2012.333333 5 27 33 14
2012.416667 5 26 34 11
2012.5 9 23 34 12
2012.583333 23 13 35 11
2012.666667 21 9 33 10
2012.75 19 8 34 10
2012.833333 9 6 32 13
2012.916667 8 6 33 11
2013 8 8 32 10
2013.083333 9 9 35 9
2013.166667 7 11 33 8
2013.25 8 21 34 9
2013.333333 7 35 33 8
2013.416667 7 31 33 9
2013.5 10 45 34 9
2013.583333 14 16 35 9
2013.666667 48 12 34 9
2013.75 77 11 33 10
2013.833333 32 9 34 12
2013.916667 17 10 34 12
2014 18 13 34 12
2014.083333 17 17 36 9
2014.166667 12 16 35 10
2014.25 10 20 38 21
2014.333333 7 60 34 12
2014.416667 7 42 34 11
2014.5 7 52 34 11
2014.583333 7 19 35 11
2014.666667 10 14 33 10
2014.75 8 11 32 11
2014.833333 8 10 31 14
2014.916667 5 10 32 13
2015 6 13 33 12
2015.083333 6 14 34 9
2015.166667 7 14 33 8
2015.25 5 17 32 8
2015.333333 4 42 33 9
2015.416667 3 35 34 9
2015.5 4 50 35 10
2015.583333 4 16 37 10
2015.666667 4 12 36 9
2015.75 3 11 34 12
2015.833333 4 10 34 13
2015.916667 3 10 34 12
2016 3 13 34 11
2016.083333 4 12 36 8
2016.166667 4 11 35 8
2016.25 4 15 35 8
2016.333333 4 39 37 8
2016.416667 4 56 39 8
2016.5 3 64 38 10
2016.583333 3 26 37 10
2016.666667 3 13 34 9
2016.75 3 11 32 12
2016.833333 2 10 32 12
2016.916667 2 9 34 11
2017 4 12 34 9
2017.083333 4 13 34 7
2017.166667 3 11 33 7
2017.25 3 12 34 7
2017.333333 4 11 38 7
2017.416667 3 13 34 8
2017.5 3 18 33 8
2017.583333 4 68 37 9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment