blob: b676b9544617696869ce37460f821ff99ee5746f [file] [log] [blame]
/* eslint-disable prefer-rest-params, no-param-reassign */
// Copied and modified from
// https://github.com/kmandov/d3-horizon-chart
import d3 from 'd3';
import './horizon.css';
const horizonChart = function () {
let colors = [
'#313695',
'#4575b4',
'#74add1',
'#abd9e9',
'#fee090',
'#fdae61',
'#f46d43',
'#d73027',
];
let height = 30;
const y = d3.scale.linear().range([0, height]);
let bands = colors.length >> 1; // number of bands in each direction (positive / negative)
let width = 1000;
let offsetX = 0;
let spacing = 0;
let mode = 'offset';
let axis;
let title;
let extent; // the extent is derived from the data, unless explicitly set via .extent([min, max])
let x;
let canvas;
function my(data) {
const horizon = d3.select(this);
const step = width / data.length;
horizon.append('span')
.attr('class', 'title')
.text(title);
horizon.append('span')
.attr('class', 'value');
canvas = horizon.append('canvas');
canvas
.attr('width', width)
.attr('height', height);
const context = canvas.node().getContext('2d');
context.imageSmoothingEnabled = false;
// update the y scale, based on the data extents
const ext = extent || d3.extent(data, d => d.y);
const max = Math.max(-ext[0], ext[1]);
y.domain([0, max]);
// x = d3.scaleTime().domain[];
axis = d3.svg.axis(x).ticks(5);
context.clearRect(0, 0, width, height);
// context.translate(0.5, 0.5);
// the data frame currently being shown:
const startIndex = Math.floor(Math.max(0, -(offsetX / step)));
const endIndex = Math.floor(Math.min(data.length, startIndex + (width / step)));
// skip drawing if there's no data to be drawn
if (startIndex > data.length) {
return;
}
// we are drawing positive & negative bands separately to avoid mutating canvas state
// http://www.html5rocks.com/en/tutorials/canvas/performance/
let negative = false;
// draw positive bands
let value;
let bExtents;
for (let b = 0; b < bands; b += 1) {
context.fillStyle = colors[bands + b];
// Adjust the range based on the current band index.
bExtents = (b + 1 - bands) * height;
y.range([bands * height + bExtents, bExtents]);
// only the current data frame is being drawn i.e. what's visible:
for (let i = startIndex; i < endIndex; i++) {
value = data[i].y;
if (value <= 0) { negative = true; continue; }
if (value === undefined) {
continue;
}
context.fillRect(offsetX + i * step, y(value), step + 1, y(0) - y(value));
}
}
// draw negative bands
if (negative) {
// mirror the negative bands, by flipping the canvas
if (mode === 'offset') {
context.translate(0, height);
context.scale(1, -1);
}
for (let b = 0; b < bands; b++) {
context.fillStyle = colors[bands - b - 1];
// Adjust the range based on the current band index.
bExtents = (b + 1 - bands) * height;
y.range([bands * height + bExtents, bExtents]);
// only the current data frame is being drawn i.e. what's visible:
for (let ii = startIndex; ii < endIndex; ii++) {
value = data[ii].y;
if (value >= 0) {
continue;
}
context.fillRect(offsetX + ii * step, y(-value), step + 1, y(0) - y(-value));
}
}
}
}
my.axis = function (_) {
if (!arguments.length) { return axis; }
axis = _;
return my;
};
my.title = function (_) {
if (!arguments.length) { return title; }
title = _;
return my;
};
my.canvas = function (_) {
if (!arguments.length) { return canvas; }
canvas = _;
return my;
};
// Array of colors representing the number of bands
my.colors = function (_) {
if (!arguments.length) {
return colors;
}
colors = _;
// update the number of bands
bands = colors.length >> 1;
return my;
};
my.height = function (_) {
if (!arguments.length) { return height; }
height = _;
return my;
};
my.width = function (_) {
if (!arguments.length) { return width; }
width = _;
return my;
};
my.spacing = function (_) {
if (!arguments.length) { return spacing; }
spacing = _;
return my;
};
// mirror or offset
my.mode = function (_) {
if (!arguments.length) { return mode; }
mode = _;
return my;
};
my.extent = function (_) {
if (!arguments.length) { return extent; }
extent = _;
return my;
};
my.offsetX = function (_) {
if (!arguments.length) { return offsetX; }
offsetX = _;
return my;
};
return my;
};
function horizonViz(slice, payload) {
const fd = slice.formData;
const div = d3.select(slice.selector);
div.selectAll('*').remove();
let extent;
if (fd.horizon_color_scale === 'overall') {
let allValues = [];
payload.data.forEach(function (d) {
allValues = allValues.concat(d.values);
});
extent = d3.extent(allValues, d => d.y);
} else if (fd.horizon_color_scale === 'change') {
payload.data.forEach(function (series) {
const t0y = series.values[0].y; // value at time 0
series.values = series.values.map(d =>
Object.assign({}, d, { y: d.y - t0y }),
);
});
}
div.selectAll('.horizon')
.data(payload.data)
.enter()
.append('div')
.attr('class', 'horizon')
.each(function (d, i) {
horizonChart()
.height(fd.series_height)
.width(slice.width())
.extent(extent)
.title(d.key)
.call(this, d.values, i);
});
}
module.exports = horizonViz;