2020-02-19 07:36:11 +00:00
|
|
|
const _ = require('lodash');
|
2020-02-18 11:29:51 +00:00
|
|
|
const d3 = require('d3');
|
|
|
|
const moment = require('moment');
|
|
|
|
|
2020-02-19 07:36:11 +00:00
|
|
|
export class CdfChartD3 {
|
2020-02-18 11:29:51 +00:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.attrs = {
|
|
|
|
svgWidth: 400,
|
|
|
|
svgHeight: 400,
|
|
|
|
|
|
|
|
marginTop: 5,
|
|
|
|
marginBottom: 5,
|
|
|
|
marginRight: 50,
|
|
|
|
marginLeft: 5,
|
|
|
|
|
2020-02-19 07:36:11 +00:00
|
|
|
container: null,
|
2020-02-18 11:29:51 +00:00
|
|
|
minX: false,
|
|
|
|
maxX: false,
|
|
|
|
scale: 'linear',
|
2020-02-19 07:36:11 +00:00
|
|
|
timeScale: null,
|
2020-02-18 11:29:51 +00:00
|
|
|
showDistributionLines: true,
|
|
|
|
areaColors: ['#E1E5EC', '#E1E5EC'],
|
|
|
|
logBase: 10,
|
|
|
|
verticalLine: 110,
|
|
|
|
showVerticalLine: true,
|
|
|
|
data: null,
|
|
|
|
onHover: (e) => {
|
|
|
|
},
|
|
|
|
};
|
|
|
|
this.hoverLine = null;
|
|
|
|
this.xScale = null;
|
|
|
|
this.dataPoints = null;
|
|
|
|
this.mouseover = this.mouseover.bind(this);
|
|
|
|
this.mouseout = this.mouseout.bind(this);
|
2020-02-18 11:58:34 +00:00
|
|
|
this.formatDates = this.formatDates.bind(this);
|
2020-02-18 11:29:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
svgWidth(svgWidth) {
|
|
|
|
this.attrs.svgWidth = svgWidth;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
svgHeight(height) {
|
|
|
|
this.attrs.svgHeight = height;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
maxX(maxX) {
|
|
|
|
this.attrs.maxX = maxX;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
minX(minX) {
|
|
|
|
this.attrs.minX = minX;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2020-02-18 12:11:22 +00:00
|
|
|
scale(scale) {
|
|
|
|
this.attrs.scale = scale;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2020-02-19 07:36:11 +00:00
|
|
|
timeScale(timeScale) {
|
|
|
|
this.attrs.timeScale = timeScale;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2020-02-18 11:29:51 +00:00
|
|
|
onHover(onHover) {
|
|
|
|
this.attrs.onHover = onHover;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
marginBottom(marginBottom) {
|
|
|
|
this.attrs.marginBottom = marginBottom;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
marginLeft(marginLeft) {
|
|
|
|
this.attrs.marginLeft = marginLeft;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
marginRight(marginRight) {
|
|
|
|
this.attrs.marginRight = marginRight;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
marginTop(marginTop) {
|
|
|
|
this.attrs.marginTop = marginTop;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
showDistributionLines(showDistributionLines) {
|
|
|
|
this.attrs.showDistributionLines = showDistributionLines;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
verticalLine(verticalLine) {
|
|
|
|
this.attrs.verticalLine = verticalLine;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
showVerticalLine(showVerticalLine) {
|
|
|
|
this.attrs.showVerticalLine = showVerticalLine;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
container(container) {
|
|
|
|
this.attrs.container = container;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
data(data) {
|
|
|
|
this.attrs.data = data;
|
|
|
|
return this;
|
|
|
|
}
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-18 11:29:51 +00:00
|
|
|
/**
|
|
|
|
* @param key
|
|
|
|
* @returns {[]}
|
|
|
|
*/
|
2020-02-19 08:42:54 +00:00
|
|
|
getDataPoints(key) {
|
2020-02-18 11:29:51 +00:00
|
|
|
const dt = [];
|
|
|
|
const data = this.attrs.data[key];
|
|
|
|
const len = data.xs.length;
|
|
|
|
|
|
|
|
for (let i = 0; i < len; i++) {
|
2020-02-19 07:36:11 +00:00
|
|
|
dt.push({ x: data.xs[i], y: data.ys[i] });
|
2020-02-17 21:52:21 +00:00
|
|
|
}
|
|
|
|
|
2020-02-18 11:29:51 +00:00
|
|
|
return dt;
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const attrs = this.attrs;
|
|
|
|
const container = d3.select(attrs.container);
|
2020-02-19 08:42:54 +00:00
|
|
|
if (container.node() === null) {
|
|
|
|
console.error('Container for D3 is not defined.');
|
|
|
|
return;
|
|
|
|
}
|
2020-02-18 11:29:51 +00:00
|
|
|
|
2020-02-19 07:36:11 +00:00
|
|
|
// Sets the width from the DOM element.
|
2020-02-18 09:31:47 +00:00
|
|
|
const containerRect = container.node().getBoundingClientRect();
|
2020-02-17 21:52:21 +00:00
|
|
|
if (containerRect.width > 0) {
|
|
|
|
attrs.svgWidth = containerRect.width;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculated properties.
|
2020-02-18 09:31:47 +00:00
|
|
|
const calc = {};
|
2020-02-17 21:52:21 +00:00
|
|
|
calc.chartLeftMargin = attrs.marginLeft;
|
|
|
|
calc.chartTopMargin = attrs.marginTop;
|
|
|
|
calc.chartWidth = attrs.svgWidth - attrs.marginRight - attrs.marginLeft;
|
|
|
|
calc.chartHeight = attrs.svgHeight - attrs.marginBottom - attrs.marginTop;
|
|
|
|
|
2020-02-19 08:39:57 +00:00
|
|
|
const areaColorRange = d3.scaleOrdinal().range(attrs.areaColors);
|
2020-02-19 08:42:54 +00:00
|
|
|
this.dataPoints = [this.getDataPoints('primary')];
|
2020-02-17 21:52:21 +00:00
|
|
|
|
|
|
|
// Scales.
|
2020-02-18 09:31:47 +00:00
|
|
|
const xMin = d3.min(attrs.data.primary.xs);
|
|
|
|
const xMax = d3.max(attrs.data.primary.xs);
|
2020-02-17 21:52:21 +00:00
|
|
|
|
|
|
|
if (attrs.scale === 'linear') {
|
2020-02-18 11:33:32 +00:00
|
|
|
this.xScale = d3.scaleLinear()
|
2020-02-18 11:58:34 +00:00
|
|
|
.domain([attrs.minX || xMin, attrs.maxX || xMax])
|
|
|
|
.range([0, calc.chartWidth]);
|
2020-02-17 21:52:21 +00:00
|
|
|
} else {
|
2020-02-18 11:33:32 +00:00
|
|
|
this.xScale = d3.scaleLog()
|
2020-02-17 21:52:21 +00:00
|
|
|
.base(attrs.logBase)
|
2020-02-18 09:31:47 +00:00
|
|
|
.domain([attrs.minX, attrs.maxX])
|
2020-02-17 21:52:21 +00:00
|
|
|
.range([0, calc.chartWidth]);
|
|
|
|
}
|
|
|
|
|
2020-02-18 09:31:47 +00:00
|
|
|
const yMin = d3.min(attrs.data.primary.ys);
|
|
|
|
const yMax = d3.max(attrs.data.primary.ys);
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-18 11:33:32 +00:00
|
|
|
this.yScale = d3.scaleLinear()
|
2020-02-18 09:31:47 +00:00
|
|
|
.domain([yMin, yMax])
|
2020-02-17 21:52:21 +00:00
|
|
|
.range([calc.chartHeight, 0]);
|
|
|
|
|
|
|
|
// Axis generator.
|
2020-02-19 07:36:11 +00:00
|
|
|
if (!!this.attrs.timeScale) {
|
|
|
|
const zero = _.get(this.attrs.timeScale, 'zero', moment());
|
2020-02-19 08:33:50 +00:00
|
|
|
const step = _.get(this.attrs.timeScale, 'step', 'years');
|
|
|
|
const length = _.get(this.attrs.timeScale, 'length', moment());
|
2020-02-19 07:36:11 +00:00
|
|
|
|
2020-02-19 08:33:50 +00:00
|
|
|
const xScaleTime = d3.scaleTime()
|
2020-02-19 07:36:11 +00:00
|
|
|
.domain([zero.toDate(), length.toDate()])
|
2020-02-19 08:33:50 +00:00
|
|
|
.nice()
|
2020-02-19 07:36:11 +00:00
|
|
|
.range([0, calc.chartWidth]);
|
|
|
|
|
2020-02-18 11:58:34 +00:00
|
|
|
this.xAxis = d3.axisBottom()
|
2020-02-19 08:33:50 +00:00
|
|
|
.scale(xScaleTime)
|
|
|
|
.ticks(this.getTimeTicksByStr(step))
|
2020-02-18 11:58:34 +00:00
|
|
|
.tickFormat(this.formatDates);
|
|
|
|
} else {
|
|
|
|
this.xAxis = d3.axisBottom(this.xScale)
|
|
|
|
.ticks(3)
|
|
|
|
.tickFormat(d => {
|
|
|
|
if (Math.abs(d) < 1) {
|
|
|
|
return d3.format(".2")(d);
|
|
|
|
} else if (xMin > 1000 && xMax < 3000) {
|
|
|
|
// Condition which identifies years; 2019, 2020, 2021.
|
|
|
|
return d3.format(".0")(d);
|
|
|
|
} else {
|
|
|
|
const prefix = d3.formatPrefix(".0", d);
|
|
|
|
return prefix(d).replace("G", "B");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-19 08:39:57 +00:00
|
|
|
// Objects.
|
2020-02-18 09:31:47 +00:00
|
|
|
const line = d3.line()
|
2020-02-18 11:33:32 +00:00
|
|
|
.x(d => this.xScale(d.x))
|
|
|
|
.y(d => this.yScale(d.y));
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-18 09:31:47 +00:00
|
|
|
const area = d3.area()
|
2020-02-18 11:33:32 +00:00
|
|
|
.x(d => this.xScale(d.x))
|
|
|
|
.y1(d => this.yScale(d.y))
|
2020-02-17 21:52:21 +00:00
|
|
|
.y0(calc.chartHeight);
|
|
|
|
|
|
|
|
// Add svg.
|
2020-02-18 09:31:47 +00:00
|
|
|
const svg = container
|
2020-02-17 21:52:21 +00:00
|
|
|
.patternify({ tag: 'svg', selector: 'svg-chart-container' })
|
|
|
|
.attr('width', "100%")
|
|
|
|
.attr('height', attrs.svgHeight)
|
|
|
|
.attr('pointer-events', 'none');
|
|
|
|
|
|
|
|
// Add container g element.
|
2020-02-18 11:29:51 +00:00
|
|
|
this.chart = svg
|
2020-02-17 21:52:21 +00:00
|
|
|
.patternify({ tag: 'g', selector: 'chart' })
|
|
|
|
.attr(
|
|
|
|
'transform',
|
|
|
|
'translate(' + calc.chartLeftMargin + ',' + calc.chartTopMargin + ')',
|
|
|
|
);
|
|
|
|
|
|
|
|
// Add axis.
|
2020-02-18 11:29:51 +00:00
|
|
|
this.chart.patternify({ tag: 'g', selector: 'axis' })
|
2020-02-17 21:52:21 +00:00
|
|
|
.attr('transform', 'translate(' + 0 + ',' + calc.chartHeight + ')')
|
2020-02-18 11:58:34 +00:00
|
|
|
.call(this.xAxis);
|
2020-02-17 21:52:21 +00:00
|
|
|
|
|
|
|
// Draw area.
|
2020-02-18 11:29:51 +00:00
|
|
|
this.chart
|
2020-02-17 21:52:21 +00:00
|
|
|
.patternify({
|
|
|
|
tag: 'path',
|
|
|
|
selector: 'area-path',
|
2020-02-18 11:58:34 +00:00
|
|
|
data: this.dataPoints,
|
2020-02-17 21:52:21 +00:00
|
|
|
})
|
|
|
|
.attr('d', area)
|
2020-02-19 08:39:57 +00:00
|
|
|
.attr('fill', (d, i) => areaColorRange(i))
|
2020-02-18 09:31:47 +00:00
|
|
|
.attr('opacity', (d, i) => i === 0 ? 0.7 : 0.5);
|
2020-02-17 21:52:21 +00:00
|
|
|
|
|
|
|
// Draw line.
|
|
|
|
if (attrs.showDistributionLines) {
|
2020-02-18 11:29:51 +00:00
|
|
|
this.chart
|
2020-02-17 21:52:21 +00:00
|
|
|
.patternify({
|
|
|
|
tag: 'path',
|
|
|
|
selector: 'line-path',
|
2020-02-18 11:58:34 +00:00
|
|
|
data: this.dataPoints,
|
2020-02-17 21:52:21 +00:00
|
|
|
})
|
|
|
|
.attr('d', line)
|
|
|
|
.attr('id', (d, i) => 'line-' + (i + 1))
|
2020-02-18 11:58:34 +00:00
|
|
|
.attr('opacity', (d, i) => i === 0 ? 0.7 : 1)
|
2020-02-17 21:52:21 +00:00
|
|
|
.attr('fill', 'none');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attrs.showVerticalLine) {
|
2020-02-18 11:29:51 +00:00
|
|
|
this.chart
|
|
|
|
.patternify({ tag: 'line', selector: 'v-line' })
|
2020-02-18 11:58:34 +00:00
|
|
|
.attr('x1', this.xScale(attrs.verticalLine))
|
|
|
|
.attr('x2', this.xScale(attrs.verticalLine))
|
2020-02-17 21:52:21 +00:00
|
|
|
.attr('y1', 0)
|
|
|
|
.attr('y2', calc.chartHeight)
|
|
|
|
.attr('stroke-width', 1.5)
|
|
|
|
.attr('stroke-dasharray', '6 6')
|
|
|
|
.attr('stroke', 'steelblue');
|
|
|
|
}
|
|
|
|
|
2020-02-18 11:29:51 +00:00
|
|
|
this.hoverLine = this.chart
|
|
|
|
.patternify({ tag: 'line', selector: 'hover-line' })
|
2020-02-17 21:52:21 +00:00
|
|
|
.attr('x1', 0)
|
|
|
|
.attr('x2', 0)
|
|
|
|
.attr('y1', 0)
|
|
|
|
.attr('y2', calc.chartHeight)
|
|
|
|
.attr('opacity', 0)
|
|
|
|
.attr('stroke-width', 1.5)
|
|
|
|
.attr('stroke-dasharray', '6 6')
|
|
|
|
.attr('stroke', '#22313F');
|
|
|
|
|
|
|
|
// Add drawing rectangle.
|
2020-02-18 11:29:51 +00:00
|
|
|
const thi$ = this;
|
|
|
|
this.chart
|
|
|
|
.patternify({ tag: 'rect', selector: 'mouse-rect' })
|
2020-02-17 21:52:21 +00:00
|
|
|
.attr('width', calc.chartWidth)
|
|
|
|
.attr('height', calc.chartHeight)
|
|
|
|
.attr('fill', 'transparent')
|
|
|
|
.attr('pointer-events', 'all')
|
2020-02-18 11:29:51 +00:00
|
|
|
.on('mouseover', function () {
|
|
|
|
thi$.mouseover(this);
|
|
|
|
})
|
|
|
|
.on('mousemove', function () {
|
|
|
|
thi$.mouseover(this);
|
|
|
|
})
|
|
|
|
.on('mouseout', this.mouseout);
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-18 11:29:51 +00:00
|
|
|
return this;
|
|
|
|
}
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-18 11:29:51 +00:00
|
|
|
mouseover(constructor) {
|
|
|
|
const mouse = d3.mouse(constructor);
|
|
|
|
this.hoverLine.attr('opacity', 1)
|
|
|
|
.attr('x1', mouse[0])
|
|
|
|
.attr('x2', mouse[0]);
|
|
|
|
|
|
|
|
const xValue = this.xScale.invert(mouse[0]).toFixed(2);
|
|
|
|
|
|
|
|
const range = [
|
|
|
|
this.xScale(this.dataPoints[this.dataPoints.length - 1][0].x),
|
|
|
|
this.xScale(
|
|
|
|
this.dataPoints
|
|
|
|
[this.dataPoints.length - 1]
|
|
|
|
[this.dataPoints[this.dataPoints.length - 1].length - 1].x,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
|
|
|
|
if (mouse[0] > range[0] && mouse[0] < range[1]) {
|
|
|
|
this.attrs.onHover(xValue);
|
|
|
|
} else {
|
|
|
|
this.attrs.onHover(0.0);
|
2020-02-17 21:52:21 +00:00
|
|
|
}
|
2020-02-18 09:31:47 +00:00
|
|
|
}
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-18 11:29:51 +00:00
|
|
|
mouseout() {
|
|
|
|
this.hoverLine.attr('opacity', 0)
|
|
|
|
}
|
2020-02-18 11:58:34 +00:00
|
|
|
|
|
|
|
formatDates(ts) {
|
2020-02-19 08:33:50 +00:00
|
|
|
return moment(ts).format("MMMM Do YYYY");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} step
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
|
|
|
getTimeTicksByStr(step) {
|
|
|
|
switch (step) {
|
|
|
|
case "months":
|
|
|
|
return d3.timeMonth.every(1);
|
|
|
|
case "quarters":
|
|
|
|
return d3.timeMonth.every(3);
|
|
|
|
case "hours":
|
|
|
|
return d3.timeHour.every(1);
|
|
|
|
case "days":
|
|
|
|
return d3.timeDay.every(1);
|
|
|
|
case "seconds":
|
|
|
|
return d3.timeSecond.every(1);
|
|
|
|
case "years":
|
|
|
|
return d3.timeYear.every(1);
|
|
|
|
case "minutes":
|
|
|
|
return d3.timeMinute.every(1);
|
|
|
|
case "weeks":
|
|
|
|
return d3.timeWeek.every(1);
|
|
|
|
case "milliseconds":
|
|
|
|
return d3.timeMillisecond.every(1);
|
|
|
|
default:
|
|
|
|
return d3.timeYear.every(1);
|
|
|
|
}
|
2020-02-18 11:58:34 +00:00
|
|
|
}
|
2020-02-18 11:29:51 +00:00
|
|
|
}
|
2020-02-17 21:52:21 +00:00
|
|
|
|
2020-02-19 08:39:57 +00:00
|
|
|
/**
|
|
|
|
* @todo: To rework it somehow.
|
|
|
|
* @param params
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
2020-02-19 05:20:54 +00:00
|
|
|
d3.selection.prototype.patternify = function patternify(params) {
|
|
|
|
const selector = params.selector;
|
|
|
|
const elementTag = params.tag;
|
|
|
|
const data = params.data || [selector];
|
|
|
|
|
2020-02-19 07:36:11 +00:00
|
|
|
const selection = this.selectAll('.' + selector)
|
|
|
|
.data(data, (d, i) => {
|
|
|
|
if (typeof d === 'object' && d.id) return d.id;
|
|
|
|
return i;
|
|
|
|
});
|
2020-02-19 05:20:54 +00:00
|
|
|
|
|
|
|
selection.exit().remove();
|
|
|
|
|
|
|
|
return selection
|
|
|
|
.enter()
|
|
|
|
.append(elementTag)
|
|
|
|
.merge(selection)
|
|
|
|
.attr('class', selector);
|
|
|
|
};
|