Skip to content

Instantly share code, notes, and snippets.

@Saigesp
Last active October 22, 2018 04:07
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 Saigesp/13029d084201f5012391e231215961f0 to your computer and use it in GitHub Desktop.
Save Saigesp/13029d084201f5012391e231215961f0 to your computer and use it in GitHub Desktop.
D3v4 Multiple Linechart
13029d084201f5012391e231215961f0

Multiple Linechart

D3 implementation of multiple linechar

See the demo and more charts from d3graphs repository

Features:

  • Object oriented approach
  • Responsive
  • Resizable

Requires:

  • D3 v4+

Default options:

{
    margin: {top: 40, right: 20, bottom: 40, left: 40},
    keys: [],
    colors: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
    greycolor: 'rgba(120,120,120,0.3)',
    fontsize: '12px',
    xgrid: false,
    ygrid: false,
    yscaleformat: '.0f',
    datefield: 'date',
    dateformat: '%Y-%m-%d',
    internalR: 4,
    externalR: 12,
    currentkey: false,
    title: false,
    source: false,
}
class LineChart {
constructor(selection, data, config = {}) {
let self = this;
this.selection = selection;
this.data = data;
// Graph configuration
this.cfg = {
margin: {top: 40, right: 20, bottom: 40, left: 40},
keys: [],
colors: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
greycolor: 'rgba(120,120,120,0.3)',
fontsize: '12px',
xgrid: false,
ygrid: false,
yscaleformat: '.0f',
datefield: 'date',
dateformat: '%Y-%m-%d', // https://github.com/d3/d3-time-format/blob/master/README.md#locale_format
internalR: 4,
externalR: 12,
currentkey: false,
title: false,
source: false,
};
Object.keys(config).forEach(function(key) {
if(config[key] instanceof Object && config[key] instanceof Array === false){
Object.keys(config[key]).forEach(function(sk) {
self.cfg[key][sk] = config[key][sk];
});
} else self.cfg[key] = config[key];
});
this.cfg.width = parseInt(this.selection.node().offsetWidth) - this.cfg.margin.left - this.cfg.margin.right,
this.cfg.height = parseInt(this.selection.node().offsetHeight)- this.cfg.margin.top - this.cfg.margin.bottom;
this.xScale = d3.scaleTime().rangeRound([0, this.cfg.width]),
this.yScale = d3.scaleLinear().rangeRound([this.cfg.height, 0]);
this.colorScale = d3.scaleOrdinal().range(this.cfg.colors)
// Extract the x labels for the axis and scale domain
this.xLabels = this.data.map(function (d) { return +d[self.cfg.datefield]; })
this.parseTime = d3.timeParse(this.cfg.dateformat),
this.formatTime = d3.timeFormat('%d-%m-%Y'),
window.addEventListener("resize", function(){ self.draw() });
this.initGraph();
}
initGraph() {
let self = this;
this.dataT = [];
this.cfg.keys.forEach(function(j,i){
self.dataT[i] = {};
self.dataT[i]['key'] = j
self.dataT[i]['values'] = []
});
this.data.forEach(function(d){ d.jsdate = self.parseTime(d[self.cfg.datefield]); });
this.data.sort(function(a,b){return b.jsdate - a.jsdate })
this.data.forEach(function(d){
d.min = 9999999999;
d.max = -9999999999;
self.cfg.keys.forEach(function(j, i){
self.dataT[i]['values'].push({'x': d.jsdate, 'y': +d[j], 'k': i})
if (d[j] < d.min) d.min = +d[j];
if (d[j] > d.max) d.max = +d[j];
})
});
this.xScale.domain(d3.extent(this.data, function(d) { return d.jsdate; }));
this.yScale.domain([0, d3.max(this.data, function(d) { return d.max; })]);
// SVG
this.svg = this.selection.append('svg')
.attr("class", "chart linechart")
.attr("viewBox", "0 0 "+(this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)+" "+(this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom))
.attr("width", this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)
.attr("height", this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom);
this.g = this.svg.append("g")
.attr("transform", "translate(" + (self.cfg.margin.left) + "," + (self.cfg.margin.top) + ")");
// GRIDLINES
if(this.cfg.xgrid) this.xGrid = this.g.append("g").attr("class", "grid grid--x")
if(this.cfg.ygrid) this.yGrid = this.g.append("g").attr("class", "grid grid--y");
// AXIS
this.xAxis = this.g.append("g").attr("class", "axis axis--x")
this.yAxis = this.g.append("g").attr("class", "axis axis--y")
// TITLE
if(self.cfg.title) this.title = this.svg.append('text').attr('class', 'title label').attr('text-anchor', 'middle').text(self.cfg.title)
// SOURCE
if(self.cfg.source) this.source = this.svg.append('text').attr('class', 'source label').html(self.cfg.source)
// LINES
this.lineg = this.g.selectAll(".line--group")
.data(this.dataT)
.enter().append('g')
.attr("class", function(d){
return "line--group line--group__"+d.key;
});
this.lines = this.lineg.append('path')
.attr("class", "line")
.style('stroke', function(d, i){
return !self.cfg.currentkey || d.key == self.cfg.currentkey ? self.colorScale(i) : self.cfg.greycolor;
})
// POINTS
this.pointsg = []
self.cfg.keys.forEach(function(k, i){
var gp = self.g.selectAll('.point--group .point--group__'+k)
.data(self.data).enter()
.append('g')
.attr('class', 'point--group point--group__'+k)
gp.append('circle')
.attr('class', 'external')
.attr('r', self.cfg.externalR)
.on('mouseover', function(){
d3.select(this.parentNode).classed('is-active', true);
self.svg.classed('is-active', true)
})
.on('mouseout', function(){
d3.select(this.parentNode).classed('is-active', false);
self.svg.classed('is-active', false)
})
gp.append('circle')
.attr('class', 'internal')
.attr('fill', function(){
return !self.cfg.currentkey || k == self.cfg.currentkey ? self.colorScale(i) : self.cfg.greycolor;
})
.attr('r', self.cfg.internalR)
gp.append("title")
.text(function(d) { return d[k] + ' ' + k});
self.pointsg.push({selection:gp, key:k })
})
self.draw()
}
draw(){
var self = this;
this.cfg.width = parseInt(this.selection.node().offsetWidth) - this.cfg.margin.left - this.cfg.margin.right;
this.cfg.height = parseInt(this.selection.node().offsetHeight)- this.cfg.margin.top - this.cfg.margin.bottom;
this.xScale.rangeRound([0, this.cfg.width]);
this.yScale.rangeRound([this.cfg.height, 0]);
this.svg
.attr("viewBox", "0 0 "+(this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)+" "+(this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom))
.attr("width", this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)
.attr("height", this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom);
// GRIDLINES
if(this.cfg.xgrid){
this.xGrid.call(self.make_x_gridlines()
.tickSize(self.cfg.height)
.tickFormat(""))
}
if(this.cfg.ygrid){
this.yGrid.call(self.make_y_gridlines()
.tickSize(-self.cfg.width)
.tickFormat("")
.ticks(3, self.cfg.yscaleformat));
}
// AXIS
this.xAxis.attr("transform", "translate(0," + this.cfg.height + ")").call(d3.axisBottom(self.xScale));
this.yAxis.call(d3.axisLeft(self.yScale).ticks(3, self.cfg.yscaleformat));
// TITLE
if(self.cfg.title) this.title.attr('transform', 'translate('+ ((self.cfg.width/2) + self.cfg.margin.left) +',20)')
// SOURCE
if(self.cfg.source) this.source.attr('transform', 'translate('+ (self.cfg.margin.left) +','+(self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom - 5)+')')
// LINES
this.line = d3.line()
.x(function(d){ return self.xScale(d.x); })
.y(function(d){ return self.yScale(d.y); });
this.lines.attr("d", function(d) { return self.line(d.values) });
// POINTS
this.pointsg.forEach(function(p, i){
p.selection.attr('transform', function(d){ return 'translate('+self.xScale(d.jsdate)+','+self.yScale(d[p.key])+')'; })
})
}
// gridlines in x axis function
make_x_gridlines() {
return d3.axisBottom(this.xScale);
}
// gridlines in y axis function
make_y_gridlines() {
return d3.axisLeft(this.yScale);
}
// Data functions
setData(data){
this.data = data;
}
getData(){
return this.data;
}
};
<html>
<head>
<meta charset="utf-8">
</head>
<style>
.chart .label {
font-family: sans-serif;
font-size: 12px;
cursor: default;
}
.chart .line {
fill: transparent;
stroke-width: 2px;
}
.chart .grid line {
opacity: 0.1;
}
.chart .grid path {
fill: transparent;
stroke: transparent;
}
.chart .source {
fill: #7a7a7a;
font-size: 10px;
}
.chart .source a {
text-decoration: underline;
}
.chart .title {
font-weight: 700;
}
/* LINECHART */
.linechart .point--group .external {
fill: transparent;
}
.linechart .point--group .internal {
stroke: white;
}
.linechart .point--group .internal,
.linechart .point--group .label {
pointer-events: none;
opacity: 0;
transition: opacity 200ms ease;
}
.linechart:hover .point--group .internal {
opacity: 1;
}
.linechart.is-active .line {
opacity: 0.2;
}
.linechart.is-active .point--group .internal {
opacity: 0.2;
}
.linechart.is-active .point--group.is-active .internal {
opacity: 1;
}
</style>
<body>
<div id="evolution" style="width: 100%; height: 300px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="d3.linechart.js"></script>
<script>
d3.json('population.json', function(data) {
var evolution = new LineChart(d3.select('#evolution'), data.history, {
'keys': ['total', 'hombres', 'mujeres'],
'datefield': 'datetime',
'dateformat': '%Q',
'yscaleformat': '.0s',
'ygrid': true,
'colors': ['#3161e2','#e0455a', '#a438e2'],
'title': 'Población de Málaga',
'source': 'Fuente: INE',
})
})
</script>
</body>
</html>
{
"now":
{
"datetime": 1483228800000,
"total": 569002,
"hombres": 273636,
"mujeres": 295366
},
"history": [
{
"datetime": 946684800000,
"total": 531565,
"hombres": 252570,
"mujeres": 278995
},
{
"datetime": 978307200000,
"total": 534207,
"hombres": 254477,
"mujeres": 279730
},
{
"datetime": 1009843200000,
"total": 535686,
"hombres": 255964,
"mujeres": 279722
},
{
"datetime": 1041379200000,
"total": 547105,
"hombres": 262983,
"mujeres": 284122
},
{
"datetime": 1072915200000,
"total": 547731,
"hombres": 263776,
"mujeres": 283955
},
{
"datetime": 1104537600000,
"total": 558287,
"hombres": 269479,
"mujeres": 288808
},
{
"datetime": 1136073600000,
"total": 560631,
"hombres": 270672,
"mujeres": 289959
},
{
"datetime": 1167609600000,
"total": 561250,
"hombres": 271042,
"mujeres": 290208
},
{
"datetime": 1199145600000,
"total": 566447,
"hombres": 273299,
"mujeres": 293148
},
{
"datetime": 1230768000000,
"total": 568305,
"hombres": 274209,
"mujeres": 294096
},
{
"datetime": 1262304000000,
"total": 568507,
"hombres": 273958,
"mujeres": 294549
},
{
"datetime": 1293840000000,
"total": 568030,
"hombres": 273355,
"mujeres": 294675
},
{
"datetime": 1325376000000,
"total": 567433,
"hombres": 272927,
"mujeres": 294506
},
{
"datetime": 1356998400000,
"total": 568479,
"hombres": 273475,
"mujeres": 295004
},
{
"datetime": 1388534400000,
"total": 566913,
"hombres": 272674,
"mujeres": 294239
},
{
"datetime": 1420070400000,
"total": 569130,
"hombres": 273817,
"mujeres": 295313
},
{
"datetime": 1451606400000,
"total": 569009,
"hombres": 273715,
"mujeres": 295294
},
{
"datetime": 1483228800000,
"total": 569002,
"hombres": 273636,
"mujeres": 295366
}]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment