Skip to content

Instantly share code, notes, and snippets.

@ptvans
Last active September 21, 2016 07:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ptvans/8fc90a54688dd9bc7c1136a6e5d42e51 to your computer and use it in GitHub Desktop.
Save ptvans/8fc90a54688dd9bc7c1136a6e5d42e51 to your computer and use it in GitHub Desktop.
1988 Porsche 959 Sport
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

forked from ptvans's block: 1988 Porsche 959 Sport

<!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>
<!--
1988 Porsche 959 Sport
2dr Coupe 6-cyl. 2849cc/575hp Twin Turbo FI
https://www.hagerty.com/apps/valuationtools/values/131827?yearRange=15
-->
<script src="multiline.js" charset="utf-8"></script>
<script type="text/javascript">
var parseDate = d3.time.format("%Y-%m-%dT%H:%M:%S").parse;
d3.csv('multiline_data.csv', function(error, data) {
data.forEach(function (d) {
d.date = parseDate(d.date);
d.value1 = +d.value1;
d.value2 = +d.value2;
d.value3 = +d.value3;
d.value4 = +d.value4;
});
var chart = makeLineChart(data, 'date', {
'Concours': {column: 'value1'},
'Excellent': {column: 'value2'},
'Good': {column: 'value3'},
'Fair': {column: 'value4'}
});
chart.bind({selector:"#chart-line1",chartSize:{height:452, width:960}, axisLabels: {xAxis:'Dates', yAxis: 'Values'}});
chart.render();
});
</script>
</body>
</html>
.chart-wrapper {
max-width: 650px;
min-width: 304px;
margin: 0 auto;
background-color: #f5f8f9;
}
.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: 1px;
}
.chart-wrapper .line {
fill: none;
stroke: steelblue;
stroke-width: 3px;
}
.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: white;
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.formatAsDate = d3.time.format("%d-%b-%y");
chart.xFormatter = chart.formatAsDate;
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.time.scale().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;
}
value1 value2 value3 value4 indexValue date averageValue buzz notes currency
310000 285000 240000 205000 100 2006-09-01T00:00:00 USD
310000 285000 240000 205000 100 2007-01-01T00:00:00 USD
345900 311000 281000 246000 109.12280701754386 2007-05-01T00:00:00 USD
358000 320000 288000 251000 112.28070175438596 2007-09-01T00:00:00 USD
350000 315000 285000 249000 110.52631578947368 2008-01-01T00:00:00 USD
396000 341000 297000 256000 119.64912280701755 2008-05-01T00:00:00 USD
396000 341000 297000 256000 119.64912280701755 2008-09-01T00:00:00 USD
396000 341000 297000 256000 119.64912280701755 2009-01-01T00:00:00 USD
396000 341000 297000 256000 119.64912280701755 2009-05-01T00:00:00 USD
396000 341000 297000 256000 119.64912280701755 2009-09-01T00:00:00 USD
396000 341000 297000 256000 119.64912280701755 2010-01-01T00:00:00 USD
396000 341000 297000 256000 119.64912280701755 2010-05-01T00:00:00 USD
436000 361000 311000 275000 126.66666666666667 2010-09-01T00:00:00 USD
462000 394000 320000 282000 138.24561403508773 2011-01-01T00:00:00 USD
450000 390000 311000 270000 136.8421052631579 2011-05-01T00:00:00 Prices on these incredibly sophisticated supercars have held firm the last several years. Not cheap, but a killer deal compared with rival Ferrari F40s, which seem positively brutish by comparison. USD
458000 396000 317000 274000 138.94736842105263 2011-09-01T00:00:00 USD
458000 396000 317000 274000 138.94736842105263 2012-01-01T00:00:00 USD
480000 411000 331000 278000 144.21052631578948 2012-05-01T00:00:00 USD
480000 411000 331000 278000 144.21052631578948 2012-09-01T00:00:00 USD
496000 423000 343000 285000 148.42105263157896 2013-01-01T00:00:00 USD
609000 533000 411000 328000 187.01754385964912 2013-05-01T00:00:00 USD
620000 549000 437000 360000 192.6315789473684 2013-09-01T00:00:00 USD
830000 690000 450000 375000 242.10526315789474 2014-01-01T00:00:00 USD
1000000 825000 595000 487000 289.4736842105263 2014-05-01T00:00:00 USD
1000000 825000 595000 487000 289.4736842105263 2014-09-01T00:00:00 USD
1200000 890000 667000 539000 312.280701754386 2015-01-01T00:00:00 USD
1700000 1200000 950000 827000 421.05263157894734 2015-05-01T00:00:00 USD
1750000 1250000 976000 844000 438.5964912280702 2015-09-01T00:00:00 USD
1800000 1300000 1000000 865000 456.140350877193 2016-01-01T00:00:00 USD
1850000 1350000 1050000 885000 473.6842105263158 2016-05-01T00:00:00 USD
1850000 1350000 1050000 875000 473.6842105263158 2016-09-01T00:00:00 USD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment