[data] begin with zero data, bars rendered

This commit is contained in:
Prateeksha Singh 2018-02-27 22:18:57 +05:30
parent 8d2246e6c3
commit 8452e909bb
17 changed files with 479 additions and 459 deletions

View File

@ -232,6 +232,19 @@ function getStringWidth(string, charWidth) {
return (string+"").length * charWidth; return (string+"").length * charWidth;
} }
function getBarHeightAndYAttr(yTop, zeroLine) {
let height, y;
if (yTop < zeroLine) {
height = zeroLine - yTop;
y = yTop;
} else {
height = yTop - zeroLine;
y = zeroLine;
}
return [height, y];
}
function equilizeNoOfElements(array1, array2, function equilizeNoOfElements(array1, array2,
extra_count=array2.length - array1.length) { extra_count=array2.length - array1.length) {
@ -347,26 +360,6 @@ function createSVG(tag, o) {
return element; return element;
} }
function renderVerticalGradient(svgDefElem, gradientId) {
return createSVG('linearGradient', {
inside: svgDefElem,
id: gradientId,
x1: 0,
x2: 0,
y1: 0,
y2: 1
});
}
function setGradientStop(gradElem, offset, color, opacity) {
return createSVG('stop', {
'inside': gradElem,
'style': `stop-color: ${color}`,
'offset': offset,
'stop-opacity': opacity
});
}
function makeSVGContainer(parent, className, width, height) { function makeSVGContainer(parent, className, width, height) {
return createSVG('svg', { return createSVG('svg', {
className: className, className: className,
@ -390,7 +383,13 @@ function makeSVGGroup(parent, className, transform='') {
}); });
} }
function wrapInSVGGroup(elements, className='') {
let g = createSVG('g', {
className: className
});
elements.forEach(e => g.appendChild(e));
return g;
}
function makePath(pathStr, className='', stroke='none', fill='none') { function makePath(pathStr, className='', stroke='none', fill='none') {
return createSVG('path', { return createSVG('path', {
@ -403,20 +402,7 @@ function makePath(pathStr, className='', stroke='none', fill='none') {
}); });
} }
function makeGradient(svgDefElem, color, lighter = false) {
let gradientId ='path-fill-gradient' + '-' + color;
let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
let opacities = [1, 0.6, 0.2];
if(lighter) {
opacities = [0.4, 0.2, 0];
}
setGradientStop(gradientDef, "0%", color, opacities[0]);
setGradientStop(gradientDef, "50%", color, opacities[1]);
setGradientStop(gradientDef, "100%", color, opacities[2]);
return gradientId;
}
function makeHeatSquare(className, x, y, size, fill='none', data={}) { function makeHeatSquare(className, x, y, size, fill='none', data={}) {
let args = { let args = {
@ -638,6 +624,37 @@ function yRegion(y1, y2, width, label) {
return region; return region;
} }
function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
// console.log(yTop, meta.zeroLine, y, offset);
let rect = createSVG('rect', {
className: `bar mini`,
style: `fill: ${color}`,
'data-point-index': index,
x: x - meta.barsWidth/2,
y: y - offset,
width: width,
height: height || meta.minHeight
});
if(!label && !label.length) {
return rect;
} else {
let text = createSVG('text', {
className: 'data-point-value',
x: x,
y: y - offset,
dy: (FONT_SIZE / 2 * -1) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
});
return wrapInSVGGroup([rect, text]);
}
}
const PRESET_COLOR_MAP = { const PRESET_COLOR_MAP = {
'light-blue': '#7cd6fd', 'light-blue': '#7cd6fd',
'blue': '#5e64ff', 'blue': '#5e64ff',
@ -872,13 +889,15 @@ class BaseChart {
this.title = title; this.title = title;
this.subtitle = subtitle; this.subtitle = subtitle;
this.argHeight = height; this.argHeight = height;
this.type = type;
this.isNavigable = isNavigable; this.isNavigable = isNavigable;
if(this.isNavigable) { if(this.isNavigable) {
this.currentIndex = 0; this.currentIndex = 0;
} }
this.data = this.prepareData(data); this.realData = this.prepareData(data);
this.data = this.prepareFirstData(this.realData);
this.colors = []; this.colors = [];
this.config = {}; this.config = {};
this.state = {}; this.state = {};
@ -990,6 +1009,7 @@ class BaseChart {
this.calcWidth(); this.calcWidth();
this.makeChartArea(); this.makeChartArea();
this.calc();
this.initComponents(); // Only depend on the drawArea made in makeChartArea this.initComponents(); // Only depend on the drawArea made in makeChartArea
this.setupComponents(); this.setupComponents();
@ -1002,6 +1022,7 @@ class BaseChart {
// TODO: remove timeout and decrease post animate time in chart component // TODO: remove timeout and decrease post animate time in chart component
if(init) { if(init) {
this.data = this.realData;
setTimeout(() => {this.update();}, 1000); setTimeout(() => {this.update();}, 1000);
} }
} }
@ -1029,10 +1050,10 @@ class BaseChart {
calc() {} // builds state calc() {} // builds state
render(animate=true) { render(components=this.components, animate=true) {
// Can decouple to this.refreshComponents() first to save animation timeout // Can decouple to this.refreshComponents() first to save animation timeout
let elementsToAnimate = []; let elementsToAnimate = [];
this.components.forEach(c => { components.forEach(c => {
elementsToAnimate = elementsToAnimate.concat(c.update(animate)); elementsToAnimate = elementsToAnimate.concat(c.update(animate));
}); });
if(elementsToAnimate.length > 0) { if(elementsToAnimate.length > 0) {
@ -1138,8 +1159,10 @@ class BaseChart {
const Y_AXIS_MARGIN = 60; const Y_AXIS_MARGIN = 60;
const MIN_BAR_PERCENT_HEIGHT = 0.01;
const DEFAULT_AXIS_CHART_TYPE = 'line'; const DEFAULT_AXIS_CHART_TYPE = 'line';
const AXIS_DATASET_CHART_TYPES = ['line', 'bar'];
const BAR_CHART_SPACE_RATIO = 0.5;
function dataPrep(data, type) { function dataPrep(data, type) {
data.labels = data.labels || []; data.labels = data.labels || [];
@ -1176,23 +1199,60 @@ function dataPrep(data, type) {
// Set labels // Set labels
// //
// Set index
d.index = i;
// Set type // Set type
if(!d.chartType ) { if(!d.chartType ) {
d.chartType = type || DEFAULT_AXIS_CHART_TYPE; if(!AXIS_DATASET_CHART_TYPES.includes(type)) type === DEFAULT_AXIS_CHART_TYPE;
d.chartType = type;
} }
}); });
// Markers // Markers
// Regions // Regions
// Set start and end // data.yRegions = data.yRegions || [];
if(data.yRegions) {
data.yRegions.map(d => {
if(d.end < d.start) {
[d.start, d.end] = [d.end, start];
}
});
}
return data; return data;
} }
function zeroDataPrep(realData) {
let datasetLength = realData.labels.length;
let zeroArray = new Array(datasetLength).fill(0);
let zeroData = {
labels: realData.labels,
datasets: realData.datasets.map(d => {
return {
name: '',
values: zeroArray,
chartType: d.chartType
}
}),
yRegions: [
{
start: 0,
end: 0,
label: ''
}
],
yMarkers: [
{
value: 0,
label: ''
}
]
};
return zeroData;
}
class ChartComponent$1 { class ChartComponent$1 {
constructor({ constructor({
layerClass = '', layerClass = '',
@ -1215,8 +1275,9 @@ class ChartComponent$1 {
this.store = []; this.store = [];
this.layerClass = typeof(layerClass) === 'function' this.layerClass = layerClass;
? layerClass() : layerClass; this.layerClass = typeof(this.layerClass) === 'function'
? this.layerClass() : this.layerClass;
this.refresh(); this.refresh();
} }
@ -1394,81 +1455,61 @@ let componentConfigs = {
}, },
barGraph: { barGraph: {
// opt:[ layerClass: function() { return 'dataset-units dataset-' + this.constants.index; },
// 'barGraph',
// this.drawArea,
// {
// controller: barController,
// index: index,
// color: this.colors[index],
// valuesOverPoints: this.valuesOverPoints,
// stacked: this.barOptions && this.barOptions.stacked,
// spaceRatio: 0.5,
// minHeight: this.height * MIN_BAR_PERCENT_HEIGHT
// },
// {
// barsWidth: this.state.unitWidth * (1 - spaceRatio),
// barWidth: barsWidth/(stacked ? 1 : this.state.noOfDatasets),
// },
// function() {
// let s = this.state;
// return {
// barsWidth: this.state.unitWidth * (1 - spaceRatio),
// barWidth: barsWidth/(stacked ? 1 : this.state.noOfDatasets),
// positions: s.xAxisPositions,
// labels: s.xAxisLabels,
// }
// }.bind(this)
// ],
layerClass() { return 'y-regions' + this.constants.index; },
makeElements(data) { makeElements(data) {
let c = this.constants; let c = this.constants;
return data.yPositions.map((y, j) => return data.yPositions.map((y, j) => {
barController.draw( // console.log(data.cumulativeYPos, data.cumulativeYPos[j]);
return datasetBar(
data.xPositions[j], data.xPositions[j],
y, y,
color, c.barWidth,
c.color,
(c.valuesOverPoints ? (c.stacked ? data.cumulativeYs[j] : data.values[j]) : ''), (c.valuesOverPoints ? (c.stacked ? data.cumulativeYs[j] : data.values[j]) : ''),
j, j,
y - (data.cumulativePositions ? data.cumulativePositions[j] : y) y - (c.stacked ? data.cumulativeYPos[j] : y),
{
zeroLine: c.zeroLine,
barsWidth: c.barsWidth,
minHeight: c.minHeight
}
) )
); });
}, },
postMake() { postMake() {
if((!this.constants.stacked)) { if((!this.constants.stacked)) {
this.layer.setAttribute('transform', this.layer.setAttribute('transform',
`translate(${unitRenderer.consts.width * index}, 0)`); `translate(${this.constants.width * this.constants.index}, 0)`);
} }
}, },
animateElements(newData) { animateElements(newData) {
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); // [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
let newPos = newData.map(d => d.end); // let newPos = newData.map(d => d.end);
let newLabels = newData.map(d => d.label); // let newLabels = newData.map(d => d.label);
let newStarts = newData.map(d => d.start); // let newStarts = newData.map(d => d.start);
let oldPos = this.oldData.map(d => d.end); // let oldPos = this.oldData.map(d => d.end);
let oldLabels = this.oldData.map(d => d.label); // let oldLabels = this.oldData.map(d => d.label);
let oldStarts = this.oldData.map(d => d.start); // let oldStarts = this.oldData.map(d => d.start);
this.render(oldPos.map((pos, i) => { // this.render(oldPos.map((pos, i) => {
return { // return {
start: oldStarts[i], // start: oldStarts[i],
end: oldPos[i], // end: oldPos[i],
label: newLabels[i] // label: newLabels[i]
} // }
})); // }));
let animateElements = []; // let animateElements = [];
this.store.map((rectGroup, i) => { // this.store.map((rectGroup, i) => {
animateElements = animateElements.concat(animateRegion( // animateElements = animateElements.concat(animateRegion(
rectGroup, newStarts[i], newPos[i], oldPos[i] // rectGroup, newStarts[i], newPos[i], oldPos[i]
)); // ));
}); // });
return animateElements; // return animateElements;
} }
}, },
@ -1486,101 +1527,6 @@ function getComponent(name, constants, getData) {
return new ChartComponent$1(config); return new ChartComponent$1(config);
} }
function getPaths(yList, xList, color, heatline=false, regionFill=false) {
let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
let pointsStr = pointsList.join("L");
let path = makePath("M"+pointsStr, 'line-graph-path', color);
// HeatLine
if(heatline) {
let gradient_id = makeGradient(this.svgDefs, color);
path.style.stroke = `url(#${gradient_id})`;
}
let components = [path];
// Region
if(regionFill) {
let gradient_id_region = makeGradient(this.svgDefs, color, true);
let zeroLine = this.state.yAxis.zeroLine;
// TODO: use zeroLine OR minimum
let pathStr = "M" + `0,${zeroLine}L` + pointsStr + `L${this.width},${zeroLine}`;
components.push(makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`));
}
return components;
}
// class BarChart extends AxisChart {
// constructor(args) {
// super(args);
// this.type = 'bar';
// this.setup();
// }
// configure(args) {
// super.configure(args);
// this.config.xAxisMode = args.xAxisMode || 'tick';
// this.config.yAxisMode = args.yAxisMode || 'span';
// }
// // =================================
// makeOverlay() {
// // Just make one out of the first element
// let index = this.xAxisLabels.length - 1;
// let unit = this.y[0].svg_units[index];
// this.setCurrentDataPoint(index);
// if(this.overlay) {
// this.overlay.parentNode.removeChild(this.overlay);
// }
// this.overlay = unit.cloneNode();
// this.overlay.style.fill = '#000000';
// this.overlay.style.opacity = '0.4';
// this.drawArea.appendChild(this.overlay);
// }
// bindOverlay() {
// // on event, update overlay
// this.parent.addEventListener('data-select', (e) => {
// this.update_overlay(e.svg_unit);
// });
// }
// bind_units(units_array) {
// units_array.map(unit => {
// unit.addEventListener('click', () => {
// let index = unit.getAttribute('data-point-index');
// this.setCurrentDataPoint(index);
// });
// });
// }
// update_overlay(unit) {
// let attributes = [];
// Object.keys(unit.attributes).map(index => {
// attributes.push(unit.attributes[index]);
// });
// attributes.filter(attr => attr.specified).map(attr => {
// this.overlay.setAttribute(attr.name, attr.nodeValue);
// });
// this.overlay.style.fill = '#000000';
// this.overlay.style.opacity = '0.4';
// }
// onLeftArrow() {
// this.setCurrentDataPoint(this.currentIndex - 1);
// }
// onRightArrow() {
// this.setCurrentDataPoint(this.currentIndex + 1);
// }
// }
function normalize(x) { function normalize(x) {
// Calculates mantissa and exponent of a number // Calculates mantissa and exponent of a number
// Returns normalized number and exponent // Returns normalized number and exponent
@ -1757,17 +1703,7 @@ function getZeroIndex(yPts) {
return zeroIndex; return zeroIndex;
} }
function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) {
let range = max - min;
let part = range * 1.0 / noOfIntervals;
let intervals = [];
for(var i = 0; i <= noOfIntervals; i++) {
intervals.push(min + part * i);
}
return asc ? intervals : intervals.reverse();
}
function getIntervalSize(orderedArray) { function getIntervalSize(orderedArray) {
return orderedArray[1] - orderedArray[0]; return orderedArray[1] - orderedArray[0];
@ -1777,6 +1713,10 @@ function getValueRange(orderedArray) {
return orderedArray[orderedArray.length-1] - orderedArray[0]; return orderedArray[orderedArray.length-1] - orderedArray[0];
} }
function scale(val, yAxis) {
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier)
}
function calcDistribution(values, distributionSize) { function calcDistribution(values, distributionSize) {
// Assume non-negative values, // Assume non-negative values,
// implying distribution minimum at zero // implying distribution minimum at zero
@ -1805,19 +1745,19 @@ class AxisChart extends BaseChart {
this.valuesOverPoints = args.valuesOverPoints; this.valuesOverPoints = args.valuesOverPoints;
this.formatTooltipY = args.formatTooltipY; this.formatTooltipY = args.formatTooltipY;
this.formatTooltipX = args.formatTooltipX; this.formatTooltipX = args.formatTooltipX;
this.barOptions = args.barOptions; this.barOptions = args.barOptions || {};
this.lineOptions = args.lineOptions; this.lineOptions = args.lineOptions || {};
this.type = args.type || 'line'; this.type = args.type || 'line';
this.xAxisMode = args.xAxisMode || 'span'; this.xAxisMode = args.xAxisMode || 'span';
this.yAxisMode = args.yAxisMode || 'span'; this.yAxisMode = args.yAxisMode || 'span';
this.zeroLine = this.height; this.zeroLine = this.height;
this.setTrivialState(); // this.setTrivialState();
this.setup(); this.setup();
} }
configure(args) { configure(args) {3;
super.configure(); super.configure();
// TODO: set in options and use // TODO: set in options and use
@ -1826,38 +1766,6 @@ class AxisChart extends BaseChart {
this.config.yAxisMode = args.yAxisMode; this.config.yAxisMode = args.yAxisMode;
} }
setTrivialState() {
// Define data and stuff
let yTempPos = getRealIntervals(this.height, 4, 0, 0);
this.state = {
xAxis: {
positions: [],
labels: [],
},
yAxis: {
positions: yTempPos,
labels: yTempPos.map(d => ""),
},
yRegions: [
{
start: this.height,
end: this.height,
label: ''
}
],
yMarkers: [
{
position: this.height,
label: ''
}
]
};
this.calcWidth();
this.calcXPositions(this.state);
}
setMargins() { setMargins() {
super.setMargins(); super.setMargins();
this.translateXLeft = Y_AXIS_MARGIN; this.translateXLeft = Y_AXIS_MARGIN;
@ -1868,10 +1776,12 @@ class AxisChart extends BaseChart {
return dataPrep(data, this.type); return dataPrep(data, this.type);
} }
prepareFirstData(data=this.data) {
return zeroDataPrep(data);
}
calc() { calc() {
this.calcXPositions(); this.calcXPositions();
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); this.calcYAxisParameters(this.getAllYValues(), this.type === 'line');
} }
@ -1905,61 +1815,60 @@ class AxisChart extends BaseChart {
}; };
this.calcYUnits(); this.calcYUnits();
this.calcYMaximums(); this.calcYExtremes();
this.calcYRegions(); this.calcYRegions();
} }
calcYUnits() { calcYUnits() {
let s = this.state; let s = this.state;
this.data.datasets.map(d => { let scaleAll = values => values.map(val => scale(val, s.yAxis));
d.positions = d.values.map(val =>
floatTwo(s.yAxis.zeroLine - val * s.yAxis.scaleMultiplier));
});
if(this.barOptions && this.barOptions.stacked) { s.datasets = this.data.datasets.map((d, i) => {
this.data.datasets.map((d, i) => { let values = d.values;
d.cumulativePositions = d.cumulativeYs.map(val => let cumulativeYs = d.cumulativeYs || [];
floatTwo(s.yAxis.zeroLine - val * s.yAxis.scaleMultiplier)); return {
}); name: d.name,
} index: i,
chartType: d.chartType,
values: values,
yPositions: scaleAll(values),
cumulativeYs: cumulativeYs,
cumulativeYPos: scaleAll(cumulativeYs),
};
});
} }
calcYMaximums() { calcYExtremes() {
let s = this.state; let s = this.state;
if(this.barOptions && this.barOptions.stacked) { if(this.barOptions && this.barOptions.stacked) {
s.yExtremes = this.data.datasets[this.data.datasets.length - 1].cumulativePositions; s.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPos;
return; return;
} }
s.yExtremes = new Array(s.datasetLength).fill(9999); s.yExtremes = new Array(s.datasetLength).fill(9999);
this.data.datasets.map((d, i) => { s.datasets.map((d, i) => {
d.positions.map((pos, j) => { d.yPositions.map((pos, j) => {
if(pos < s.yExtremes[j]) { if(pos < s.yExtremes[j]) {
s.yExtremes[j] = pos; s.yExtremes[j] = pos;
} }
}); });
}); });
// Tooltip refresh should not be needed?
// this.chartWrapper.removeChild(this.tip.container);
// this.make_tooltip();
} }
calcYRegions() { calcYRegions() {
let s = this.state; let s = this.state;
if(this.data.yMarkers) { if(this.data.yMarkers) {
this.state.yMarkers = this.data.yMarkers.map(d => { this.state.yMarkers = this.data.yMarkers.map(d => {
d.position = floatTwo(s.yAxis.zeroLine - d.value * s.yAxis.scaleMultiplier); d.position = scale(d.value, s.yAxis);
d.label += ': ' + d.value; d.label += ': ' + d.value;
return d; return d;
}); });
} }
if(this.data.yRegions) { if(this.data.yRegions) {
this.state.yRegions = this.data.yRegions.map(d => { this.state.yRegions = this.data.yRegions.map(d => {
if(d.end < d.start) { d.start = scale(d.start, s.yAxis);
[d.start, d.end] = [d.end, start]; d.end = scale(d.end, s.yAxis);
}
d.start = floatTwo(s.yAxis.zeroLine - d.start * s.yAxis.scaleMultiplier);
d.end = floatTwo(s.yAxis.zeroLine - d.end * s.yAxis.scaleMultiplier);
return d; return d;
}); });
} }
@ -1982,6 +1891,9 @@ class AxisChart extends BaseChart {
} }
initComponents() { initComponents() {
let s = this.state;
// console.log('this.state', Object.assign({}, this.state));
// console.log('this.state', this.state);
this.componentConfigs = [ this.componentConfigs = [
[ [
'yAxis', 'yAxis',
@ -2008,7 +1920,57 @@ class AxisChart extends BaseChart {
pos: 'right' pos: 'right'
} }
], ],
];
this.componentConfigs.map(args => {
args.push(
function() {
return this.state[args[0]];
}.bind(this)
);
});
let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
// console.log('barDatasets', barDatasets, this.state.datasets);
// Bars
let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;
let barsWidth = s.unitWidth * (1 - spaceRatio);
let barWidth = barsWidth/(this.barOptions.stacked ? 1 : barDatasets.length);
let barsConfigs = barDatasets.map(d => {
let index = d.index;
return [
'barGraph',
{
index: index,
color: this.colors[index],
// same for all datasets
valuesOverPoints: this.valuesOverPoints,
minHeight: this.height * MIN_BAR_PERCENT_HEIGHT,
barsWidth: barsWidth,
barWidth: barWidth,
zeroLine: s.yAxis.zeroLine
},
function() {
let s = this.state;
let d = s.datasets[index];
return {
xPositions: s.xAxis.positions,
yPositions: d.yPositions,
cumulativeYPos: d.cumulativeYPos,
values: d.values,
cumulativeYs: d.cumulativeYs
};
}.bind(this)
];
});
let markerConfigs = [
[ [
'yMarkers', 'yMarkers',
{ {
@ -2017,17 +1979,23 @@ class AxisChart extends BaseChart {
} }
] ]
]; ];
markerConfigs.map(args => {
args.push(
function() {
return this.state[args[0]];
}.bind(this)
);
});
this.componentConfigs = this.componentConfigs.concat(barsConfigs, markerConfigs);
} }
setupComponents() { setupComponents() {
let optionals = ['yMarkers', 'yRegions']; let optionals = ['yMarkers', 'yRegions'];
this.components = new Map(this.componentConfigs this.components = new Map(this.componentConfigs
.filter(args => !optionals.includes(args[0]) || this.data[args[0]]) .filter(args => !optionals.includes(args[0]) || this.state[args[0]] || args[0] === 'barGraph')
.map(args => { .map(args => {
args.push(
function() {
return this.state[args[0]];
}.bind(this)
);
return [args[0], getComponent(...args)]; return [args[0], getComponent(...args)];
})); }));
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -177,7 +177,7 @@ let type_data = {
{ {
name: "Yet Another", name: "Yet Another",
values: [15, 20, -3, -15, 58, 12, -17, 37], values: [15, 20, -3, -15, 58, 12, -17, 37],
chartType: 'line' chartType: 'bar'
} }
// temp : Stacked // temp : Stacked

View File

@ -1,16 +1,15 @@
import BaseChart from './BaseChart'; import BaseChart from './BaseChart';
import { dataPrep } from './axis-chart-utils'; import { dataPrep, zeroDataPrep } from './axis-chart-utils';
import { Y_AXIS_MARGIN } from '../utils/constants'; import { Y_AXIS_MARGIN } from '../utils/constants';
import { getComponent } from '../objects/ChartComponents'; import { getComponent } from '../objects/ChartComponents';
import { BarChartController, LineChartController, getPaths } from '../objects/AxisChartControllers';
import { AxisChartRenderer } from '../utils/draw'; import { AxisChartRenderer } from '../utils/draw';
import { getOffset, fire } from '../utils/dom'; import { getOffset, fire } from '../utils/dom';
import { equilizeNoOfElements } from '../utils/draw-utils'; import { equilizeNoOfElements } from '../utils/draw-utils';
import { Animator, translateHoriLine } from '../utils/animate'; import { Animator, translateHoriLine } from '../utils/animate';
import { runSMILAnimation } from '../utils/animation'; import { runSMILAnimation } from '../utils/animation';
import { getRealIntervals, calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex } from '../utils/intervals'; import { getRealIntervals, calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale } from '../utils/intervals';
import { floatTwo, fillArray, bindChange } from '../utils/helpers'; import { floatTwo, fillArray, bindChange } from '../utils/helpers';
import { MIN_BAR_PERCENT_HEIGHT, DEFAULT_AXIS_CHART_TYPE } from '../utils/constants'; import { MIN_BAR_PERCENT_HEIGHT, DEFAULT_AXIS_CHART_TYPE, BAR_CHART_SPACE_RATIO } from '../utils/constants';
export default class AxisChart extends BaseChart { export default class AxisChart extends BaseChart {
constructor(args) { constructor(args) {
@ -19,19 +18,19 @@ export default class AxisChart extends BaseChart {
this.valuesOverPoints = args.valuesOverPoints; this.valuesOverPoints = args.valuesOverPoints;
this.formatTooltipY = args.formatTooltipY; this.formatTooltipY = args.formatTooltipY;
this.formatTooltipX = args.formatTooltipX; this.formatTooltipX = args.formatTooltipX;
this.barOptions = args.barOptions; this.barOptions = args.barOptions || {};
this.lineOptions = args.lineOptions; this.lineOptions = args.lineOptions || {};
this.type = args.type || 'line'; this.type = args.type || 'line';
this.xAxisMode = args.xAxisMode || 'span'; this.xAxisMode = args.xAxisMode || 'span';
this.yAxisMode = args.yAxisMode || 'span'; this.yAxisMode = args.yAxisMode || 'span';
this.zeroLine = this.height; this.zeroLine = this.height;
this.setTrivialState(); // this.setTrivialState();
this.setup(); this.setup();
} }
configure(args) { configure(args) {3
super.configure(); super.configure();
// TODO: set in options and use // TODO: set in options and use
@ -40,38 +39,6 @@ export default class AxisChart extends BaseChart {
this.config.yAxisMode = args.yAxisMode; this.config.yAxisMode = args.yAxisMode;
} }
setTrivialState() {
// Define data and stuff
let yTempPos = getRealIntervals(this.height, 4, 0, 0);
this.state = {
xAxis: {
positions: [],
labels: [],
},
yAxis: {
positions: yTempPos,
labels: yTempPos.map(d => ""),
},
yRegions: [
{
start: this.height,
end: this.height,
label: ''
}
],
yMarkers: [
{
position: this.height,
label: ''
}
]
}
this.calcWidth();
this.calcXPositions(this.state);
}
setMargins() { setMargins() {
super.setMargins(); super.setMargins();
this.translateXLeft = Y_AXIS_MARGIN; this.translateXLeft = Y_AXIS_MARGIN;
@ -82,10 +49,12 @@ export default class AxisChart extends BaseChart {
return dataPrep(data, this.type); return dataPrep(data, this.type);
} }
prepareFirstData(data=this.data) {
return zeroDataPrep(data);
}
calc() { calc() {
this.calcXPositions(); this.calcXPositions();
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); this.calcYAxisParameters(this.getAllYValues(), this.type === 'line');
} }
@ -119,61 +88,60 @@ export default class AxisChart extends BaseChart {
} }
this.calcYUnits(); this.calcYUnits();
this.calcYMaximums(); this.calcYExtremes();
this.calcYRegions(); this.calcYRegions();
} }
calcYUnits() { calcYUnits() {
let s = this.state; let s = this.state;
this.data.datasets.map(d => { let scaleAll = values => values.map(val => scale(val, s.yAxis));
d.positions = d.values.map(val =>
floatTwo(s.yAxis.zeroLine - val * s.yAxis.scaleMultiplier));
});
if(this.barOptions && this.barOptions.stacked) { s.datasets = this.data.datasets.map((d, i) => {
this.data.datasets.map((d, i) => { let values = d.values;
d.cumulativePositions = d.cumulativeYs.map(val => let cumulativeYs = d.cumulativeYs || [];
floatTwo(s.yAxis.zeroLine - val * s.yAxis.scaleMultiplier)); return {
}); name: d.name,
} index: i,
chartType: d.chartType,
values: values,
yPositions: scaleAll(values),
cumulativeYs: cumulativeYs,
cumulativeYPos: scaleAll(cumulativeYs),
};
});
} }
calcYMaximums() { calcYExtremes() {
let s = this.state; let s = this.state;
if(this.barOptions && this.barOptions.stacked) { if(this.barOptions && this.barOptions.stacked) {
s.yExtremes = this.data.datasets[this.data.datasets.length - 1].cumulativePositions; s.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPos;
return; return;
} }
s.yExtremes = new Array(s.datasetLength).fill(9999); s.yExtremes = new Array(s.datasetLength).fill(9999);
this.data.datasets.map((d, i) => { s.datasets.map((d, i) => {
d.positions.map((pos, j) => { d.yPositions.map((pos, j) => {
if(pos < s.yExtremes[j]) { if(pos < s.yExtremes[j]) {
s.yExtremes[j] = pos; s.yExtremes[j] = pos;
} }
}); });
}); });
// Tooltip refresh should not be needed?
// this.chartWrapper.removeChild(this.tip.container);
// this.make_tooltip();
} }
calcYRegions() { calcYRegions() {
let s = this.state; let s = this.state;
if(this.data.yMarkers) { if(this.data.yMarkers) {
this.state.yMarkers = this.data.yMarkers.map(d => { this.state.yMarkers = this.data.yMarkers.map(d => {
d.position = floatTwo(s.yAxis.zeroLine - d.value * s.yAxis.scaleMultiplier); d.position = scale(d.value, s.yAxis);
d.label += ': ' + d.value; d.label += ': ' + d.value;
return d; return d;
}); });
} }
if(this.data.yRegions) { if(this.data.yRegions) {
this.state.yRegions = this.data.yRegions.map(d => { this.state.yRegions = this.data.yRegions.map(d => {
if(d.end < d.start) { d.start = scale(d.start, s.yAxis);
[d.start, d.end] = [d.end, start]; d.end = scale(d.end, s.yAxis);
}
d.start = floatTwo(s.yAxis.zeroLine - d.start * s.yAxis.scaleMultiplier);
d.end = floatTwo(s.yAxis.zeroLine - d.end * s.yAxis.scaleMultiplier);
return d; return d;
}); });
} }
@ -196,6 +164,9 @@ export default class AxisChart extends BaseChart {
} }
initComponents() { initComponents() {
let s = this.state;
// console.log('this.state', Object.assign({}, this.state));
// console.log('this.state', this.state);
this.componentConfigs = [ this.componentConfigs = [
[ [
'yAxis', 'yAxis',
@ -222,7 +193,57 @@ export default class AxisChart extends BaseChart {
pos: 'right' pos: 'right'
} }
], ],
];
this.componentConfigs.map(args => {
args.push(
function() {
return this.state[args[0]];
}.bind(this)
);
});
let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
// console.log('barDatasets', barDatasets, this.state.datasets);
// Bars
let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;
let barsWidth = s.unitWidth * (1 - spaceRatio);
let barWidth = barsWidth/(this.barOptions.stacked ? 1 : barDatasets.length);
let barsConfigs = barDatasets.map(d => {
let index = d.index;
return [
'barGraph',
{
index: index,
color: this.colors[index],
// same for all datasets
valuesOverPoints: this.valuesOverPoints,
minHeight: this.height * MIN_BAR_PERCENT_HEIGHT,
barsWidth: barsWidth,
barWidth: barWidth,
zeroLine: s.yAxis.zeroLine
},
function() {
let s = this.state;
let d = s.datasets[index];
return {
xPositions: s.xAxis.positions,
yPositions: d.yPositions,
cumulativeYPos: d.cumulativeYPos,
values: d.values,
cumulativeYs: d.cumulativeYs
};
}.bind(this)
];
});
let markerConfigs = [
[ [
'yMarkers', 'yMarkers',
{ {
@ -231,17 +252,23 @@ export default class AxisChart extends BaseChart {
} }
] ]
]; ];
markerConfigs.map(args => {
args.push(
function() {
return this.state[args[0]];
}.bind(this)
);
});
this.componentConfigs = this.componentConfigs.concat(barsConfigs, markerConfigs);
} }
setupComponents() { setupComponents() {
let optionals = ['yMarkers', 'yRegions']; let optionals = ['yMarkers', 'yRegions'];
this.components = new Map(this.componentConfigs this.components = new Map(this.componentConfigs
.filter(args => !optionals.includes(args[0]) || this.data[args[0]]) .filter(args => !optionals.includes(args[0]) || this.state[args[0]] || args[0] === 'barGraph')
.map(args => { .map(args => {
args.push(
function() {
return this.state[args[0]];
}.bind(this)
);
return [args[0], getComponent(...args)]; return [args[0], getComponent(...args)];
})); }));
} }

View File

@ -28,13 +28,15 @@ export default class BaseChart {
this.title = title; this.title = title;
this.subtitle = subtitle; this.subtitle = subtitle;
this.argHeight = height; this.argHeight = height;
this.type = type;
this.isNavigable = isNavigable; this.isNavigable = isNavigable;
if(this.isNavigable) { if(this.isNavigable) {
this.currentIndex = 0; this.currentIndex = 0;
} }
this.data = this.prepareData(data);; this.realData = this.prepareData(data);
this.data = this.prepareFirstData(this.realData);
this.colors = []; this.colors = [];
this.config = {}; this.config = {};
this.state = {}; this.state = {};
@ -148,6 +150,7 @@ export default class BaseChart {
this.calcWidth(); this.calcWidth();
this.makeChartArea(); this.makeChartArea();
this.calc();
this.initComponents(); // Only depend on the drawArea made in makeChartArea this.initComponents(); // Only depend on the drawArea made in makeChartArea
this.setupComponents(); this.setupComponents();
@ -160,6 +163,7 @@ export default class BaseChart {
// TODO: remove timeout and decrease post animate time in chart component // TODO: remove timeout and decrease post animate time in chart component
if(init) { if(init) {
this.data = this.realData;
setTimeout(() => {this.update();}, 1000); setTimeout(() => {this.update();}, 1000);
} }
} }
@ -187,10 +191,10 @@ export default class BaseChart {
calc() {} // builds state calc() {} // builds state
render(animate=true) { render(components=this.components, animate=true) {
// Can decouple to this.refreshComponents() first to save animation timeout // Can decouple to this.refreshComponents() first to save animation timeout
let elementsToAnimate = []; let elementsToAnimate = [];
this.components.forEach(c => { components.forEach(c => {
elementsToAnimate = elementsToAnimate.concat(c.update(animate)); elementsToAnimate = elementsToAnimate.concat(c.update(animate));
}); });
if(elementsToAnimate.length > 0) { if(elementsToAnimate.length > 0) {

View File

@ -1,5 +1,5 @@
import { floatTwo, fillArray } from '../utils/helpers'; import { floatTwo, fillArray } from '../utils/helpers';
import { DEFAULT_AXIS_CHART_TYPE } from '../utils/constants'; import { DEFAULT_AXIS_CHART_TYPE, AXIS_DATASET_CHART_TYPES } from '../utils/constants';
export function dataPrep(data, type) { export function dataPrep(data, type) {
data.labels = data.labels || []; data.labels = data.labels || [];
@ -36,19 +36,56 @@ export function dataPrep(data, type) {
// Set labels // Set labels
// //
// Set index
d.index = i;
// Set type // Set type
if(!d.chartType ) { if(!d.chartType ) {
d.chartType = type || DEFAULT_AXIS_CHART_TYPE; if(!AXIS_DATASET_CHART_TYPES.includes(type)) type === DEFAULT_AXIS_CHART_TYPE;
d.chartType = type;
} }
}); });
// Markers // Markers
// Regions // Regions
// Set start and end // data.yRegions = data.yRegions || [];
if(data.yRegions) {
data.yRegions.map(d => {
if(d.end < d.start) {
[d.start, d.end] = [d.end, start];
}
});
}
return data; return data;
}
export function zeroDataPrep(realData) {
let datasetLength = realData.labels.length;
let zeroArray = new Array(datasetLength).fill(0);
let zeroData = {
labels: realData.labels,
datasets: realData.datasets.map(d => {
return {
name: '',
values: zeroArray,
chartType: d.chartType
}
}),
yRegions: [
{
start: 0,
end: 0,
label: ''
}
],
yMarkers: [
{
value: 0,
label: ''
}
]
};
return zeroData;
} }

View File

@ -67,36 +67,6 @@ export class BarChartController extends AxisChartController {
? m.options.stacked : m.noOfDatasets); ? m.options.stacked : m.noOfDatasets);
} }
draw(x, yTop, color, label='', index=0, offset=0) {
let [height, y] = getBarHeightAndYAttr(yTop, this.meta.zeroLine);
let rect = createSVG('rect', {
className: `bar mini`,
style: `fill: ${color}`,
'data-point-index': index,
x: x - this.consts.barsWidth/2,
y: y - offset,
width: this.consts.width,
height: height || this.consts.minHeight
});
if(!label && !label.length) {
return rect;
} else {
let text = createSVG('text', {
className: 'data-point-value',
x: x,
y: y - offset,
dy: (FONT_SIZE / 2 * -1) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
});
return wrapInSVGGroup([rect, text]);
}
}
animate(bar, x, yTop, index, noOfDatasets) { animate(bar, x, yTop, index, noOfDatasets) {
let start = x - this.meta.unitWidth/4; let start = x - this.meta.unitWidth/4;
let width = (this.meta.unitWidth/2)/noOfDatasets; let width = (this.meta.unitWidth/2)/noOfDatasets;

View File

@ -1,5 +1,5 @@
import { makeSVGGroup } from '../utils/draw'; import { makeSVGGroup } from '../utils/draw';
import { xLine, yLine, yMarker, yRegion } from '../utils/draw'; import { xLine, yLine, yMarker, yRegion, datasetBar } from '../utils/draw';
import { equilizeNoOfElements } from '../utils/draw-utils'; import { equilizeNoOfElements } from '../utils/draw-utils';
import { Animator, translateHoriLine, translateVertLine, animateRegion } from '../utils/animate'; import { Animator, translateHoriLine, translateVertLine, animateRegion } from '../utils/animate';
@ -25,8 +25,9 @@ class ChartComponent {
this.store = []; this.store = [];
this.layerClass = typeof(layerClass) === 'function' this.layerClass = layerClass;
? layerClass() : layerClass; this.layerClass = typeof(this.layerClass) === 'function'
? this.layerClass() : this.layerClass;
this.refresh(); this.refresh();
} }
@ -204,81 +205,61 @@ let componentConfigs = {
}, },
barGraph: { barGraph: {
// opt:[ layerClass: function() { return 'dataset-units dataset-' + this.constants.index; },
// 'barGraph',
// this.drawArea,
// {
// controller: barController,
// index: index,
// color: this.colors[index],
// valuesOverPoints: this.valuesOverPoints,
// stacked: this.barOptions && this.barOptions.stacked,
// spaceRatio: 0.5,
// minHeight: this.height * MIN_BAR_PERCENT_HEIGHT
// },
// {
// barsWidth: this.state.unitWidth * (1 - spaceRatio),
// barWidth: barsWidth/(stacked ? 1 : this.state.noOfDatasets),
// },
// function() {
// let s = this.state;
// return {
// barsWidth: this.state.unitWidth * (1 - spaceRatio),
// barWidth: barsWidth/(stacked ? 1 : this.state.noOfDatasets),
// positions: s.xAxisPositions,
// labels: s.xAxisLabels,
// }
// }.bind(this)
// ],
layerClass() { return 'y-regions' + this.constants.index; },
makeElements(data) { makeElements(data) {
let c = this.constants; let c = this.constants;
return data.yPositions.map((y, j) => return data.yPositions.map((y, j) => {
barController.draw( // console.log(data.cumulativeYPos, data.cumulativeYPos[j]);
return datasetBar(
data.xPositions[j], data.xPositions[j],
y, y,
color, c.barWidth,
c.color,
(c.valuesOverPoints ? (c.stacked ? data.cumulativeYs[j] : data.values[j]) : ''), (c.valuesOverPoints ? (c.stacked ? data.cumulativeYs[j] : data.values[j]) : ''),
j, j,
y - (data.cumulativePositions ? data.cumulativePositions[j] : y) y - (c.stacked ? data.cumulativeYPos[j] : y),
{
zeroLine: c.zeroLine,
barsWidth: c.barsWidth,
minHeight: c.minHeight
}
) )
); });
}, },
postMake() { postMake() {
if((!this.constants.stacked)) { if((!this.constants.stacked)) {
this.layer.setAttribute('transform', this.layer.setAttribute('transform',
`translate(${unitRenderer.consts.width * index}, 0)`); `translate(${this.constants.width * this.constants.index}, 0)`);
} }
}, },
animateElements(newData) { animateElements(newData) {
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); // [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
let newPos = newData.map(d => d.end); // let newPos = newData.map(d => d.end);
let newLabels = newData.map(d => d.label); // let newLabels = newData.map(d => d.label);
let newStarts = newData.map(d => d.start); // let newStarts = newData.map(d => d.start);
let oldPos = this.oldData.map(d => d.end); // let oldPos = this.oldData.map(d => d.end);
let oldLabels = this.oldData.map(d => d.label); // let oldLabels = this.oldData.map(d => d.label);
let oldStarts = this.oldData.map(d => d.start); // let oldStarts = this.oldData.map(d => d.start);
this.render(oldPos.map((pos, i) => { // this.render(oldPos.map((pos, i) => {
return { // return {
start: oldStarts[i], // start: oldStarts[i],
end: oldPos[i], // end: oldPos[i],
label: newLabels[i] // label: newLabels[i]
} // }
})); // }));
let animateElements = []; // let animateElements = [];
this.store.map((rectGroup, i) => { // this.store.map((rectGroup, i) => {
animateElements = animateElements.concat(animateRegion( // animateElements = animateElements.concat(animateRegion(
rectGroup, newStarts[i], newPos[i], oldPos[i] // rectGroup, newStarts[i], newPos[i], oldPos[i]
)); // ));
}); // });
return animateElements; // return animateElements;
} }
}, },

View File

@ -1,4 +1,6 @@
export const Y_AXIS_MARGIN = 60; export const Y_AXIS_MARGIN = 60;
export const MIN_BAR_PERCENT_HEIGHT = 0.01; export const MIN_BAR_PERCENT_HEIGHT = 0.01;
export const DEFAULT_AXIS_CHART_TYPE = 'line'; export const DEFAULT_AXIS_CHART_TYPE = 'line';
export const AXIS_DATASET_CHART_TYPES = ['line', 'bar'];
export const BAR_CHART_SPACE_RATIO = 0.5;

View File

@ -2,15 +2,9 @@ import { fillArray } from './helpers';
export function getBarHeightAndYAttr(yTop, zeroLine) { export function getBarHeightAndYAttr(yTop, zeroLine) {
let height, y; let height, y;
if (yTop <= zeroLine) { if (yTop < zeroLine) {
height = zeroLine - yTop; height = zeroLine - yTop;
y = yTop; y = yTop;
// In case of invisible bars
if(height === 0) {
height = totalHeight * MIN_BAR_PERCENT_HEIGHT;
y -= height;
}
} else { } else {
height = yTop - zeroLine; height = yTop - zeroLine;
y = zeroLine; y = zeroLine;

View File

@ -360,6 +360,37 @@ export function yRegion(y1, y2, width, label) {
return region; return region;
} }
export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
// console.log(yTop, meta.zeroLine, y, offset);
let rect = createSVG('rect', {
className: `bar mini`,
style: `fill: ${color}`,
'data-point-index': index,
x: x - meta.barsWidth/2,
y: y - offset,
width: width,
height: height || meta.minHeight
});
if(!label && !label.length) {
return rect;
} else {
let text = createSVG('text', {
className: 'data-point-value',
x: x,
y: y - offset,
dy: (FONT_SIZE / 2 * -1) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
});
return wrapInSVGGroup([rect, text]);
}
}
export class AxisChartRenderer { export class AxisChartRenderer {
constructor(state) { constructor(state) {
this.refreshState(state); this.refreshState(state);

View File

@ -1,3 +1,5 @@
import { floatTwo } from './helpers';
function normalize(x) { function normalize(x) {
// Calculates mantissa and exponent of a number // Calculates mantissa and exponent of a number
// Returns normalized number and exponent // Returns normalized number and exponent
@ -194,6 +196,10 @@ export function getValueRange(orderedArray) {
return orderedArray[orderedArray.length-1] - orderedArray[0]; return orderedArray[orderedArray.length-1] - orderedArray[0];
} }
export function scale(val, yAxis) {
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier)
}
export function calcDistribution(values, distributionSize) { export function calcDistribution(values, distributionSize) {
// Assume non-negative values, // Assume non-negative values,
// implying distribution minimum at zero // implying distribution minimum at zero