This two series chart toggles between a multi-line chart and a difference area chart. When mouseing over the chart, the closest data point will display it's y-axis measurement.
Last active
July 12, 2017 21:00
-
-
Save TommyCoin80/ee23cb1461d04d020d1458c9400861d7 to your computer and use it in GitHub Desktop.
Multi-Line and Difference Area Chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<head> | |
<link href='https://fonts.googleapis.com/css?family=Play' rel='stylesheet' type='text/css'> | |
<style> | |
body { | |
margin:auto; | |
font-family: 'Play', sans-serif; | |
font-size:100%; | |
} | |
text { | |
font-family: 'Play', sans-serif; | |
} | |
</style> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script> | |
var LineChart = function(opts) { | |
this.elementId = opts.elementId; | |
this.unit = {x:opts.x,y:opts.y} || {x:"x",y:"y"}; | |
this.height = opts.height || 500; | |
this.width = opts.width || 960; | |
this.margin = opts.margin || {top:10,left:50,bottom:100,right:10}; | |
this.label = {}; | |
this.label.multi = opts.multiLabels || this.unit.y; | |
this.label.difference = opts.difLabels || ["Decrease","Increase"] | |
this.curve = d3.curveLinear; | |
this.s = {}; | |
this.duration = opts.duration || 250; | |
this.data = {}; | |
this.data.toggles = opts.toggles || ["Multi","Difference"]; | |
this.prepData(opts.data); | |
this.setScales(); | |
this.axis = {}; | |
this.axis.labels = opts.axisLabels || ["Count","Difference"] | |
this.setGrids(); | |
this.setAxes(); | |
this.draw(); | |
this.drawAxes(); | |
this.drawGrids(); | |
this.drawLines(); | |
this.drawTitle(opts.title || ""); | |
this.drawFocus(); | |
this.drawToggle(); | |
this.drawLegend(); | |
this.drawVoronoi(d3.merge(this.data.nested.map(function(d) { return d.values;})), this.scale.y); | |
} | |
LineChart.prototype.drawTitle = function(title) { | |
this.s.chart.append("g") | |
.attr("transform", "translate(" + (this.width + this.margin.right)/2 + "," + ( -15) + ")") | |
.append("text") | |
.text(title) | |
.style("text-anchor","middle") | |
.style("fill","black") | |
.style("font-size", "2em") | |
} | |
LineChart.prototype.drawToggle = function() { | |
var _this = this, | |
toggleHeight = 30; | |
this.view = this.data.toggles[0]; | |
var toggles = this.s.svg.append("g").attr("transform","translate(" + | |
(this.margin.left + this.width*(3/4) - this.scale.toggle.range()[1]/2) + "," + | |
(this.margin.top + this.height + this.margin.bottom - toggleHeight - 2) + ")") | |
.selectAll(".toggle") | |
.data(this.data.toggles) | |
.enter() | |
.append("g") | |
.attr("transform", function(d) { return "translate("+ _this.scale.toggle(d) + ",0)"}) | |
.style("cursor","pointer") | |
.on("click", function(d) { _this.clickToggle(d);}) | |
toggles.append("rect") | |
.attr("width", this.scale.toggle.bandwidth()) | |
.attr("height", toggleHeight) | |
.style("fill","white") | |
.style("stroke-width",1) | |
.style("stroke","black") | |
toggles.append("text") | |
.text(function(d) { return d;}) | |
.style("text-anchor","middle") | |
.attr("dx", this.scale.toggle.bandwidth()/2) | |
.attr("dy", 20); | |
this.s.slider = this.s.svg.append("g").attr("transform","translate(" + | |
(this.margin.left + this.width*(3/4) - this.scale.toggle.range()[1]/2) + "," + | |
(this.margin.top + this.height + this.margin.bottom - toggleHeight - 2) + ")") | |
.append("rect") | |
.attr("width", _this.scale.toggle.bandwidth()) | |
.attr("height", toggleHeight) | |
.style("fill","none") | |
.style("stroke","black") | |
.style("stroke-width",4) | |
} | |
LineChart.prototype.drawLegend = function() { | |
var _this = this, | |
legendHeight = 30; | |
var legend = this.s.svg.append("g").attr("transform","translate(" + | |
(this.margin.left + this.width*(1/4) - this.scale.legend.range()[1]/2) + "," + | |
(this.margin.top + this.height + this.margin.bottom - legendHeight - 2) + ")") | |
.selectAll(".legend") | |
.data(this.label.multi) | |
.enter() | |
.append("g") | |
.attr("transform", function(d) { return "translate("+ _this.scale.legend(d) + ",0)"}) | |
legend.append("rect") | |
.attr("width", this.scale.legend.bandwidth()) | |
.attr("height", legendHeight) | |
.style("fill",function(d,i) { return _this.scale.color(_this.unit.y[i])}) | |
this.s.legend = legend.append("text") | |
.text(function(d) { return d;}) | |
.style("fill","white") | |
.style("text-anchor","middle") | |
.attr("dx", this.scale.legend.bandwidth()/2) | |
.attr("dy", 20); | |
} | |
LineChart.prototype.clickToggle = function(d) { | |
if(d == this.view) return; | |
this.view = d; | |
this.s.slider.transition() | |
.duration(this.duration) | |
.attr("x", this.scale.toggle(d)); | |
if(this.view == this.data.toggles[0]) { | |
this.toggleMulti(); | |
} else { | |
this.toggleDifference(); | |
} | |
} | |
LineChart.prototype.toggleDifference = function() { | |
var _this = this; | |
this.s.axis.left.transition() | |
.duration(this.duration) | |
.call(_this.axis.difference); | |
this.s.grid.transition() | |
.duration(this.duration) | |
.call(this.grid.difference) | |
var line = d3.area() | |
.x(function(d) { return _this.scale.x(d.x)}) | |
.y0(function(d) { return _this.scale.difference(d.dif0)}) | |
.y1(function(d) { return _this.scale.difference(d.dif1)}) | |
.curve(this.curve) | |
this.s.lines.transition() | |
.duration(this.duration) | |
.attr("d", function(d) { return line(d.values);}) | |
.style("fill", function(d) { return _this.scale.color(d.key)}) | |
.style("fill-opacity",.75) | |
.style("stroke-opacity",0); | |
this.s.axis.label.transition() | |
.duration(this.duration/2) | |
.style("opacity",0) | |
.transition() | |
.duration(0) | |
.text(this.axis.labels[1]) | |
.transition() | |
.duration(this.duration/2) | |
.style("opacity",1) | |
this.s.legend.transition() | |
.duration(this.duration/2) | |
.style("opacity",0) | |
.transition() | |
.duration(0) | |
.text(function(d,i) { return _this.label.difference[i];}) | |
.transition() | |
.duration(this.duration/2) | |
.style("opacity",1) | |
this.s.voronoi.remove(); | |
this.drawVoronoi(this.data.tabular.map(function(d) { | |
return { | |
x:d[_this.unit.x], | |
y:d.dif, | |
key: d.dif >= 0 ? _this.unit.y[1]:_this.unit.y[0] | |
}; | |
}), this.scale.difference) | |
} | |
LineChart.prototype.toggleMulti = function() { | |
var _this = this; | |
this.s.axis.left.transition() | |
.duration(this.duration) | |
.call(_this.axis.y); | |
this.s.grid.transition() | |
.duration(this.duration) | |
.call(this.grid.y) | |
var line = d3.area() | |
.x(function(d) { return _this.scale.x(d.x)}) | |
.y(function(d) { return _this.scale.y(d.y)}) | |
.curve(this.curve); | |
this.s.lines.transition() | |
.duration(this.duration) | |
.attr("d", function(d) { return line(d.values);}) | |
.style("fill-opacity",0) | |
.style("stroke-opacity",1); | |
this.s.axis.label.transition() | |
.duration(this.duration/2) | |
.style("opacity",0) | |
.transition() | |
.duration(0) | |
.text(this.axis.labels[0]) | |
.transition() | |
.duration(this.duration/2) | |
.style("opacity",1) | |
this.s.legend.transition() | |
.duration(this.duration/2) | |
.style("opacity",0) | |
.transition() | |
.duration(0) | |
.text(function(d,i) { return _this.label.multi[i];}) | |
.transition() | |
.duration(this.duration/2) | |
.style("opacity",1) | |
this.s.voronoi.remove(); | |
this.drawVoronoi(d3.merge(this.data.nested.map(function(d) { return d.values;})), this.scale.y); | |
} | |
LineChart.prototype.drawFocus = function() { | |
var _this = this; | |
this.s.focus = this.s.chart.append("g") | |
.attr("transform", "translate(-100,-100)"); | |
this.s.focus.append("circle") | |
.attr("r", 5.5) | |
.style("fill","none") | |
.style("stroke-width",2); | |
this.s.focus.append("rect"); | |
this.s.focus.append("text") | |
.style("text-anchor","middle") | |
.attr("y", -14); | |
} | |
LineChart.prototype.enterPoly = function(d,scale) { | |
var _this = this; | |
this.s.focus.attr("transform", "translate(" + this.scale.x(d.data.x) + "," + scale(d.data.y) + ")") | |
this.s.focus.select("circle").style("stroke", this.scale.color(d.data.key)) | |
this.s.focus.select("text").text(d3.format(",.3r")(d.data.y)).style("fill","white").each(function() { | |
var bBox = d3.select(this).node().getBBox(); | |
_this.s.focus.select("rect") | |
.attr("width", bBox.width + 6) | |
.attr("height",bBox.height) | |
.attr("x",bBox.x - 3) | |
.attr("y",bBox.y+.5) | |
.attr("rx",3) | |
.style("fill", _this.scale.color(d.data.key)) | |
}) | |
} | |
LineChart.prototype.leaveVoronoi = function() { | |
this.s.focus.attr("transform", "translate(-100,-100)"); | |
} | |
LineChart.prototype.drawVoronoi = function(vData,scale) { | |
var _this = this; | |
var voronoi = d3.voronoi() | |
.x(function(d) { return _this.scale.x(d.x); }) | |
.y(function(d) { return scale(d.y); }) | |
.extent([[-this.margin.left, -this.margin.top], [this.width + this.margin.right, this.height + this.margin.bottom]]); | |
this.s.voronoi = this.s.chart.append("g").on("mouseleave", function() { _this.leaveVoronoi();}); | |
this.s.voronoi.selectAll("path") | |
.data(voronoi.polygons(vData)) | |
.enter().append("path") | |
.attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; }) | |
.style("fill","black") | |
.style("fill-opacity",0) | |
.style("stroke",'black') | |
.style("stroke-opacity", 0) | |
.on("mouseenter", function(d) { _this.enterPoly(d,scale);}); | |
} | |
LineChart.prototype.draw = function() { | |
this.s.svg = d3.select(this.elementId) | |
.append("svg") | |
.attr("height", this.height + this.margin.top + this.margin.bottom) | |
.attr("width", this.width + this.margin.left + this.margin.right) | |
.style("-webkit-user-select","none") | |
.style("cursor","default"); | |
this.s.chart = this.s.svg.append("g") | |
.attr("transform","translate(" + this.margin.left + "," + this.margin.top + ")"); | |
} | |
LineChart.prototype.drawAxes = function() { | |
this.s.axis = {}; | |
this.s.axis.bottom = this.s.chart.append("g").attr("transform","translate(0," + (this.height + 20) + ")").call(this.axis.x); | |
this.s.axis.left = this.s.chart.append("g").attr("transform","translate(-20,0)").call(this.axis.y); | |
this.s.axis.label = this.s.axis.left.append("text") | |
.style("fill","black") | |
.attr("transform","rotate(90)") | |
.style("text-anchor","start") | |
.style("font-size","1.3em") | |
.attr("dy", -2.5) | |
.text(this.axis.labels[0]) | |
} | |
LineChart.prototype.drawLines = function() { | |
var _this = this; | |
var line = d3.area() | |
.x(function(d) { return _this.scale.x(d.x)}) | |
.y(function(d) { return _this.scale.y(d.y)}) | |
.curve(this.curve); | |
this.s.lines = this.s.chart.selectAll(".line") | |
.data(this.data.nested) | |
.enter() | |
.append("path") | |
.attr("d", function(d) { return line(d.values);}) | |
.style("fill","none") | |
.style("stroke", function(d) { return _this.scale.color(d.key)}) | |
.style("stroke-dasharray", function(d) { return _this.scale.dash(d.key)}) | |
.style("stroke-width",3.5) | |
.style("stroke-linecap","round") | |
} | |
LineChart.prototype.prepData = function(data) { | |
var _this = this; | |
data.forEach(function(d) { | |
d[_this.unit.x] = d3.timeParse('%Y-%m-%d')(d[_this.unit.x]) | |
}); | |
this.data.tabular = data.map(function(d) { | |
var o = d; | |
o.dif = d[_this.unit.y[1]] - d[_this.unit.y[0]]; | |
return o; | |
}) | |
this.data.nested = this.unit.y.map(function(d,i) { | |
return {key:d, values: _this.data.tabular.map(function(e) { | |
if(i == 1) { | |
var dif0 = e.dif>=0?e.dif:0; | |
var dif1 = 0; | |
} else { | |
var dif0 = 0; | |
var dif1 = e.dif<0?e.dif:0; | |
} | |
return {x:e[_this.unit.x], y:e[d], dif:e.dif, dif0:dif0,dif1:dif1, key:d}; | |
}) | |
}}); | |
}; | |
LineChart.prototype.setScales = function() { | |
var _this = this; | |
this.scale = {}; | |
this.scale.x = d3.scaleTime() | |
.domain(d3.extent(this.data.tabular.map(function(d) { return d[_this.unit.x]}))) | |
.range([0, _this.width]); | |
this.scale.y = d3.scaleLinear() | |
.domain([d3.min(this.data.tabular.map(function(d) { return d3.min(_this.unit.y.map(function(e) { return d[e]}))})), | |
d3.max(this.data.tabular.map(function(d) { return d3.max(_this.unit.y.map(function(e) { return d[e]}))}))]) | |
.range([this.height,0]) | |
this.scale.difference = d3.scaleLinear() | |
.domain(d3.extent(this.data.tabular.map(function(d) { return d.dif;}))) | |
.range([this.height,0]); | |
this.scale.color = d3.scaleOrdinal() | |
.domain([this.unit.y]) | |
.range(["#5cb85c","#f0ad4e"]) | |
this.scale.dash = d3.scaleOrdinal() | |
.domain([this.unit.y]) | |
.range(["0, 0","0, 0"]) | |
this.scale.toggle = d3.scaleBand() | |
.domain(this.data.toggles) | |
.range([0,270]); | |
this.scale.legend = d3.scaleBand() | |
.domain(this.label.multi) | |
.range([0,270]); | |
} | |
LineChart.prototype.setAxes = function() { | |
this.axis.x = d3.axisBottom(this.scale.x).tickFormat(d3.timeFormat('%m/%d')); | |
this.axis.y = d3.axisLeft(this.scale.y); | |
this.axis.difference = d3.axisLeft(this.scale.difference); | |
} | |
LineChart.prototype.setGrids = function() { | |
var _this = this; | |
this.grid = {}; | |
this.grid.y = d3.axisLeft(this.scale.y) | |
.tickSizeInner(-this.width) | |
.tickFormat(""); | |
this.grid.difference = d3.axisLeft(this.scale.difference) | |
.tickSizeInner(-this.width) | |
.tickFormat(""); | |
} | |
LineChart.prototype.drawGrids = function() { | |
var _this = this; | |
this.s.grid = this.s.chart.append("g") | |
.call(this.grid.y) | |
.each(function(d) { | |
d3.select(this) | |
.style("opacity", .10) | |
.select(".domain") | |
.style("display","none") | |
}) | |
} | |
</script> | |
<body> | |
<div id="chartDiv"></div> | |
<script> | |
var data = [ | |
{date:"2017-05-27",year16:37000,year17:46000}, | |
{date:"2017-05-28",year16:37250,year17:45500}, | |
{date:"2017-05-29",year16:37500,year17:42000}, | |
{date:"2017-05-30",year16:37000,year17:39000}, | |
{date:"2017-05-31",year16:34000,year17:41500}, | |
{date:"2017-06-01",year16:35500,year17:37700}, | |
{date:"2017-06-02",year16:33900,year17:37500}, | |
{date:"2017-06-03",year16:34500,year17:41000}, | |
{date:"2017-06-04",year16:32500,year17:41500}, | |
{date:"2017-06-05",year16:34500,year17:40000}, | |
{date:"2017-06-06",year16:34000,year17:41500}, | |
{date:"2017-06-07",year16:34100,year17:37400}, | |
{date:"2017-06-08",year16:41000,year17:37500}, | |
{date:"2017-06-09",year16:34500,year17:37000}, | |
{date:"2017-06-10",year16:32500,year17:28500}, | |
{date:"2017-06-11",year16:29000,year17:31500}, | |
{date:"2017-06-12",year16:30100,year17:37500}, | |
{date:"2017-06-13",year16:34000,year17:36000}, | |
{date:"2017-06-14",year16:33500,year17:33500}, | |
{date:"2017-06-15",year16:34900,year17:33000}, | |
{date:"2017-06-16",year16:32500,year17:32500}, | |
{date:"2017-06-17",year16:29000,year17:29000}, | |
{date:"2017-06-18",year16:26000,year17:29000}, | |
{date:"2017-06-19",year16:25000,year17:34900}, | |
{date:"2017-06-20",year16:31000,year17:36000}, | |
{date:"2017-06-21",year16:34000,year17:32500}, | |
{date:"2017-06-22",year16:33500,year17:33500}, | |
{date:"2017-06-23",year16:33000,year17:31000} | |
]; | |
var lineChart = new LineChart({ | |
data:data, | |
x:"date", | |
y:["year16","year17"], | |
multiLabels:["2016","2017"], | |
axisLabels: ["Daily Count","Year over Year"], | |
elementId:"#chartDiv", | |
height:335, | |
width:830, | |
margin: {top:80,left:80,bottom:85,right:50}, | |
duration:1000, | |
title:"WDW 2016 vs 2017 Attendance Since May 27" | |
}) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment