charts/src/js/charts/AxisChart.js
2018-02-26 19:48:23 +05:30

510 lines
13 KiB
JavaScript

import BaseChart from './BaseChart';
import { dataPrep } from './axis-chart-utils';
import { Y_AXIS_MARGIN } from '../utils/constants';
import { getComponent } from '../objects/ChartComponents';
import { BarChartController, LineChartController, getPaths } from '../objects/AxisChartControllers';
import { AxisChartRenderer } from '../utils/draw';
import { getOffset, fire } from '../utils/dom';
import { equilizeNoOfElements } from '../utils/draw-utils';
import { Animator, translateHoriLine } from '../utils/animate';
import { runSMILAnimation } from '../utils/animation';
import { getRealIntervals, calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex } from '../utils/intervals';
import { floatTwo, fillArray } from '../utils/helpers';
import { MIN_BAR_PERCENT_HEIGHT, DEFAULT_AXIS_CHART_TYPE } from '../utils/constants';
export default class AxisChart extends BaseChart {
constructor(args) {
super(args);
this.isSeries = args.isSeries;
this.valuesOverPoints = args.valuesOverPoints;
this.formatTooltipY = args.formatTooltipY;
this.formatTooltipX = args.formatTooltipX;
this.barOptions = args.barOptions;
this.lineOptions = args.lineOptions;
this.type = args.type || 'line';
this.xAxisMode = args.xAxisMode || 'span';
this.yAxisMode = args.yAxisMode || 'span';
this.zeroLine = this.height;
this.setTrivialState();
this.setup();
}
configure(args) {
super.configure();
this.config.xAxisMode = args.xAxisMode;
this.config.yAxisMode = args.yAxisMode;
}
setTrivialState() {
// Define data and stuff
let xLabels = this.data.labels;
this.state = {
xAxis: {
positions: [],
labels: xLabels,
},
yAxis: {
positions: [],
labels: [],
},
datasetLength: xLabels.length
}
this.setObservers();
}
setObservers() {
// go through each component and check the keys in this.state it depends on
// set an observe() on each of those keys for that component
}
setMargins() {
super.setMargins();
this.translateXLeft = Y_AXIS_MARGIN;
this.translateXRight = Y_AXIS_MARGIN;
}
prepareData(data=this.data) {
return dataPrep(data, this.type);
}
calc() {
let s = this.state;
this.calcXPositions();
s.datasetsLabels = this.data.datasets.map(d => d.name);
this.setYAxis();
this.calcYUnits();
this.calcYMaximums();
this.calcYRegions();
}
setYAxis() {
this.calcYAxisParameters(this.state.yAxis, this.getAllYValues(), this.type === 'line');
this.state.zeroLine = this.state.yAxis.zeroLine;
}
calcXPositions() {
let s = this.state;
this.setUnitWidthAndXOffset();
s.xAxisPositions = s.xAxis.labels.map((d, i) =>
floatTwo(s.xOffset + i * s.unitWidth)
);
s.xUnitPositions = new Array(this.data.datasets.length).fill(s.xAxisPositions);
}
calcYAxisParameters(yAxis, dataValues, withMinimum = 'false') {
yAxis.labels = calcChartIntervals(dataValues, withMinimum);
const yPts = yAxis.labels;
yAxis.scaleMultiplier = this.height / getValueRange(yPts);
const intervalHeight = getIntervalSize(yPts) * yAxis.scaleMultiplier;
yAxis.zeroLine = this.height - (getZeroIndex(yPts) * intervalHeight);
yAxis.positions = yPts.map(d => yAxis.zeroLine - d * yAxis.scaleMultiplier);
}
calcYUnits() {
let s = this.state;
this.data.datasets.map(d => {
d.positions = d.values.map(val =>
floatTwo(s.yAxis.zeroLine - val * s.yAxis.scaleMultiplier));
});
if(this.barOptions && this.barOptions.stacked) {
this.data.datasets.map((d, i) => {
d.cumulativePositions = d.cumulativeYs.map(val =>
floatTwo(s.yAxis.zeroLine - val * s.yAxis.scaleMultiplier));
});
}
}
calcYMaximums() {
let s = this.state;
if(this.barOptions && this.barOptions.stacked) {
s.yExtremes = this.data.datasets[this.data.datasets.length - 1].cumulativePositions;
return;
}
s.yExtremes = new Array(s.datasetLength).fill(9999);
this.data.datasets.map((d, i) => {
d.positions.map((pos, j) => {
if(pos < s.yExtremes[j]) {
s.yExtremes[j] = pos;
}
});
});
// Tooltip refresh should not be needed?
// this.chartWrapper.removeChild(this.tip.container);
// this.make_tooltip();
}
calcYRegions() {
let s = this.state;
if(this.data.yMarkers) {
this.data.yMarkers = this.data.yMarkers.map(d => {
d.position = floatTwo(s.yAxis.zeroLine - d.value * s.yAxis.scaleMultiplier);
d.label += ': ' + d.value;
return d;
});
}
if(this.data.yRegions) {
this.data.yRegions = this.data.yRegions.map(d => {
if(d.end < d.start) {
[d.start, d.end] = [d.end, start];
}
d.start = floatTwo(s.yAxis.zeroLine - d.start * s.yAxis.scaleMultiplier);
d.end = floatTwo(s.yAxis.zeroLine - d.end * s.yAxis.scaleMultiplier);
return d;
});
}
}
// Default, as per bar, and mixed. Only line will be a special case
setUnitWidthAndXOffset() {
this.state.unitWidth = this.width/(this.state.datasetLength);
this.state.xOffset = this.state.unitWidth/2;
}
getAllYValues() {
// TODO: yMarkers, regions, sums, every Y value ever
let key = 'values';
if(this.barOptions && this.barOptions.stacked) {
key = 'cumulativeYs';
let cumulative = new Array(this.state.datasetLength).fill(0);
this.data.datasets.map((d, i) => {
let values = this.data.datasets[i].values;
d[key] = cumulative = cumulative.map((c, i) => c + values[i]);
});
}
return [].concat(...this.data.datasets.map(d => d[key]));
}
initComponents() {
this.componentConfigs = [
[
'yAxis',
this.drawArea,
{
mode: this.yAxisMode,
width: this.width,
// pos: 'right'
},
{
positions: getRealIntervals(this.height, 4, 0, 0),
labels: getRealIntervals(this.height, 4, 0, 0).map(d => d + ""),
},
function() {
let s = this.state;
return {
positions: s.yAxis.positions,
labels: s.yAxis.labels,
}
}.bind(this)
],
[
'xAxis',
this.drawArea,
{
mode: this.xAxisMode,
height: this.height,
// pos: 'right'
},
{
positions: getRealIntervals(this.width, 4, 0, 1),
labels: getRealIntervals(this.width, 4, 0, 1).map(d => d + ""),
},
function() {
let s = this.state;
return {
positions: s.xAxisPositions,
labels: s.xAxis.labels,
}
}.bind(this)
],
[
'yRegions',
this.drawArea,
{
// mode: this.yAxisMode,
width: this.width,
pos: 'right'
},
[
{
start: this.height,
end: this.height,
label: ''
}
],
function() {
return this.data.yRegions || [];
}.bind(this)
],
[
'yMarkers',
this.drawArea,
{
// mode: this.yAxisMode,
width: this.width,
pos: 'right'
},
[
{
position: this.height,
label: ''
}
],
function() {
return this.data.yMarkers || [];
}.bind(this)
]
];
}
setupComponents() {
let optionals = ['yMarkers', 'yRegions'];
this.components = this.componentConfigs
.filter(args => !optionals.includes(args[0]) || this.data[args[0]])
.map(args => getComponent(...args));
}
getChartComponents() {
let dataUnitsComponents = []
// this.state is not defined at this stage
this.data.datasets.forEach((d, index) => {
if(d.chartType === 'line') {
dataUnitsComponents.push(this.getPathComponent(d, index));
}
let renderer = this.unitRenderers[d.chartType];
dataUnitsComponents.push(this.getDataUnitComponent(
index, renderer
));
});
return dataUnitsComponents;
}
getDataUnitComponent(index, unitRenderer) {
return new ChartComponent({
layerClass: 'dataset-units dataset-' + index,
makeElements: () => {
// yPositions, xPostions, color, valuesOverPoints,
let d = this.data.datasets[index];
return d.positions.map((y, j) => {
return unitRenderer.draw(
this.state.xAxisPositions[j],
y,
this.colors[index],
(this.valuesOverPoints ? (this.barOptions &&
this.barOptions.stacked ? d.cumulativeYs[j] : d.values[j]) : ''),
j,
y - (d.cumulativePositions ? d.cumulativePositions[j] : y)
);
});
},
postMake: function() {
let translate_layer = () => {
this.layer.setAttribute('transform', `translate(${unitRenderer.consts.width * index}, 0)`);
}
// let d = this.data.datasets[index];
if(this.meta.type === 'bar' && (!this.meta.barOptions
|| !this.meta.barOptions.stacked)) {
translate_layer();
}
},
animate: (svgUnits) => {
// have been updated in axis render;
let newX = this.state.xAxisPositions;
let newY = this.data.datasets[index].positions;
let lastUnit = svgUnits[svgUnits.length - 1];
let parentNode = lastUnit.parentNode;
if(this.oldState.xExtra > 0) {
for(var i = 0; i<this.oldState.xExtra; i++) {
let unit = lastUnit.cloneNode(true);
parentNode.appendChild(unit);
svgUnits.push(unit);
}
}
svgUnits.map((unit, i) => {
if(newX[i] === undefined || newY[i] === undefined) return;
this.elementsToAnimate.push(unitRenderer.animate(
unit, // unit, with info to replace where it came from in the data
newX[i],
newY[i],
index,
this.data.datasets.length
));
});
}
});
}
getPathComponent(d, index) {
return new ChartComponent({
layerClass: 'path dataset-path',
setData: () => {},
makeElements: () => {
let d = this.data.datasets[index];
let color = this.colors[index];
return getPaths(
d.positions,
this.state.xAxisPositions,
color,
this.config.heatline,
this.config.regionFill
);
},
animate: (paths) => {
let newX = this.state.xAxisPositions;
let newY = this.data.datasets[index].positions;
let oldX = this.oldState.xAxisPositions;
let oldY = this.oldState.datasets[index].positions;
let parentNode = paths[0].parentNode;
[oldX, newX] = equilizeNoOfElements(oldX, newX);
[oldY, newY] = equilizeNoOfElements(oldY, newY);
if(this.oldState.xExtra > 0) {
paths = getPaths(
oldY, oldX, this.colors[index],
this.config.heatline,
this.config.regionFill
);
parentNode.textContent = '';
paths.map(path => parentNode.appendChild(path));
}
const newPointsList = newY.map((y, i) => (newX[i] + ',' + y));
this.elementsToAnimate = this.elementsToAnimate
.concat(this.renderer.animatepath(paths, newPointsList.join("L")));
}
});
}
bindTooltip() {
// TODO: could be in tooltip itself, as it is a given functionality for its parent
this.chartWrapper.addEventListener('mousemove', (e) => {
let o = getOffset(this.chartWrapper);
let relX = e.pageX - o.left - this.translateXLeft;
let relY = e.pageY - o.top - this.translateY;
if(relY < this.height + this.translateY * 2) {
this.mapTooltipXPosition(relX);
} else {
this.tip.hide_tip();
}
});
}
mapTooltipXPosition(relX) {
let s = this.state;
if(!s.yExtremes) return;
let titles = s.xAxis.labels;
if(this.formatTooltipX && this.formatTooltipX(titles[0])) {
titles = titles.map(d=>this.formatTooltipX(d));
}
let formatY = this.formatTooltipY && this.formatTooltipY(this.y[0].values[0]);
for(var i=s.datasetLength - 1; i >= 0 ; i--) {
let xVal = s.xAxisPositions[i];
// let delta = i === 0 ? s.unitWidth : xVal - s.xAxisPositions[i-1];
if(relX > xVal - s.unitWidth/2) {
let x = xVal + this.translateXLeft;
let y = s.yExtremes[i] + this.translateY;
let values = this.data.datasets.map((set, j) => {
return {
title: set.title,
value: formatY ? this.formatTooltipY(set.values[i]) : set.values[i],
color: this.colors[j],
};
});
this.tip.set_values(x, y, titles[i], '', values);
this.tip.show_tip();
break;
}
}
}
getDataPoint(index=this.current_index) {
// check for length
let data_point = {
index: index
};
let y = this.y[0];
['svg_units', 'yUnitPositions', 'values'].map(key => {
let data_key = key.slice(0, key.length-1);
data_point[data_key] = y[key][index];
});
data_point.label = this.xAxis.labels[index];
return data_point;
}
setCurrentDataPoint(index) {
index = parseInt(index);
if(index < 0) index = 0;
if(index >= this.xAxis.labels.length) index = this.xAxis.labels.length - 1;
if(index === this.current_index) return;
this.current_index = index;
$.fire(this.parent, "data-select", this.getDataPoint());
}
// API
addDataPoint(label, datasetValues, index=this.state.datasetLength) {
super.addDataPoint(label, datasetValues, index);
// console.log(label, datasetValues, this.data.labels);
this.data.labels.splice(index, 0, label);
this.data.datasets.map((d, i) => {
d.values.splice(index, 0, datasetValues[i]);
});
// console.log(this.data);
this.update(this.data);
}
removeDataPoint(index = this.state.datasetLength-1) {
super.removeDataPoint(index);
this.data.labels.splice(index, 1);
this.data.datasets.map(d => {
d.values.splice(index, 1);
});
this.update(this.data);
}
// updateData() {
// // animate if same no. of datasets,
// // else return new chart
// //
// }
}
// keep a binding at the end of chart