Skip to content

Instantly share code, notes, and snippets.

@TommyCoin80
Last active July 12, 2017 21:00
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 TommyCoin80/ee23cb1461d04d020d1458c9400861d7 to your computer and use it in GitHub Desktop.
Save TommyCoin80/ee23cb1461d04d020d1458c9400861d7 to your computer and use it in GitHub Desktop.
Multi-Line and Difference Area Chart

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.

<!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