Skip to content

Instantly share code, notes, and snippets.

@Saigesp Saigesp/.block
Last active Oct 19, 2018

Embed
What would you like to do?
D3v4 feverchart with linear regression
281f92fae4192c4a569fc992d10a9914

Linear Trends

D3 implementation of linear trends

See the demo and more charts from d3graphs repository

Features:

  • Object oriented approach
  • Responsive

Requires:

  • D3 v4+
  • leastSquares.js

Default options:

{
    'margin': {'top': 40, 'right': 20, 'bottom': 40, 'left': 40},
    'keys': [],
    'colors': ['#AAAAAA', '#000000'],
    'fontsize': '12px',
    'xgrid': false,
    'ygrid': false,
    'yscaleformat': '.0f',
    'datefield': 'date',
    'dateformat': '%Y-%m-%d',
    'internalR': 4,
    'externalR': 12,
    'title': false,
    'source': false,
}
class TrendLineChart {
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': ['#AAAAAA', '#000000'],
'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,
'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'),
this.line = d3.line()
.x(function(d){ return self.xScale(d.x); })
.y(function(d){ return self.yScale(d.y); });
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]);
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; })]);
// Least Squares Calculus
var xSeries = d3.range(1, this.xLabels.length + 1);
var ySeries = this.data.map(function(d) { return parseFloat(+d[self.cfg.keys[0]]); });
var leastSquaresCoeff = leastSquares(xSeries, ySeries);
var x1 = this.xLabels[0];
var y1 = leastSquaresCoeff[0] + leastSquaresCoeff[1];
var x2 = this.xLabels[this.xLabels.length - 1];
var y2 = leastSquaresCoeff[0] * xSeries.length + leastSquaresCoeff[1];
var trendData = [[x1,y1,x2,y2]];
var parseTrendTime = d3.timeParse("%Y");
// SVG
this.svg = this.selection.append('svg')
.attr("class", "chart linechart linechart-trends")
.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")
.call(self.make_x_gridlines()
.tickSize(self.cfg.height)
.tickFormat(""))
}
if(this.cfg.ygrid){
this.yGrid = this.g.append("g")
.attr("class", "grid grid--y")
.call(self.make_y_gridlines()
.tickSize(-self.cfg.width)
.tickFormat("")
.ticks(3, self.cfg.yscaleformat));
}
// AXIS
this.g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + this.cfg.height + ")")
.call(d3.axisBottom(self.xScale));
this.g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(self.yScale)
.ticks(3, self.cfg.yscaleformat));
// TITLE
if(self.cfg.title){
this.svg.append('text')
.attr('class', 'title label')
.attr('text-anchor', 'middle')
.attr('transform', 'translate('+ (self.cfg.width/2) +',20)')
.text(self.cfg.title)
}
// SOURCE
if(self.cfg.source){
this.svg.append('text')
.attr('class', 'source label')
.attr('transform', 'translate('+ (self.cfg.margin.left) +','+(self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom - 5)+')')
.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 d3-"+d.key;
});
this.lineg.append('path')
.attr("class", "line")
.style('stroke', function(d, i){ return self.colorScale(i); })
.attr("d", function(d) {
return self.line(d.values);
});
// LEAST SQUARES DRAW
this.trends = this.g.append('g')
.attr('class', 'trends')
this.trendline = this.trends.selectAll(".trendline")
.data(trendData).enter()
.append("line")
.attr("class", "line trendline")
.attr("x1", function(d) { return self.xScale(parseTrendTime(d[0])); })
.attr("y1", function(d) { return self.yScale(+d[1]); })
.attr("x2", function(d) { return self.xScale(parseTrendTime(d[2])); })
.attr("y2", function(d) { return self.yScale(+d[3]); })
.attr("stroke", "black")
.attr("stroke-width", 1);
// POINTS
this.pointsg = this.g.selectAll('.point--group')
.data(this.data).enter()
.append('g')
.attr('class', 'point--group')
.attr('transform', function(d){
return 'translate('+self.xScale(d.jsdate)+','+self.yScale(d[self.cfg.keys[0]])+')';
})
this.pointsg.append('circle')
.attr('class', 'external')
.attr('r', this.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)
})
this.pointsg.append('circle')
.attr('class', 'internal')
.attr('r', this.cfg.internalR)
this.pointsg.append('text')
.attr('class', 'label point-label')
.attr('y', 4)
.attr('x', function(d){
return self.xScale(d.jsdate) > self.cfg.width*0.8 ? -8 : 8;
})
.attr('text-anchor', function(d){
return self.xScale(d.jsdate) > self.cfg.width*0.8 ? 'end' : 'start';
})
.text(function(d){
return d[self.cfg.keys[0]]
})
}
// 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;
}
resize(){
}
};
<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 .trendline {
stroke: #ff4e00;
}
.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;
}
.point--group .external {
fill: transparent;
}
.chart .point--group .internal {
stroke: white;
}
.chart .point--group .internal,
.chart .point--group .label {
pointer-events: none;
opacity: 0;
transition: opacity 200ms ease;
}
.chart:hover .point--group .internal,
.chart .point--group.is-active .label {
opacity: 1;
}
.chart.is-active .line {
opacity: 0.2;
}
.chart.is-active .point--group .internal {
opacity: 0.2;
}
</style>
<body>
<div id="biomass" style="width: 900px; height: 300px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="leastSquares.js"></script>
<script src="d3.trendschart.js"></script>
<script>
d3.csv('sardines.csv', function(data) {
var biomass = new TrendLineChart(d3.select('#biomass'), data, {
'keys': ['biomass'],
'datefield': 'year',
'dateformat': '%Y',
'ygrid': true,
'yscaleformat': 's',
'colors': ['#000000'],
'title': 'Biomasa de 1 año y mayor (Biomass 1+) de sardinas en el Mar Cantábrico y las aguas del Atlántico ibérico',
'source': 'Fuente: <a href="http://ices.dk/sites/pub/Publication%20Reports/Advice/2018/2018/pil.27.8c9a.pdf">ICES Advice on fishing opportunities, catch, and effort</a>. 13 julio 2018.',
})
})
</script>
</body>
</html>
// returns slope, intercept and r-square of the line
function leastSquares(xSeries, ySeries) {
var reduceSumFunc = function(prev, cur) { return prev + cur; };
var xBar = xSeries.reduce(reduceSumFunc) * 1.0 / xSeries.length;
var yBar = ySeries.reduce(reduceSumFunc) * 1.0 / ySeries.length;
var ssXX = xSeries.map(function(d) { return Math.pow(d - xBar, 2); })
.reduce(reduceSumFunc);
var ssYY = ySeries.map(function(d) { return Math.pow(d - yBar, 2); })
.reduce(reduceSumFunc);
var ssXY = xSeries.map(function(d, i) { return (d - xBar) * (ySeries[i] - yBar); })
.reduce(reduceSumFunc);
var slope = ssXY / ssXX;
var intercept = yBar - (xBar * slope);
var rSquare = Math.pow(ssXY, 2) / (ssXX * ssYY);
return [slope, intercept, rSquare];
}
year recruitment high low biomass high low catch
1978 36254400 48435260 24073540 519250 681161 357339 145609
1979 42282000 56015640 28548360 673759 884216 463302 157241
1980 48279400 62933460 33625340 847100 1099904 594296 194802
1981 29888500 40398020 19378980 1016040 1302875 729205 216517
1982 16093600 23713500 8473700 945305 1215241 675369 206946
1983 71388800 86662840 56114760 747620 975785 519455 183837
1984 21284100 29095200 13473000 1163730 1408436 919024 206005
1985 18615200 25193000 12037400 987669 1188337 787001 208439
1986 15832200 21789720 9874680 797987 960115 635859 187363
1987 34436500 42779760 26093240 643902 779540 508264 177696
1988 18675400 24606620 12744180 709409 841823 576995 161531
1989 17827500 23433280 12221720 628061 747019 509103 140961
1990 18664000 24444260 12883740 565453 674503 456403 149429
1991 54589000 64160220 45017780 520169 626679 413659 132587
1992 37157900 44535640 29780160 855475 992578 718372 130250
1993 16291400 20913540 11669260 966562 1102589 830535 142495
1994 14177300 17988280 10366320 814927 930284 699570 136582
1995 11103300 14139440 8067160 675905 772542 579268 125280
1996 15876700 19332400 12421000 541822 621746 461898 116736
1997 10671500 13482900 7860100 481004 551959 410049 115814
1998 13613200 16777740 10448660 389790 451681 327899 108924
1999 10490400 13399720 7581080 374188 435390 312986 94091
2000 32442700 38097680 26787720 320879 378422 263336 85786
2001 20113300 24529540 15697060 482370 557202 407538 101957
2002 11336900 14566620 8107180 496553 573052 420054 99673
2003 8913870 11888950 5938790 471972 547425 396519 97831
2004 37750200 43156440 32343960 412877 483871 341883 98020
2005 13165100 16073120 10257080 552191 633671 470711 97345
2006 4211350 5693490 2729210 647079 729964 564194 87023
2007 5787690 7352958 4222422 509834 575831 443837 96469
2008 7428350 9071960 5784740 394772 447214 342330 101464
2009 8483120 10064766 6901474 295909 336503 255315 87740
2010 4832010 5975504 3688516 247453 280046 214860 89571
2011 3949080 4945576 2952584 177018 203244 150792 80403
2012 4202470 5200714 3204226 130021 153844 106198 54857
2013 4556910 5711584 3402236 118062 142445 93679 45818
2014 3254200 4287154 2221246 119032 147190 90874 27937
2015 5736580 7543358 3929802 112943 143184 82702 20595
2016 6596320 8928520 4264120 143795 184079 103511 22704
2017 2307970 3617824 998116 175449 229217 121681 21911
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.