[animate] lines, cleanup

This commit is contained in:
Prateeksha Singh 2018-03-02 22:43:40 +05:30
parent dc1fa4d373
commit 02a1c4c5cf
20 changed files with 466 additions and 611 deletions

View File

@ -279,7 +279,7 @@ function equilizeNoOfElements(array1, array2,
// } // }
const UNIT_ANIM_DUR = 350; const UNIT_ANIM_DUR = 350;
const PATH_ANIM_DUR = 350;
const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR; const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;
const REPLACE_ALL_NEW_DUR = 250; const REPLACE_ALL_NEW_DUR = 250;
@ -331,8 +331,8 @@ function animateBar(bar, x, yTop, width, index=0, meta={}) {
STD_EASING STD_EASING
]; ];
let old = bar.getAttribute("transform").split("(")[1].slice(0, -1); let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(bar, old, [x, y], MARKER_LINE_ANIM_DUR); let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [rectAnim, groupAnim]; return [rectAnim, groupAnim];
} else { } else {
return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]]; return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]];
@ -340,21 +340,41 @@ function animateBar(bar, x, yTop, width, index=0, meta={}) {
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein); // bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
} }
/* function animateDot(dot, x, y) {
if(dot.nodeName !== 'circle') {
let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [groupAnim];
} else {
return [[dot, {cx: x, cy: y}, UNIT_ANIM_DUR, STD_EASING]];
}
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}
<filter id="glow" x="-10%" y="-10%" width="120%" height="120%"> function animatePath(paths, newXList, newYList, zeroLine) {
<feGaussianBlur stdDeviation="0.5 0.5" result="glow"></feGaussianBlur> let pathComponents = [];
<feMerge>
<feMergeNode in="glow"></feMergeNode>
<feMergeNode in="glow"></feMergeNode>
<feMergeNode in="glow"></feMergeNode>
</feMerge>
</filter>
filter: url(#glow); let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y));
fill: #fff; let pathStr = pointsStr.join("L");
*/ const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath);
if(paths.region) {
let regStartPt = `${newXList[0]},${zeroLine}L`;
let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;
const animRegion = [
paths.region,
{d:"M" + regStartPt + pathStr + regEndPt},
PATH_ANIM_DUR,
STD_EASING
];
pathComponents.push(animRegion);
}
return pathComponents;
}
const AXIS_TICK_LENGTH = 6; const AXIS_TICK_LENGTH = 6;
const LABEL_MARGIN = 4; const LABEL_MARGIN = 4;
@ -397,6 +417,26 @@ 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,
@ -433,7 +473,20 @@ function makePath(pathStr, className='', stroke='none', fill='none') {
}); });
} }
function makeGradient(svgDefElem, color, lighter = false) {
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
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 = {
@ -695,6 +748,68 @@ function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={})
} }
} }
function datasetDot(x, y, radius, color, label='', index=0, meta={}) {
let dot = createSVG('circle', {
style: `fill: ${color}`,
'data-point-index': index,
cx: x,
cy: y,
r: radius
});
if(!label && !label.length) {
return dot;
} else {
dot.setAttribute('cy', 0);
dot.setAttribute('cx', 0);
let text = createSVG('text', {
className: 'data-point-value',
x: 0,
y: 0,
dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
});
let group = createSVG('g', {
transform: `translate(${x}, ${y})`
});
group.appendChild(dot);
group.appendChild(text);
return group;
}
}
function getPaths(xList, yList, color, options={}, meta={}) {
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(options.heatline) {
let gradient_id = makeGradient(meta.svgDefs, color);
path.style.stroke = `url(#${gradient_id})`;
}
let paths = {
path: path
};
// Region
if(options.regionFill) {
let gradient_id_region = makeGradient(meta.svgDefs, color, true);
// TODO: use zeroLine OR minimum
let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
}
return paths;
}
const PRESET_COLOR_MAP = { const PRESET_COLOR_MAP = {
'light-blue': '#7cd6fd', 'light-blue': '#7cd6fd',
'blue': '#5e64ff', 'blue': '#5e64ff',
@ -1047,17 +1162,16 @@ class BaseChart {
draw(init=false) { draw(init=false) {
this.calcWidth(); this.calcWidth();
this.makeChartArea();
this.calc(); this.calc();
this.initComponents(); // Only depend on the drawArea made in makeChartArea this.makeChartArea();
this.initComponents();
this.setupComponents(); this.setupComponents();
this.components.forEach(c => c.setup(this.drawArea)); // or c.build() this.components.forEach(c => c.setup(this.drawArea)); // or c.build()
this.components.forEach(c => c.make()); // or c.build() this.components.forEach(c => c.make()); // or c.build()
this.renderLegend();
this.renderLegend();
this.setupNavigation(init); this.setupNavigation(init);
// TODO: remove timeout and decrease post animate time in chart component // TODO: remove timeout and decrease post animate time in chart component
@ -1199,10 +1313,13 @@ 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 AXIS_DATASET_CHART_TYPES = ['line', 'bar'];
const BAR_CHART_SPACE_RATIO = 0.5; const BAR_CHART_SPACE_RATIO = 0.5;
const MIN_BAR_PERCENT_HEIGHT = 0.01;
const LINE_CHART_DOT_SIZE = 4;
function dataPrep(data, type) { function dataPrep(data, type) {
data.labels = data.labels || []; data.labels = data.labels || [];
@ -1520,7 +1637,6 @@ let componentConfigs = {
let newValues = newData.values; let newValues = newData.values;
let newCYs = newData.cumulativeYs; let newCYs = newData.cumulativeYs;
let oldXPos = this.oldData.xPositions; let oldXPos = this.oldData.xPositions;
let oldYPos = this.oldData.yPositions; let oldYPos = this.oldData.yPositions;
let oldCYPos = this.oldData.cumulativeYPos; let oldCYPos = this.oldData.cumulativeYPos;
@ -1560,7 +1676,77 @@ let componentConfigs = {
}, },
lineGraph: { lineGraph: {
layerClass: function() { return 'dataset-units dataset-' + this.constants.index; },
makeElements(data) {
let c = this.constants;
this.paths = getPaths(
data.xPositions,
data.yPositions,
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
},
{
svgDefs: c.svgDefs,
zeroLine: data.zeroLine
}
);
this.dots = [];
if(!c.hideDots) {
this.dots = data.yPositions.map((y, j) => {
return datasetDot(
data.xPositions[j],
y,
data.radius,
c.color,
(c.valuesOverPoints ? data.values[j] : ''),
j
)
});
}
return Object.values(this.paths).concat(this.dots);
},
animateElements(newData) {
let newXPos = newData.xPositions;
let newYPos = newData.yPositions;
let newValues = newData.values;
let oldXPos = this.oldData.xPositions;
let oldYPos = this.oldData.yPositions;
let oldValues = this.oldData.values;
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
[oldValues, newValues] = equilizeNoOfElements(oldValues, newValues);
this.render({
xPositions: oldXPos,
yPositions: oldYPos,
values: newValues,
zeroLine: this.oldData.zeroLine,
radius: this.oldData.radius,
});
let animateElements = [];
animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
if(this.dots.length) {
this.dots.map((dot, i) => {
animateElements = animateElements.concat(animateDot(
dot, newXPos[i], newYPos[i]));
});
}
return animateElements;
}
} }
}; };
@ -1840,6 +2026,10 @@ class AxisChart extends BaseChart {
// Default, as per bar, and mixed. Only line will be a special case // Default, as per bar, and mixed. Only line will be a special case
s.xOffset = s.unitWidth/2; s.xOffset = s.unitWidth/2;
// // For a pure Line Chart
// s.unitWidth = this.width/(s.datasetLength - 1);
// s.xOffset = 0;
s.xAxis = { s.xAxis = {
labels: labels, labels: labels,
positions: labels.map((d, i) => positions: labels.map((d, i) =>
@ -1979,8 +2169,6 @@ class AxisChart extends BaseChart {
// console.log('barDatasets', barDatasets, this.state.datasets); // console.log('barDatasets', barDatasets, this.state.datasets);
// Bars
let barsConfigs = barDatasets.map(d => { let barsConfigs = barDatasets.map(d => {
let index = d.index; let index = d.index;
return [ return [
@ -2019,6 +2207,38 @@ class AxisChart extends BaseChart {
]; ];
}); });
let lineConfigs = lineDatasets.map(d => {
let index = d.index;
return [
'lineGraph' + '-' + d.index,
{
index: index,
color: this.colors[index],
svgDefs: this.svgDefs,
heatline: this.lineOptions.heatline,
regionFill: this.lineOptions.regionFill,
hideDots: this.lineOptions.hideDots,
// same for all datasets
valuesOverPoints: this.valuesOverPoints,
},
function() {
let s = this.state;
let d = s.datasets[index];
return {
xPositions: s.xAxis.positions,
yPositions: d.yPositions,
values: d.values,
zeroLine: s.yAxis.zeroLine,
radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE,
};
}.bind(this)
];
});
let markerConfigs = [ let markerConfigs = [
[ [
'yMarkers', 'yMarkers',
@ -2037,7 +2257,7 @@ class AxisChart extends BaseChart {
); );
}); });
this.componentConfigs = this.componentConfigs.concat(barsConfigs, markerConfigs); this.componentConfigs = this.componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs);
} }
setupComponents() { setupComponents() {
@ -2049,130 +2269,6 @@ class AxisChart extends BaseChart {
})); }));
} }
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.xAxis.positions[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.xAxis.positions;
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.xAxis.positions,
color,
this.config.heatline,
this.config.regionFill
);
},
animate: (paths) => {
let newX = this.state.xAxis.positions;
let newY = this.data.datasets[index].positions;
let oldX = this.oldState.xAxis.positions;
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() { bindTooltip() {
// TODO: could be in tooltip itself, as it is a given functionality for its parent // TODO: could be in tooltip itself, as it is a given functionality for its parent
this.chartWrapper.addEventListener('mousemove', (e) => { this.chartWrapper.addEventListener('mousemove', (e) => {
@ -2277,72 +2373,6 @@ class AxisChart extends BaseChart {
// keep a binding at the end of chart // keep a binding at the end of chart
class LineChart extends AxisChart {
constructor(args) {
super(args);
this.type = 'line';
if(Object.getPrototypeOf(this) !== LineChart.prototype) {
return;
}
this.setup();
}
configure(args) {
super.configure(args);
this.config.xAxisMode = args.xAxisMode || 'span';
this.config.yAxisMode = args.yAxisMode || 'span';
this.config.dotRadius = args.dotRadius || 4;
this.config.heatline = args.heatline || 0;
this.config.regionFill = args.regionFill || 0;
this.config.showDots = args.showDots || 1;
}
configUnits() {
this.unitArgs = {
type: 'dot',
args: { radius: this.config.dotRadius }
};
}
// temp commented
setUnitWidthAndXOffset() {
this.state.unitWidth = this.width/(this.state.datasetLength - 1);
this.state.xOffset = 0;
}
}
class ScatterChart extends LineChart {
constructor(args) {
super(args);
this.type = 'scatter';
if(!args.dotRadius) {
this.dotRadius = 8;
} else {
this.dotRadius = args.dotRadius;
}
this.setup();
}
setup_values() {
super.setup_values();
this.unit_args = {
type: 'dot',
args: { radius: this.dotRadius }
};
}
make_paths() {}
make_path() {}
}
class MultiAxisChart extends AxisChart { class MultiAxisChart extends AxisChart {
constructor(args) { constructor(args) {
super(args); super(args);
@ -3123,7 +3153,6 @@ class Heatmap extends BaseChart {
const chartTypes = { const chartTypes = {
mixed: AxisChart, mixed: AxisChart,
multiaxis: MultiAxisChart, multiaxis: MultiAxisChart,
scatter: ScatterChart,
percentage: PercentageChart, percentage: PercentageChart,
heatmap: Heatmap, heatmap: Heatmap,
pie: PieChart pie: PieChart

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

@ -80,7 +80,7 @@ let line_composite_chart = new Chart ({
parent: c2, parent: c2,
data: line_composite_data, data: line_composite_data,
type: 'line', type: 'line',
options: { lineOptions: {
dotSize: 10 dotSize: 10
}, },
height: 180, height: 180,
@ -208,7 +208,7 @@ let type_chart = new Chart({
yAxisMode: 'span', yAxisMode: 'span',
valuesOverPoints: 1, valuesOverPoints: 1,
barOptions: { barOptions: {
// stacked: 1 stacked: 1
} }
// formatTooltipX: d => (d + '').toUpperCase(), // formatTooltipX: d => (d + '').toUpperCase(),
// formatTooltipY: d => d + ' pts' // formatTooltipY: d => d + ' pts'
@ -261,7 +261,7 @@ let plot_chart_args = {
colors: ['blue'], colors: ['blue'],
isSeries: 1, isSeries: 1,
lineOptions: { lineOptions: {
showDots: 0, hideDots: 1,
heatline: 1, heatline: 1,
}, },
xAxisMode: 'tick', xAxisMode: 'tick',
@ -286,7 +286,7 @@ Array.prototype.slice.call(
config = [0, 1, 0]; config = [0, 1, 0];
} }
plot_chart_args.showDots = config[0]; plot_chart_args.hideDots = config[0];
plot_chart_args.heatline = config[1]; plot_chart_args.heatline = config[1];
plot_chart_args.regionFill = config[2]; plot_chart_args.regionFill = config[2];
@ -339,7 +339,9 @@ let update_chart = new Chart({
height: 250, height: 250,
colors: ['red'], colors: ['red'],
isSeries: 1, isSeries: 1,
regionFill: 1 lineOptions: {
regionFill: 1
},
}); });
let chart_update_buttons = document.querySelector('.chart-update-buttons'); let chart_update_buttons = document.querySelector('.chart-update-buttons');

View File

@ -167,13 +167,14 @@
<div id="chart-trends" class="border"></div> <div id="chart-trends" class="border"></div>
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> <div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary" data-type="line">Line</button> <button type="button" class="btn btn-sm btn-secondary" data-type="line">Line</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="dots">Dots</button>
<button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button> <button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="region">Region</button> <button type="button" class="btn btn-sm btn-secondary" data-type="region">Region</button>
</div> </div>
<pre><code class="hljs javascript margin-vertical-px"> ... <pre><code class="hljs javascript margin-vertical-px"> ...
type: 'line', // Line Chart specific properties: type: 'line', // Line Chart specific properties:
showDots: 0, // Show data points on the line; default 1 hideDots: 1, // Hide data points on the line; default 0
heatline: 1, // Show a value-wise line gradient; default 0 heatline: 1, // Show a value-wise line gradient; default 0
regionFill: 1, // Fill the area under the graph; default 0 regionFill: 1, // Fill the area under the graph; default 0
...</code></pre> ...</code></pre>

View File

@ -1,6 +1,5 @@
import '../scss/charts.scss'; import '../scss/charts.scss';
import ScatterChart from './charts/ScatterChart';
import MultiAxisChart from './charts/MultiAxisChart'; import MultiAxisChart from './charts/MultiAxisChart';
import PercentageChart from './charts/PercentageChart'; import PercentageChart from './charts/PercentageChart';
import PieChart from './charts/PieChart'; import PieChart from './charts/PieChart';
@ -23,7 +22,6 @@ import AxisChart from './charts/AxisChart';
const chartTypes = { const chartTypes = {
mixed: AxisChart, mixed: AxisChart,
multiaxis: MultiAxisChart, multiaxis: MultiAxisChart,
scatter: ScatterChart,
percentage: PercentageChart, percentage: PercentageChart,
heatmap: Heatmap, heatmap: Heatmap,
pie: PieChart pie: PieChart

View File

@ -2,14 +2,10 @@ import BaseChart from './BaseChart';
import { dataPrep, zeroDataPrep } 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 { AxisChartRenderer } from '../utils/draw';
import { getOffset, fire } from '../utils/dom'; import { getOffset, fire } from '../utils/dom';
import { equilizeNoOfElements } from '../utils/draw-utils'; import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale } from '../utils/intervals';
import { Animator, translateHoriLine } from '../utils/animate'; import { floatTwo } from '../utils/helpers';
import { runSMILAnimation } from '../utils/animation'; import { MIN_BAR_PERCENT_HEIGHT, DEFAULT_AXIS_CHART_TYPE, BAR_CHART_SPACE_RATIO, LINE_CHART_DOT_SIZE } from '../utils/constants';
import { getRealIntervals, calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale } from '../utils/intervals';
import { floatTwo, fillArray, bindChange } from '../utils/helpers';
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) {
@ -66,6 +62,10 @@ export default class AxisChart extends BaseChart {
// Default, as per bar, and mixed. Only line will be a special case // Default, as per bar, and mixed. Only line will be a special case
s.xOffset = s.unitWidth/2; s.xOffset = s.unitWidth/2;
// // For a pure Line Chart
// s.unitWidth = this.width/(s.datasetLength - 1);
// s.xOffset = 0;
s.xAxis = { s.xAxis = {
labels: labels, labels: labels,
positions: labels.map((d, i) => positions: labels.map((d, i) =>
@ -208,8 +208,6 @@ export default class AxisChart extends BaseChart {
// console.log('barDatasets', barDatasets, this.state.datasets); // console.log('barDatasets', barDatasets, this.state.datasets);
// Bars
let barsConfigs = barDatasets.map(d => { let barsConfigs = barDatasets.map(d => {
let index = d.index; let index = d.index;
return [ return [
@ -248,6 +246,38 @@ export default class AxisChart extends BaseChart {
]; ];
}); });
let lineConfigs = lineDatasets.map(d => {
let index = d.index;
return [
'lineGraph' + '-' + d.index,
{
index: index,
color: this.colors[index],
svgDefs: this.svgDefs,
heatline: this.lineOptions.heatline,
regionFill: this.lineOptions.regionFill,
hideDots: this.lineOptions.hideDots,
// same for all datasets
valuesOverPoints: this.valuesOverPoints,
},
function() {
let s = this.state;
let d = s.datasets[index];
return {
xPositions: s.xAxis.positions,
yPositions: d.yPositions,
values: d.values,
zeroLine: s.yAxis.zeroLine,
radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE,
};
}.bind(this)
];
});
let markerConfigs = [ let markerConfigs = [
[ [
'yMarkers', 'yMarkers',
@ -266,7 +296,7 @@ export default class AxisChart extends BaseChart {
); );
}); });
this.componentConfigs = this.componentConfigs.concat(barsConfigs, markerConfigs); this.componentConfigs = this.componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs);
} }
setupComponents() { setupComponents() {
@ -278,130 +308,6 @@ export default class AxisChart extends BaseChart {
})); }));
} }
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.xAxis.positions[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.xAxis.positions;
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.xAxis.positions,
color,
this.config.heatline,
this.config.regionFill
);
},
animate: (paths) => {
let newX = this.state.xAxis.positions;
let newY = this.data.datasets[index].positions;
let oldX = this.oldState.xAxis.positions;
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() { bindTooltip() {
// TODO: could be in tooltip itself, as it is a given functionality for its parent // TODO: could be in tooltip itself, as it is a given functionality for its parent
this.chartWrapper.addEventListener('mousemove', (e) => { this.chartWrapper.addEventListener('mousemove', (e) => {

View File

@ -148,17 +148,16 @@ export default class BaseChart {
draw(init=false) { draw(init=false) {
this.calcWidth(); this.calcWidth();
this.makeChartArea();
this.calc(); this.calc();
this.initComponents(); // Only depend on the drawArea made in makeChartArea this.makeChartArea();
this.initComponents();
this.setupComponents(); this.setupComponents();
this.components.forEach(c => c.setup(this.drawArea)); // or c.build() this.components.forEach(c => c.setup(this.drawArea)); // or c.build()
this.components.forEach(c => c.make()); // or c.build() this.components.forEach(c => c.make()); // or c.build()
this.renderLegend();
this.renderLegend();
this.setupNavigation(init); this.setupNavigation(init);
// TODO: remove timeout and decrease post animate time in chart component // TODO: remove timeout and decrease post animate time in chart component

View File

@ -1,43 +0,0 @@
import AxisChart from './AxisChart';
// import { ChartComponent } from '../objects/ChartComponents';
import { makeSVGGroup, makePath, makeGradient } from '../utils/draw';
import { equilizeNoOfElements } from '../utils/draw-utils';
export default class LineChart extends AxisChart {
constructor(args) {
super(args);
this.type = 'line';
if(Object.getPrototypeOf(this) !== LineChart.prototype) {
return;
}
this.setup();
}
configure(args) {
super.configure(args);
this.config.xAxisMode = args.xAxisMode || 'span';
this.config.yAxisMode = args.yAxisMode || 'span';
this.config.dotRadius = args.dotRadius || 4;
this.config.heatline = args.heatline || 0;
this.config.regionFill = args.regionFill || 0;
this.config.showDots = args.showDots || 1;
}
configUnits() {
this.unitArgs = {
type: 'dot',
args: { radius: this.config.dotRadius }
};
}
// temp commented
setUnitWidthAndXOffset() {
this.state.unitWidth = this.width/(this.state.datasetLength - 1);
this.state.xOffset = 0;
}
}

View File

@ -1,28 +0,0 @@
import LineChart from './LineChart';
export default class ScatterChart extends LineChart {
constructor(args) {
super(args);
this.type = 'scatter';
if(!args.dotRadius) {
this.dotRadius = 8;
} else {
this.dotRadius = args.dotRadius;
}
this.setup();
}
setup_values() {
super.setup_values();
this.unit_args = {
type: 'dot',
args: { radius: this.dotRadius }
};
}
make_paths() {}
make_path() {}
}

View File

@ -55,63 +55,9 @@ export class LineChartController extends AxisChartController {
}; };
} }
draw(x, y, color, label='', index=0) {
let dot = createSVG('circle', {
style: `fill: ${color}`,
'data-point-index': index,
cx: x,
cy: y,
r: this.consts.radius
});
if(!label && !label.length) {
return dot;
} else {
let text = createSVG('text', {
className: 'data-point-value',
x: x,
y: y,
dy: (FONT_SIZE / 2 * -1 - this.consts.radius) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
});
return wrapInSVGGroup([dot, text]);
}
}
animate(dot, x, yTop) {
return [dot, {cx: x, cy: yTop}, UNIT_ANIM_DUR, STD_EASING];
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}
} }
export 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 { // class BarChart extends AxisChart {
// constructor(args) { // constructor(args) {

View File

@ -1,7 +1,7 @@
import { makeSVGGroup } from '../utils/draw'; import { makeSVGGroup } from '../utils/draw';
import { xLine, yLine, yMarker, yRegion, datasetBar } from '../utils/draw'; import { xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, getPaths } from '../utils/draw';
import { equilizeNoOfElements } from '../utils/draw-utils'; import { equilizeNoOfElements } from '../utils/draw-utils';
import { Animator, translateHoriLine, translateVertLine, animateRegion, animateBar } from '../utils/animate'; import { translateHoriLine, translateVertLine, animateRegion, animateBar, animateDot, animatePath } from '../utils/animate';
class ChartComponent { class ChartComponent {
constructor({ constructor({
@ -230,7 +230,6 @@ let componentConfigs = {
let newValues = newData.values; let newValues = newData.values;
let newCYs = newData.cumulativeYs; let newCYs = newData.cumulativeYs;
let oldXPos = this.oldData.xPositions; let oldXPos = this.oldData.xPositions;
let oldYPos = this.oldData.yPositions; let oldYPos = this.oldData.yPositions;
let oldCYPos = this.oldData.cumulativeYPos; let oldCYPos = this.oldData.cumulativeYPos;
@ -270,7 +269,79 @@ let componentConfigs = {
}, },
lineGraph: { lineGraph: {
layerClass: function() { return 'dataset-units dataset-' + this.constants.index; },
makeElements(data) {
let c = this.constants;
this.paths = getPaths(
data.xPositions,
data.yPositions,
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
},
{
svgDefs: c.svgDefs,
zeroLine: data.zeroLine
}
)
this.dots = []
if(!c.hideDots) {
this.dots = data.yPositions.map((y, j) => {
return datasetDot(
data.xPositions[j],
y,
data.radius,
c.color,
(c.valuesOverPoints ? data.values[j] : ''),
j
)
});
}
return Object.values(this.paths).concat(this.dots);
},
animateElements(newData) {
let c = this.constants;
let newXPos = newData.xPositions;
let newYPos = newData.yPositions;
let newValues = newData.values;
let oldXPos = this.oldData.xPositions;
let oldYPos = this.oldData.yPositions;
let oldValues = this.oldData.values;
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
[oldValues, newValues] = equilizeNoOfElements(oldValues, newValues);
this.render({
xPositions: oldXPos,
yPositions: oldYPos,
values: newValues,
zeroLine: this.oldData.zeroLine,
radius: this.oldData.radius,
});
let animateElements = [];
animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
if(this.dots.length) {
this.dots.map((dot, i) => {
animateElements = animateElements.concat(animateDot(
dot, newXPos[i], newYPos[i]));
});
}
return animateElements;
}
} }
} }

View File

@ -53,8 +53,8 @@ export function animateBar(bar, x, yTop, width, index=0, meta={}) {
STD_EASING STD_EASING
] ]
let old = bar.getAttribute("transform").split("(")[1].slice(0, -1); let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(bar, old, [x, y], MARKER_LINE_ANIM_DUR); let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [rectAnim, groupAnim]; return [rectAnim, groupAnim];
} else { } else {
return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]]; return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]];
@ -62,57 +62,39 @@ export function animateBar(bar, x, yTop, width, index=0, meta={}) {
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein); // bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
} }
export var Animator = (function() { export function animateDot(dot, x, y) {
var Animator = function(totalHeight, totalWidth, zeroLine, avgUnitWidth) { if(dot.nodeName !== 'circle') {
// constants let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1);
this.totalHeight = totalHeight; let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
this.totalWidth = totalWidth; return [groupAnim];
} else {
return [[dot, {cx: x, cy: y}, UNIT_ANIM_DUR, STD_EASING]];
}
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}
// changeables export function animatePath(paths, newXList, newYList, zeroLine) {
this.avgUnitWidth = avgUnitWidth; let pathComponents = [];
this.zeroLine = zeroLine;
};
Animator.prototype = { let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y));
bar: function(barObj, x, yTop, index, noOfDatasets) { let pathStr = pointsStr.join("L");
let start = x - this.avgUnitWidth/4;
let width = (this.avgUnitWidth/2)/noOfDatasets;
let [height, y] = getBarHeightAndYAttr(yTop, this.zeroLine, this.totalHeight);
x = start + (width * index); const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath);
return [barObj, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]; if(paths.region) {
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein); let regStartPt = `${newXList[0]},${zeroLine}L`;
}, let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;
dot: function(dotObj, x, yTop) { const animRegion = [
return [dotObj, {cx: x, cy: yTop}, UNIT_ANIM_DUR, STD_EASING]; paths.region,
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein); {d:"M" + regStartPt + pathStr + regEndPt},
}, PATH_ANIM_DUR,
STD_EASING
path: function(d, pathStr) { ];
let pathComponents = []; pathComponents.push(animRegion);
const animPath = [{unit: d.path, object: d, key: 'path'}, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING]; }
pathComponents.push(animPath);
if(d.regionPath) {
let regStartPt = `0,${this.zeroLine}L`;
let regEndPt = `L${this.totalWidth}, ${this.zeroLine}`;
const animRegion = [
{unit: d.regionPath, object: d, key: 'regionPath'},
{d:"M" + regStartPt + pathStr + regEndPt},
PATH_ANIM_DUR,
STD_EASING
];
pathComponents.push(animRegion);
}
return pathComponents;
}
};
return Animator;
})();
return pathComponents;
}

View File

@ -1,6 +1,9 @@
export const Y_AXIS_MARGIN = 60; export const Y_AXIS_MARGIN = 60;
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 AXIS_DATASET_CHART_TYPES = ['line', 'bar'];
export const BAR_CHART_SPACE_RATIO = 0.5; export const BAR_CHART_SPACE_RATIO = 0.5;
export const MIN_BAR_PERCENT_HEIGHT = 0.01;
export const LINE_CHART_DOT_SIZE = 4;

View File

@ -124,7 +124,7 @@ export function makePath(pathStr, className='', stroke='none', fill='none') {
} }
export function makeGradient(svgDefElem, color, lighter = false) { export function makeGradient(svgDefElem, color, lighter = false) {
let gradientId ='path-fill-gradient' + '-' + color; let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
let opacities = [1, 0.6, 0.2]; let opacities = [1, 0.6, 0.2];
if(lighter) { if(lighter) {
@ -400,75 +400,64 @@ export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, m
} }
} }
export class AxisChartRenderer { export function datasetDot(x, y, radius, color, label='', index=0, meta={}) {
constructor(state) { let dot = createSVG('circle', {
this.refreshState(state); style: `fill: ${color}`,
} 'data-point-index': index,
cx: x,
cy: y,
r: radius
});
refreshState(state) { if(!label && !label.length) {
this.totalHeight = state.totalHeight; return dot;
this.totalWidth = state.totalWidth; } else {
this.zeroLine = state.zeroLine; dot.setAttribute('cy', 0);
this.unitWidth = state.unitWidth; dot.setAttribute('cx', 0);
this.xAxisMode = state.xAxisMode;
this.yAxisMode = state.yAxisMode;
}
setZeroline(zeroLine) { let text = createSVG('text', {
this.zeroLine = zeroLine; className: 'data-point-value',
}
xMarker() {}
xRegion() {
return createSVG('rect', {
className: `bar mini`, // remove class
style: `fill: rgba(228, 234, 239, 0.49)`,
// 'data-point-index': index,
x: 0, x: 0,
y: y2, y: 0,
width: this.totalWidth, dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
height: y1 - y2 'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
}); });
return region; let group = createSVG('g', {
} transform: `translate(${x}, ${y})`
});
group.appendChild(dot);
group.appendChild(text);
animatebar(bar, x, yTop, index, noOfDatasets) { return group;
let start = x - this.avgUnitWidth/4;
let width = (this.avgUnitWidth/2)/noOfDatasets;
let [height, y] = getBarHeightAndYAttr(yTop, this.zeroLine, this.totalHeight);
x = start + (width * index);
return [bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING];
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
}
animatedot(dot, x, yTop) {
return [dot, {cx: x, cy: yTop}, UNIT_ANIM_DUR, STD_EASING];
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}
animatepath(paths, pathStr) {
let pathComponents = [];
const animPath = [paths[0], {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath);
if(paths[1]) {
let regStartPt = `0,${this.zeroLine}L`;
let regEndPt = `L${this.totalWidth}, ${this.zeroLine}`;
const animRegion = [
paths[1],
{d:"M" + regStartPt + pathStr + regEndPt},
PATH_ANIM_DUR,
STD_EASING
];
pathComponents.push(animRegion);
}
return pathComponents;
} }
} }
export function getPaths(xList, yList, color, options={}, meta={}) {
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(options.heatline) {
let gradient_id = makeGradient(meta.svgDefs, color);
path.style.stroke = `url(#${gradient_id})`;
}
let paths = {
path: path
}
// Region
if(options.regionFill) {
let gradient_id_region = makeGradient(meta.svgDefs, color, true);
// TODO: use zeroLine OR minimum
let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
}
return paths;
}

0
src/js/utils/keyboard.js Normal file
View File