Skip to content

Instantly share code, notes, and snippets.

@ptvans
Last active February 8, 2019 23:48
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/3aabc53dcd4e4875087c134d042b1fc6 to your computer and use it in GitHub Desktop.
Save ptvans/3aabc53dcd4e4875087c134d042b1fc6 to your computer and use it in GitHub Desktop.
1977 Datsun 280Z
license: mit
border:

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

forked from ptvans's block: 1988 Porsche 959 Sport

forked from ptvans's block: 1994 Porsche 911 Carrera Turbo

forked from ptvans's block: 2003 BMW Z8

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="multiline.css">
<script src="https://d3js.org/d3.v3.js"></script>
</head>
<body>
<div class="chart-wrapper" id="chart-line1"></div>
<!--
1977 Datsun 280Z
2dr Coupe 6-cyl. 2753cc/149hp Bosch L-Jetronic FI
https://www.hagerty.com/apps/valuationtools/values/49875?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.category20c();
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
15300 10200 6900 4400 100 2006-09-01T00:00:00 USD
15300 10200 6900 4400 100 2007-01-01T00:00:00 USD
15300 10200 6900 4400 100 2007-05-01T00:00:00 USD
15300 10200 6900 4400 100 2007-09-01T00:00:00 USD
20000 12800 7000 4600 125.49019607843137 2008-01-01T00:00:00 Newsflash, these are now no longer free on your nearest supermarket bulletin board. Injected cars are more civilized than the 240, with the same pretty body under the bumpers. USD
20000 12800 7000 4500 125.49019607843137 2008-05-01T00:00:00 USD
20000 12800 7000 4500 125.49019607843137 2008-09-01T00:00:00 USD
19850 12700 6800 4250 124.50980392156863 2009-01-01T00:00:00 USD
19850 13000 6800 4100 127.45098039215686 2009-05-01T00:00:00 USD
19850 13000 6800 4100 127.45098039215686 2009-09-01T00:00:00 USD
19850 13000 6800 4000 127.45098039215686 2010-01-01T00:00:00 USD
19950 13600 6800 4000 133.33333333333334 2010-05-01T00:00:00 USD
19600 13400 6800 4000 131.37254901960785 2010-09-01T00:00:00 USD
19300 13100 6600 3850 128.4313725490196 2011-01-01T00:00:00 USD
18900 12900 6500 3600 126.47058823529412 2011-05-01T00:00:00 USD
18900 12900 6500 3600 126.47058823529412 2011-09-01T00:00:00 USD
18900 12900 6500 3600 126.47058823529412 2012-01-01T00:00:00 USD
19200 13250 6500 3600 129.90196078431373 2012-05-01T00:00:00 USD
19200 13250 6500 3600 129.90196078431373 2012-09-01T00:00:00 USD
19200 13250 6500 3600 129.90196078431373 2013-01-01T00:00:00 USD
19200 13250 6500 3600 129.90196078431373 2013-05-01T00:00:00 USD
19200 13400 6500 3600 131.37254901960785 2013-09-01T00:00:00 USD
19000 13300 6400 3600 130.3921568627451 2014-01-01T00:00:00 USD
19800 13900 6600 3700 136.27450980392157 2014-05-01T00:00:00 USD
21800 15300 7300 4100 150 2014-09-01T00:00:00 USD
22300 15900 7700 4200 155.88235294117646 2015-01-01T00:00:00 USD
29200 17300 8400 4400 169.6078431372549 2015-05-01T00:00:00 USD
29200 17300 8400 4400 169.6078431372549 2015-09-01T00:00:00 -15% for auto trans. USD
29200 17300 8500 4400 169.6078431372549 2016-01-01T00:00:00 -15% for auto trans. USD
29500 17400 8600 4400 170.58823529411765 2016-05-01T00:00:00 -15% for auto trans. USD
29500 17400 8700 4400 170.58823529411765 2016-09-01T00:00:00 -15% for auto trans. USD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment