Quellcode durchsuchen

[animate] y axes

tags/1.2.0
Prateeksha Singh vor 7 Jahren
Ursprung
Commit
b11a78aa31
19 geänderte Dateien mit 574 neuen und 564 gelöschten Zeilen
  1. +256
    -248
      dist/frappe-charts.esm.js
  2. +1
    -1
      dist/frappe-charts.min.cjs.js
  3. +1
    -1
      dist/frappe-charts.min.esm.js
  4. +2
    -1
      dist/frappe-charts.min.iife.js
  5. +1
    -0
      dist/frappe-charts.min.iife.js.map
  6. +2
    -1
      docs/assets/js/frappe-charts.min.js
  7. +1
    -0
      docs/assets/js/frappe-charts.min.js.map
  8. +1
    -0
      docs/assets/js/index.js
  9. +1
    -0
      rollup.config.js
  10. +56
    -74
      src/js/charts/AxisChart.js
  11. +33
    -63
      src/js/charts/BaseChart.js
  12. +1
    -1
      src/js/charts/LineChart.js
  13. +1
    -1
      src/js/charts/MultiAxisChart.js
  14. +36
    -34
      src/js/charts/PercentageChart.js
  15. +0
    -63
      src/js/objects/ChartComponent.js
  16. +109
    -0
      src/js/objects/ChartComponents.js
  17. +19
    -19
      src/js/utils/animate.js
  18. +32
    -48
      src/js/utils/draw.js
  19. +21
    -9
      src/js/utils/intervals.js

+ 256
- 248
dist/frappe-charts.esm.js Datei anzeigen

@@ -290,6 +290,23 @@ const REPLACE_ALL_NEW_DUR = 250;

const STD_EASING = 'easein';

function translate(unit, oldCoord, newCoord, duration) {
return [
unit,
{transform: newCoord.join(', ')},
duration,
STD_EASING,
"translate",
{transform: oldCoord.join(', ')}
];
}



function translateHoriLine(yLine, newY, oldY) {
return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
}

const AXIS_TICK_LENGTH = 6;
const LABEL_MARGIN = 4;
const FONT_SIZE = 10;
@@ -478,8 +495,8 @@ function makeHoriLine(y, label, x1, x2, options={}) {
className: className,
x1: x1,
x2: x2,
y1: y,
y2: y,
y1: 0,
y2: 0,
styles: {
stroke: options.stroke
}
@@ -487,7 +504,7 @@ function makeHoriLine(y, label, x1, x2, options={}) {

let text = createSVG('text', {
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
y: y,
y: 0,
dy: (FONT_SIZE / 2 - 2) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': x1 < x2 ? 'end' : 'start',
@@ -495,6 +512,7 @@ function makeHoriLine(y, label, x1, x2, options={}) {
});

let line = createSVG('g', {
transform: `translate(0, ${y})`,
'stroke-opacity': 1
});

@@ -508,6 +526,31 @@ function makeHoriLine(y, label, x1, x2, options={}) {
return line;
}

function yLine(y, label, width, options={}) {
if(!options.pos) options.pos = 'left';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = 'span';
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

let x1 = -1 * AXIS_TICK_LENGTH;
let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;

if(options.mode === 'tick' && options.pos === 'right') {
x1 = width + AXIS_TICK_LENGTH;
x2 = width;
}

x1 += options.offset;
x2 += options.offset;

return makeHoriLine(y, label, x1, x2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType
});
}

class AxisChartRenderer {
constructor(state) {
this.refreshState(state);
@@ -529,7 +572,7 @@ class AxisChartRenderer {
xLine(x, label, options={}) {
if(!options.pos) options.pos = 'bottom';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = this.xAxisMode;
if(!options.mode) options.mode = 'span';
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

@@ -560,30 +603,7 @@ class AxisChartRenderer {
});
}

yLine(y, label, options={}) {
if(!options.pos) options.pos = 'left';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = this.yAxisMode;
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

let x1 = -1 * AXIS_TICK_LENGTH;
let x2 = options.mode === 'span' ? this.totalWidth + AXIS_TICK_LENGTH : 0;

if(options.mode === 'tick' && options.pos === 'right') {
x1 = this.totalWidth + AXIS_TICK_LENGTH;
x2 = this.totalWidth;
}

x1 += options.offset;
x2 += options.offset;

return makeHoriLine(y, label, x1, x2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType
});
}


xMarker() {}
@@ -713,25 +733,6 @@ class AxisChartRenderer {

return pathComponents;
}

translate(unit, oldCoord, newCoord, duration) {
return [
unit,
{transform: newCoord.join(', ')},
duration,
STD_EASING,
"translate",
{transform: oldCoord.join(', ')}
];
}

translateVertLine(xLine, newX, oldX) {
return this.translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
}

translateHoriLine(yLine, newY, oldY) {
return this.translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
}
}

const PRESET_COLOR_MAP = {
@@ -1066,11 +1067,15 @@ class BaseChart {
_setup() {
this.bindWindowEvents();
this.setupConstants();
this.setupComponents();

this.setMargins();
this.makeContainer();
this.makeTooltip(); // without binding

this.calcWidth();
this.makeChartArea();
this.setupComponents();

this.draw(true);
}

@@ -1113,31 +1118,13 @@ class BaseChart {
bindTooltip() {}

draw(init=false) {
// difference from update(): draw the whole object due to groudbreaking event (init, resize, etc.)
// (draw everything, layers, groups, units)

this.calcWidth();

// refresh conponent with chart
this.refresh(this.data);

this.makeChartArea();
this.setComponentParent();
this.makeComponentLayers();

this.components.forEach(c => c.make()); // or c.build()
this.renderLegend();
this.setupNavigation(init);

// first time plain render, so no rerender
this.renderComponents();
this.renderConstants();

if(this.config.animate) this.update(this.firstUpdateData);
}
this.setupNavigation(init);

update(data) {
this.refresh(data);
this.reRender();
// TODO: remove timeout and decrease post animate time in chart component
setTimeout(() => {this.update();}, 1000);
}

calcWidth() {
@@ -1153,15 +1140,34 @@ class BaseChart {
this.width = this.baseWidth - (this.translateXLeft + this.translateXRight);
}

refresh(data) { //?? refresh?
this.oldState = this.state ? JSON.parse(JSON.stringify(this.state)) : {};
this.intermedState = {}; // use this for the extra position problems?

update(data=this.data) {
this.prepareData(data);
this.reCalc();
this.calc(); // builds state
this.refreshRenderer();
this.render();
}

prepareData() {}

renderConstants() {}

calc() {} // builds state

refreshRenderer() {
this.renderer = {};
}

render(animate=true) {
this.refreshComponents();
this.elementsToAnimate = [].concat.apply([], this.components.map(c => c.update(animate)));
console.log(this.elementsToAnimate);
if(this.elementsToAnimate) {
runSMILAnimation(this.chartWrapper, this.svg, this.elementsToAnimate);
}
}

refreshComponents() {}

makeChartArea() {
this.svg = makeSVGContainer(
this.chartWrapper,
@@ -1185,41 +1191,6 @@ class BaseChart {
);
}

prepareData() {}

renderConstants() {}

reCalc() {}
// Will update values(state)
// Will recalc specific parts depending on the update

refreshRenderer() {
this.renderer = {};
}

reRender(animate=true) {
if(!animate) {
this.renderComponents();
return;
}
this.elementsToAnimate = [];
this.loadAnimatedComponents();
runSMILAnimation(this.chartWrapper, this.svg, this.elementsToAnimate);
setTimeout(() => {
this.renderComponents();
}, 400);
// TODO: should be max anim duration required
// (opt, should not redraw if still in animate?)
}

// convenient component array abstractions
setComponentParent() { this.components.forEach(c => c.setupParent(this.drawArea)); };
makeComponentLayers() { this.components.forEach(c => c.makeLayer()); }
renderComponents() { this.components.forEach(c => c.render()); }
loadAnimatedComponents() { this.components.forEach(c => c.loadAnimatedComponents()); }

refreshComponents() { this.components.forEach(c => c.refresh(this.state, this.rawChartArgs)); }

renderLegend() {}

setupNavigation(init=false) {
@@ -1299,66 +1270,107 @@ class BaseChart {

const Y_AXIS_MARGIN = 60;

class ChartComponent {
class ChartComponent$1 {
constructor({
layerClass = '',
layerTransform = '',
initData,
parent,
constants,
data,

// called on update
setData,
preMake,
make,
makeElements,
postMake,
animate
animateElements
}) {
this.parent = parent;
this.layerClass = layerClass;
this.layerTransform = layerTransform;

this.initData = initData;
this.setData = setData;
this.constants = constants;

this.preMake = preMake;
this.make = make;
this.makeElements = makeElements;
this.postMake = postMake;

this.animate = animate;
this.animateElements = animateElements;

this.layer = undefined;
this.store = [];
}
this.layer = makeSVGGroup(this.parent, this.layerClass, this.layerTransform);

refresh(state, args) {
this.meta = Object.assign((this.meta || {}), args);
this.state = state;
}
this.data = data;

this.make();
}

render() {
this.data = this.setData(); // The only without this function?
refresh(data) {
this.data = data;
}

make() {
this.preMake && this.preMake();
this.store = this.make();
this.render(this.data);
this.postMake && this.postMake();
this.oldData = this.data;
}

render(data) {
this.store = this.makeElements(data);

this.layer.textContent = '';
this.store.forEach(element => {
this.layer.appendChild(element);
});

this.postMake && this.postMake();
}

setupParent(parent) {
this.parent = parent;
update(animate = true) {
let animateElements = [];
if(animate) {
animateElements = this.animateElements(this.data);
}
// TODO: Can we remove this?
setTimeout(() => {
this.make();
}, 1400);
return animateElements;
}
}

loadAnimatedComponents() {
this.animate(this.store);
}
function getYAxisComponent(parent, constants, initData) {
return new ChartComponent$1({
parent: parent,
layerClass: 'y axis',
constants: constants,
data: initData,
makeElements: function(data) {
return data.positions.map((position, i) =>
yLine(position, data.labels[i], this.constants.width,
{mode: this.constants.mode, pos: this.constants.pos})
);
},

animateElements: function(newData) {
let newPos = newData.positions;
let newLabels = newData.labels;

let oldPos = this.oldData.positions;
let oldLabels = this.oldData.labels;

[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);

this.render({
positions: oldPos,
labels: newLabels
});

makeLayer() {
this.layer = makeSVGGroup(this.parent, this.layerClass, this.layerTransform);
}
return this.store.map((line, i) => {
return translateHoriLine(
line, newPos[i], oldPos[i]
);
});
}
})
}

const MIN_BAR_PERCENT_HEIGHT$1 = 0.01;
@@ -1612,7 +1624,7 @@ function normalize(x) {
return [sig * man, exp];
}

function getRangeIntervals(max, min=0) {
function getChartRangeIntervals(max, min=0) {
let upperBound = Math.ceil(max);
let lowerBound = Math.floor(min);
let range = upperBound - lowerBound;
@@ -1650,19 +1662,19 @@ function getRangeIntervals(max, min=0) {
return intervals;
}

function getIntervals(maxValue, minValue=0) {
function getChartIntervals(maxValue, minValue=0) {
let [normalMaxValue, exponent] = normalize(maxValue);
let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0;

// Allow only 7 significant digits
normalMaxValue = normalMaxValue.toFixed(6);

let intervals = getRangeIntervals(normalMaxValue, normalMinValue);
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
intervals = intervals.map(value => value * Math.pow(10, exponent));
return intervals;
}

function calcIntervals(values, withMinimum=false) {
function calcChartIntervals(values, withMinimum=false) {
//*** Where the magic happens ***

// Calculates best-fit y intervals from given values
@@ -1675,7 +1687,7 @@ function calcIntervals(values, withMinimum=false) {
let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars

function getPositiveFirstIntervals(maxValue, absMinValue) {
let intervals = getIntervals(maxValue);
let intervals = getChartIntervals(maxValue);

let intervalSize = intervals[1] - intervals[0];

@@ -1693,9 +1705,9 @@ function calcIntervals(values, withMinimum=false) {
if(maxValue >= 0 && minValue >= 0) {
exponent = normalize(maxValue)[1];
if(!withMinimum) {
intervals = getIntervals(maxValue);
intervals = getChartIntervals(maxValue);
} else {
intervals = getIntervals(maxValue, minValue);
intervals = getChartIntervals(maxValue, minValue);
}
}

@@ -1733,9 +1745,9 @@ function calcIntervals(values, withMinimum=false) {

exponent = normalize(pseudoMaxValue)[1];
if(!withMinimum) {
intervals = getIntervals(pseudoMaxValue);
intervals = getChartIntervals(pseudoMaxValue);
} else {
intervals = getIntervals(pseudoMaxValue, pseudoMinValue);
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
}

intervals = intervals.reverse().map(d => d * (-1));
@@ -1765,6 +1777,18 @@ function getZeroIndex(yPts) {
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) {
return orderedArray[1] - orderedArray[0];
}
@@ -1805,6 +1829,9 @@ class AxisChart extends BaseChart {
this.lineOptions = args.lineOptions;
this.type = args.type || 'line';

this.xAxisMode = args.xAxisMode || 'span';
this.yAxisMode = args.yAxisMode || 'span';

this.setupUnitRenderer();

this.zeroLine = this.height;
@@ -1908,7 +1935,7 @@ class AxisChart extends BaseChart {
};
}

reCalc() {
calc() {
let s = this.state;

s.xAxisLabels = this.data.labels;
@@ -1940,7 +1967,7 @@ class AxisChart extends BaseChart {
}

calcYAxisParameters(yAxis, dataValues, withMinimum = 'false') {
yAxis.labels = calcIntervals(dataValues, withMinimum);
yAxis.labels = calcChartIntervals(dataValues, withMinimum);
const yPts = yAxis.labels;

yAxis.scaleMultiplier = this.height / getValueRange(yPts);
@@ -2040,78 +2067,63 @@ class AxisChart extends BaseChart {
// this.bind_units(units_array);
// }

this.yAxis = getYAxisComponent(
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 + ""),
}
);
this.components = [
...this.getYAxesComponents(),
this.getXAxisComponents(),
...this.getYRegions(),
...this.getXRegions(),
...this.getYMarkerLines(),
// ...this.getXMarkerLines(),
...this.getChartComponents(),
...this.getChartLabels(),
this.yAxis
// this.getXAxisComponents(),
// ...this.getYRegions(),
// ...this.getXRegions(),
// ...this.getYMarkerLines(),
// // ...this.getXMarkerLines(),
// ...this.getChartComponents(),
// ...this.getChartLabels(),
];
}

getYAxesComponents() {
return [new ChartComponent({
layerClass: 'y axis',
setData: () => {
// let s = this.state;

// data = {};


// return data;
},
initializeData: function() {
this.axesPositions = this.state;
},
make: () => {
// positions, labels, renderer
let s = this.state;
return s.yAxis.positions.map((position, i) =>
this.renderer.yLine(position, s.yAxis.labels[i], {pos:'right'})
);
},
animate: (yLines) => {
// Equilize
let newY = this.state.yAxis.positions;
let oldY = this.oldState.yAxis.positions;

let extra = newY.length - oldY.length;
let lastLine = yLines[yLines.length - 1];
let parentNode = lastLine.parentNode;

[oldY, newY] = equilizeNoOfElements(oldY, newY);
// console.log(newY.slice(), oldY.slice());
if(extra > 0) {
for(var i = 0; i<extra; i++) {
let line = lastLine.cloneNode(true);
parentNode.appendChild(line);
yLines.push(line);
}
}
refreshComponents() {
this.refreshYAxis();
}

yLines.map((line, i) => {
// console.log(line, newY[i], oldY[i]);
this.elementsToAnimate.push(this.renderer.translateHoriLine(
line, newY[i], oldY[i]
));
});
}
})];
refreshYAxis() {
let s = this.state;
this.yAxis.refresh({
positions: s.yAxis.positions,
labels: s.yAxis.labels,
});
}

getXAxisComponents() {
return new ChartComponent({
layerClass: 'x axis',
setData: () => {},
make: () => {
setData: () => {
let s = this.state;
let data = {
positions: s.xAxisPositions,
labels: s.xAxisLabels,
};
let constants = {
mode: this.xAxisMode,
height: this.height
};
return [data, constants];
},
makeElements: () => {
let s = this.state;
// positions
// TODO: xAxis Label spacing
return s.xAxisPositions.map((position, i) =>
this.renderer.xLine(position, s.xAxisLabels[i]
xLine(position, s.xAxisLabels[i], this.constants.height
// , {pos:'top'}
)
);
@@ -2168,7 +2180,7 @@ class AxisChart extends BaseChart {
layerClass: 'dataset-units dataset-' + index,
setData: () => {},
preMake: () => { },
make: () => {
makeElements: () => {
let d = this.state.datasets[index];

return d.positions.map((y, j) => {
@@ -2230,7 +2242,7 @@ class AxisChart extends BaseChart {
return new ChartComponent({
layerClass: 'path dataset-path',
setData: () => {},
make: () => {
makeElements: () => {
let d = this.state.datasets[index];
let color = this.colors[index];

@@ -2280,7 +2292,7 @@ class AxisChart extends BaseChart {
return new ChartComponent({
layerClass: 'y-markers',
setData: () => {},
make: () => {
makeElements: () => {
let s = this.state;
return s.yMarkers.map(marker =>
this.renderer.yMarker(marker.value, marker.name,
@@ -2301,7 +2313,7 @@ class AxisChart extends BaseChart {
return new ChartComponent({
layerClass: 'y-regions',
setData: () => {},
make: () => {
makeElements: () => {
let s = this.state;
return s.yRegions.map(region =>
this.renderer.yRegion(region.start, region.end, region.name)
@@ -2312,10 +2324,6 @@ class AxisChart extends BaseChart {
});
}

getXRegions() {
return [];
}

refreshRenderer() {
// These args are basically the current state of the chart,
// with constant and alive params mixed
@@ -2335,8 +2343,6 @@ class AxisChart extends BaseChart {
this.renderer.refreshState(state);
}

this.refreshComponents();

let meta = {
totalHeight: this.height,
totalWidth: this.width,
@@ -2778,40 +2784,42 @@ class PercentageChart extends BaseChart {
});
}

bindTooltip() {
this.slices.map((slice, i) => {
slice.addEventListener('mouseenter', () => {
let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice);
calc() {}

let x = p_off.left - g_off.left + slice.offsetWidth/2;
let y = p_off.top - g_off.top - 6;
let title = (this.formatted_labels && this.formatted_labels.length>0
? this.formatted_labels[i] : this.labels[i]) + ': ';
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);
// bindTooltip() {
// this.slices.map((slice, i) => {
// slice.addEventListener('mouseenter', () => {
// let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice);

this.tip.set_values(x, y, title, percent + "%");
this.tip.show_tip();
});
});
}
// let x = p_off.left - g_off.left + slice.offsetWidth/2;
// let y = p_off.top - g_off.top - 6;
// let title = (this.formatted_labels && this.formatted_labels.length>0
// ? this.formatted_labels[i] : this.labels[i]) + ': ';
// let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);

renderLegend() {
let x_values = this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels : this.labels;
this.legend_totals.map((d, i) => {
if(d) {
let stats = $$1.create('div', {
className: 'stats',
inside: this.statsWrapper
});
stats.innerHTML = `<span class="indicator">
<i style="background: ${this.colors[i]}"></i>
<span class="text-muted">${x_values[i]}:</span>
${d}
</span>`;
}
});
}
// this.tip.set_values(x, y, title, percent + "%");
// this.tip.show_tip();
// });
// });
// }

// renderLegend() {
// let x_values = this.formatted_labels && this.formatted_labels.length > 0
// ? this.formatted_labels : this.labels;
// this.legend_totals.map((d, i) => {
// if(d) {
// let stats = $.create('div', {
// className: 'stats',
// inside: this.statsWrapper
// });
// stats.innerHTML = `<span class="indicator">
// <i style="background: ${this.colors[i]}"></i>
// <span class="text-muted">${x_values[i]}:</span>
// ${d}
// </span>`;
// }
// });
// }
}

const ANGLE_RATIO = Math.PI / 180;


+ 1
- 1
dist/frappe-charts.min.cjs.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 1
dist/frappe-charts.min.esm.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 2
- 1
dist/frappe-charts.min.iife.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
dist/frappe-charts.min.iife.js.map
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 2
- 1
docs/assets/js/frappe-charts.min.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
docs/assets/js/frappe-charts.min.js.map
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
docs/assets/js/index.js Datei anzeigen

@@ -53,6 +53,7 @@ let bar_composite_chart = new Chart ({
isNavigable: 1,
isSeries: 1,
valuesOverPoints: 1,
yAxisMode: 'tick'
// regionFill: 1
});



+ 1
- 0
rollup.config.js Datei anzeigen

@@ -16,6 +16,7 @@ import pkg from './package.json';
export default [
{
input: 'src/js/chart.js',
sourcemap: true,
output: [
{
file: 'docs/assets/js/frappe-charts.min.js',


+ 56
- 74
src/js/charts/AxisChart.js Datei anzeigen

@@ -1,13 +1,13 @@
import BaseChart from './BaseChart';
import { Y_AXIS_MARGIN } from '../utils/margins';
import { ChartComponent } from '../objects/ChartComponent';
import { getYAxisComponent } from '../objects/ChartComponents';
import { BarChartController, LineChartController, getPaths } from '../objects/AxisChartControllers';
import { getOffset, fire } from '../utils/dom';
import { AxisChartRenderer } from '../utils/draw';
import { getOffset, fire } from '../utils/dom';
import { equilizeNoOfElements } from '../utils/draw-utils';
import { Animator } from '../utils/animate';
import { Animator, translateHoriLine } from '../utils/animate';
import { runSMILAnimation } from '../utils/animation';
import { calcIntervals, getIntervalSize, getValueRange, getZeroIndex } from '../utils/intervals';
import { getRealIntervals, calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex } from '../utils/intervals';
import { floatTwo, fillArray } from '../utils/helpers';

export default class AxisChart extends BaseChart {
@@ -21,6 +21,9 @@ export default class AxisChart extends BaseChart {
this.lineOptions = args.lineOptions;
this.type = args.type || 'line';

this.xAxisMode = args.xAxisMode || 'span';
this.yAxisMode = args.yAxisMode || 'span';

this.setupUnitRenderer();

this.zeroLine = this.height;
@@ -124,7 +127,7 @@ export default class AxisChart extends BaseChart {
};
}

reCalc() {
calc() {
let s = this.state;

s.xAxisLabels = this.data.labels;
@@ -156,7 +159,7 @@ export default class AxisChart extends BaseChart {
}

calcYAxisParameters(yAxis, dataValues, withMinimum = 'false') {
yAxis.labels = calcIntervals(dataValues, withMinimum);
yAxis.labels = calcChartIntervals(dataValues, withMinimum);
const yPts = yAxis.labels;

yAxis.scaleMultiplier = this.height / getValueRange(yPts);
@@ -256,78 +259,63 @@ export default class AxisChart extends BaseChart {
// this.bind_units(units_array);
// }

this.yAxis = getYAxisComponent(
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 + ""),
}
)
this.components = [
...this.getYAxesComponents(),
this.getXAxisComponents(),
...this.getYRegions(),
...this.getXRegions(),
...this.getYMarkerLines(),
// ...this.getXMarkerLines(),
...this.getChartComponents(),
...this.getChartLabels(),
this.yAxis
// this.getXAxisComponents(),
// ...this.getYRegions(),
// ...this.getXRegions(),
// ...this.getYMarkerLines(),
// // ...this.getXMarkerLines(),
// ...this.getChartComponents(),
// ...this.getChartLabels(),
];
}

getYAxesComponents() {
return [new ChartComponent({
layerClass: 'y axis',
setData: () => {
// let s = this.state;

// data = {};


// return data;
},
initializeData: function() {
this.axesPositions = this.state
},
make: () => {
// positions, labels, renderer
let s = this.state;
return s.yAxis.positions.map((position, i) =>
this.renderer.yLine(position, s.yAxis.labels[i], {pos:'right'})
);
},
animate: (yLines) => {
// Equilize
let newY = this.state.yAxis.positions;
let oldY = this.oldState.yAxis.positions;

let extra = newY.length - oldY.length;
let lastLine = yLines[yLines.length - 1];
let parentNode = lastLine.parentNode;

[oldY, newY] = equilizeNoOfElements(oldY, newY);
// console.log(newY.slice(), oldY.slice());
if(extra > 0) {
for(var i = 0; i<extra; i++) {
let line = lastLine.cloneNode(true);
parentNode.appendChild(line);
yLines.push(line);
}
}
refreshComponents() {
this.refreshYAxis();
}

yLines.map((line, i) => {
// console.log(line, newY[i], oldY[i]);
this.elementsToAnimate.push(this.renderer.translateHoriLine(
line, newY[i], oldY[i]
));
});
}
})];
refreshYAxis() {
let s = this.state;
this.yAxis.refresh({
positions: s.yAxis.positions,
labels: s.yAxis.labels,
});
}

getXAxisComponents() {
return new ChartComponent({
layerClass: 'x axis',
setData: () => {},
make: () => {
setData: () => {
let s = this.state;
let data = {
positions: s.xAxisPositions,
labels: s.xAxisLabels,
};
let constants = {
mode: this.xAxisMode,
height: this.height
}
return [data, constants];
},
makeElements: () => {
let s = this.state;
// positions
// TODO: xAxis Label spacing
return s.xAxisPositions.map((position, i) =>
this.renderer.xLine(position, s.xAxisLabels[i]
xLine(position, s.xAxisLabels[i], this.constants.height
// , {pos:'top'}
)
);
@@ -384,7 +372,7 @@ export default class AxisChart extends BaseChart {
layerClass: 'dataset-units dataset-' + index,
setData: () => {},
preMake: () => { },
make: () => {
makeElements: () => {
let d = this.state.datasets[index];

return d.positions.map((y, j) => {
@@ -446,7 +434,7 @@ export default class AxisChart extends BaseChart {
return new ChartComponent({
layerClass: 'path dataset-path',
setData: () => {},
make: () => {
makeElements: () => {
let d = this.state.datasets[index];
let color = this.colors[index];

@@ -496,7 +484,7 @@ export default class AxisChart extends BaseChart {
return new ChartComponent({
layerClass: 'y-markers',
setData: () => {},
make: () => {
makeElements: () => {
let s = this.state;
return s.yMarkers.map(marker =>
this.renderer.yMarker(marker.value, marker.name,
@@ -517,7 +505,7 @@ export default class AxisChart extends BaseChart {
return new ChartComponent({
layerClass: 'y-regions',
setData: () => {},
make: () => {
makeElements: () => {
let s = this.state;
return s.yRegions.map(region =>
this.renderer.yRegion(region.start, region.end, region.name)
@@ -528,10 +516,6 @@ export default class AxisChart extends BaseChart {
});
}

getXRegions() {
return [];
}

refreshRenderer() {
// These args are basically the current state of the chart,
// with constant and alive params mixed
@@ -551,8 +535,6 @@ export default class AxisChart extends BaseChart {
this.renderer.refreshState(state);
}

this.refreshComponents();

let meta = {
totalHeight: this.height,
totalWidth: this.width,


+ 33
- 63
src/js/charts/BaseChart.js Datei anzeigen

@@ -128,11 +128,15 @@ export default class BaseChart {
_setup() {
this.bindWindowEvents();
this.setupConstants();
this.setupComponents();

this.setMargins();
this.makeContainer();
this.makeTooltip(); // without binding

this.calcWidth();
this.makeChartArea();
this.setupComponents();

this.draw(true);
}

@@ -175,31 +179,13 @@ export default class BaseChart {
bindTooltip() {}

draw(init=false) {
// difference from update(): draw the whole object due to groudbreaking event (init, resize, etc.)
// (draw everything, layers, groups, units)

this.calcWidth();

// refresh conponent with chart
this.refresh(this.data);

this.makeChartArea();
this.setComponentParent();
this.makeComponentLayers();

this.components.forEach(c => c.make()); // or c.build()
this.renderLegend();
this.setupNavigation(init);

// first time plain render, so no rerender
this.renderComponents();
this.renderConstants();

if(this.config.animate) this.update(this.firstUpdateData);
}
this.setupNavigation(init);

update(data) {
this.refresh(data);
this.reRender();
// TODO: remove timeout and decrease post animate time in chart component
setTimeout(() => {this.update();}, 1000);
}

calcWidth() {
@@ -215,15 +201,34 @@ export default class BaseChart {
this.width = this.baseWidth - (this.translateXLeft + this.translateXRight);
}

refresh(data) { //?? refresh?
this.oldState = this.state ? JSON.parse(JSON.stringify(this.state)) : {};
this.intermedState = {}; // use this for the extra position problems?

update(data=this.data) {
this.prepareData(data);
this.reCalc();
this.calc(); // builds state
this.refreshRenderer();
this.render();
}

prepareData() {}

renderConstants() {}

calc() {} // builds state

refreshRenderer() {
this.renderer = {};
}

render(animate=true) {
this.refreshComponents();
this.elementsToAnimate = [].concat.apply([], this.components.map(c => c.update(animate)));
console.log(this.elementsToAnimate);
if(this.elementsToAnimate) {
runSMILAnimation(this.chartWrapper, this.svg, this.elementsToAnimate);
}
}

refreshComponents() {}

makeChartArea() {
this.svg = makeSVGContainer(
this.chartWrapper,
@@ -247,41 +252,6 @@ export default class BaseChart {
);
}

prepareData() {}

renderConstants() {}

reCalc() {}
// Will update values(state)
// Will recalc specific parts depending on the update

refreshRenderer() {
this.renderer = {};
}

reRender(animate=true) {
if(!animate) {
this.renderComponents();
return;
}
this.elementsToAnimate = [];
this.loadAnimatedComponents();
runSMILAnimation(this.chartWrapper, this.svg, this.elementsToAnimate);
setTimeout(() => {
this.renderComponents();
}, 400);
// TODO: should be max anim duration required
// (opt, should not redraw if still in animate?)
}

// convenient component array abstractions
setComponentParent() { this.components.forEach(c => c.setupParent(this.drawArea)); };
makeComponentLayers() { this.components.forEach(c => c.makeLayer()); }
renderComponents() { this.components.forEach(c => c.render()); }
loadAnimatedComponents() { this.components.forEach(c => c.loadAnimatedComponents()); }

refreshComponents() { this.components.forEach(c => c.refresh(this.state, this.rawChartArgs)); }

renderLegend() {}

setupNavigation(init=false) {


+ 1
- 1
src/js/charts/LineChart.js Datei anzeigen

@@ -1,5 +1,5 @@
import AxisChart from './AxisChart';
import { ChartComponent } from '../objects/ChartComponent';
// import { ChartComponent } from '../objects/ChartComponents';
import { makeSVGGroup, makePath, makeGradient } from '../utils/draw';
import { equilizeNoOfElements } from '../utils/draw-utils';



+ 1
- 1
src/js/charts/MultiAxisChart.js Datei anzeigen

@@ -1,6 +1,6 @@
import AxisChart from './AxisChart';
import { Y_AXIS_MARGIN } from '../utils/margins';
import { ChartComponent } from '../objects/ChartComponent';
// import { ChartComponent } from '../objects/ChartComponents';
import { floatTwo } from '../utils/helpers';

export default class MultiAxisChart extends AxisChart {


+ 36
- 34
src/js/charts/PercentageChart.js Datei anzeigen

@@ -89,38 +89,40 @@ export default class PercentageChart extends BaseChart {
});
}

bindTooltip() {
this.slices.map((slice, i) => {
slice.addEventListener('mouseenter', () => {
let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice);

let x = p_off.left - g_off.left + slice.offsetWidth/2;
let y = p_off.top - g_off.top - 6;
let title = (this.formatted_labels && this.formatted_labels.length>0
? this.formatted_labels[i] : this.labels[i]) + ': ';
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);

this.tip.set_values(x, y, title, percent + "%");
this.tip.show_tip();
});
});
}

renderLegend() {
let x_values = this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels : this.labels;
this.legend_totals.map((d, i) => {
if(d) {
let stats = $.create('div', {
className: 'stats',
inside: this.statsWrapper
});
stats.innerHTML = `<span class="indicator">
<i style="background: ${this.colors[i]}"></i>
<span class="text-muted">${x_values[i]}:</span>
${d}
</span>`;
}
});
}
calc() {}

// bindTooltip() {
// this.slices.map((slice, i) => {
// slice.addEventListener('mouseenter', () => {
// let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice);

// let x = p_off.left - g_off.left + slice.offsetWidth/2;
// let y = p_off.top - g_off.top - 6;
// let title = (this.formatted_labels && this.formatted_labels.length>0
// ? this.formatted_labels[i] : this.labels[i]) + ': ';
// let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);

// this.tip.set_values(x, y, title, percent + "%");
// this.tip.show_tip();
// });
// });
// }

// renderLegend() {
// let x_values = this.formatted_labels && this.formatted_labels.length > 0
// ? this.formatted_labels : this.labels;
// this.legend_totals.map((d, i) => {
// if(d) {
// let stats = $.create('div', {
// className: 'stats',
// inside: this.statsWrapper
// });
// stats.innerHTML = `<span class="indicator">
// <i style="background: ${this.colors[i]}"></i>
// <span class="text-muted">${x_values[i]}:</span>
// ${d}
// </span>`;
// }
// });
// }
}

+ 0
- 63
src/js/objects/ChartComponent.js Datei anzeigen

@@ -1,63 +0,0 @@
import { makeSVGGroup } from '../utils/draw';

export class ChartComponent {
constructor({
layerClass = '',
layerTransform = '',
initData,

// called on update
setData,
preMake,
make,
postMake,
animate
}) {
this.layerClass = layerClass;
this.layerTransform = layerTransform;

this.initData = initData;
this.setData = setData;

this.preMake = preMake;
this.make = make;
this.postMake = postMake;

this.animate = animate;

this.layer = undefined;
this.store = [];
}

refresh(state, args) {
this.meta = Object.assign((this.meta || {}), args);
this.state = state;
}


render() {
this.data = this.setData(); // The only without this function?

this.preMake && this.preMake();
this.store = this.make();

this.layer.textContent = '';
this.store.forEach(element => {
this.layer.appendChild(element);
});

this.postMake && this.postMake();
}

setupParent(parent) {
this.parent = parent;
}

loadAnimatedComponents() {
this.animate(this.store);
}

makeLayer() {
this.layer = makeSVGGroup(this.parent, this.layerClass, this.layerTransform);
}
}

+ 109
- 0
src/js/objects/ChartComponents.js Datei anzeigen

@@ -0,0 +1,109 @@
import { makeSVGGroup } from '../utils/draw';
import { yLine } from '../utils/draw';
import { equilizeNoOfElements } from '../utils/draw-utils';
import { Animator, translateHoriLine } from '../utils/animate';

class ChartComponent {
constructor({
layerClass = '',
layerTransform = '',
parent,
constants,
data,

// called on update
preMake,
makeElements,
postMake,
animateElements
}) {
this.parent = parent;
this.layerClass = layerClass;
this.layerTransform = layerTransform;
this.constants = constants;

this.preMake = preMake;
this.makeElements = makeElements;
this.postMake = postMake;

this.animateElements = animateElements;

this.store = [];
this.layer = makeSVGGroup(this.parent, this.layerClass, this.layerTransform);

this.data = data;

this.make();
}

refresh(data) {
this.data = data;
}

make() {
this.preMake && this.preMake();
this.render(this.data);
this.postMake && this.postMake();
this.oldData = this.data;
}

render(data) {
this.store = this.makeElements(data);

this.layer.textContent = '';
this.store.forEach(element => {
this.layer.appendChild(element);
});
}

update(animate = true) {
let animateElements = []
if(animate) {
animateElements = this.animateElements(this.data);
}
// TODO: Can we remove this?
setTimeout(() => {
this.make();
}, 1400);
return animateElements;
}
}

export function getYAxisComponent(parent, constants, initData) {
return new ChartComponent({
parent: parent,
layerClass: 'y axis',
constants: constants,
data: initData,
makeElements: function(data) {
return data.positions.map((position, i) =>
yLine(position, data.labels[i], this.constants.width,
{mode: this.constants.mode, pos: this.constants.pos})
);
},

animateElements: function(newData) {
let newPos = newData.positions;
let newLabels = newData.labels;

let oldPos = this.oldData.positions;
let oldLabels = this.oldData.labels;

let extra = newPos.length - oldPos.length;

[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);

this.render({
positions: oldPos,
labels: newLabels
});

return this.store.map((line, i) => {
return translateHoriLine(
line, newPos[i], oldPos[i]
);
});
}
})
}

+ 19
- 19
src/js/utils/animate.js Datei anzeigen

@@ -7,6 +7,25 @@ export const REPLACE_ALL_NEW_DUR = 250;

export const STD_EASING = 'easein';

export function translate(unit, oldCoord, newCoord, duration) {
return [
unit,
{transform: newCoord.join(', ')},
duration,
STD_EASING,
"translate",
{transform: oldCoord.join(', ')}
];
}

export function translateVertLine(xLine, newX, oldX) {
return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
}

export function translateHoriLine(yLine, newY, oldY) {
return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
}

export var Animator = (function() {
var Animator = function(totalHeight, totalWidth, zeroLine, avgUnitWidth) {
// constants
@@ -54,25 +73,6 @@ export var Animator = (function() {
}

return pathComponents;
},

translate: function(obj, oldCoord, newCoord, duration) {
return [
{unit: obj, array: [0], index: 0},
{transform: newCoord.join(', ')},
duration,
STD_EASING,
"translate",
{transform: oldCoord.join(', ')}
];
},

translateVertLine: function(xLine, newX, oldX) {
return this.translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
},

translateHoriLine: function(yLine, newY, oldY) {
return this.translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
}
};



+ 32
- 48
src/js/utils/draw.js Datei anzeigen

@@ -208,8 +208,8 @@ function makeHoriLine(y, label, x1, x2, options={}) {
className: className,
x1: x1,
x2: x2,
y1: y,
y2: y,
y1: 0,
y2: 0,
styles: {
stroke: options.stroke
}
@@ -217,7 +217,7 @@ function makeHoriLine(y, label, x1, x2, options={}) {

let text = createSVG('text', {
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
y: y,
y: 0,
dy: (FONT_SIZE / 2 - 2) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': x1 < x2 ? 'end' : 'start',
@@ -225,6 +225,7 @@ function makeHoriLine(y, label, x1, x2, options={}) {
});

let line = createSVG('g', {
transform: `translate(0, ${y})`,
'stroke-opacity': 1
});

@@ -238,6 +239,33 @@ function makeHoriLine(y, label, x1, x2, options={}) {
return line;
}

export function yLine(y, label, width, options={}) {
if(!options.pos) options.pos = 'left';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = 'span';
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

let x1 = -1 * AXIS_TICK_LENGTH;
let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;

if(options.mode === 'tick' && options.pos === 'right') {
x1 = width + AXIS_TICK_LENGTH
x2 = width;
}

let offset = options.pos === 'left' ? -1 * options.offset : options.offset;

x1 += options.offset;
x2 += options.offset;

return makeHoriLine(y, label, x1, x2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType
});
}

export class AxisChartRenderer {
constructor(state) {
this.refreshState(state);
@@ -259,7 +287,7 @@ export class AxisChartRenderer {
xLine(x, label, options={}) {
if(!options.pos) options.pos = 'bottom';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = this.xAxisMode;
if(!options.mode) options.mode = 'span';
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

@@ -290,32 +318,7 @@ export class AxisChartRenderer {
});
}

yLine(y, label, options={}) {
if(!options.pos) options.pos = 'left';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = this.yAxisMode;
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

let x1 = -1 * AXIS_TICK_LENGTH;
let x2 = options.mode === 'span' ? this.totalWidth + AXIS_TICK_LENGTH : 0;

if(options.mode === 'tick' && options.pos === 'right') {
x1 = this.totalWidth + AXIS_TICK_LENGTH
x2 = this.totalWidth;
}

let offset = options.pos === 'left' ? -1 * options.offset : options.offset;

x1 += options.offset;
x2 += options.offset;

return makeHoriLine(y, label, x1, x2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType
});
}


xMarker() {}
@@ -445,23 +448,4 @@ export class AxisChartRenderer {

return pathComponents;
}

translate(unit, oldCoord, newCoord, duration) {
return [
unit,
{transform: newCoord.join(', ')},
duration,
STD_EASING,
"translate",
{transform: oldCoord.join(', ')}
];
}

translateVertLine(xLine, newX, oldX) {
return this.translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
}

translateHoriLine(yLine, newY, oldY) {
return this.translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
}
}

+ 21
- 9
src/js/utils/intervals.js Datei anzeigen

@@ -21,7 +21,7 @@ function normalize(x) {
return [sig * man, exp];
}

function getRangeIntervals(max, min=0) {
function getChartRangeIntervals(max, min=0) {
let upperBound = Math.ceil(max);
let lowerBound = Math.floor(min);
let range = upperBound - lowerBound;
@@ -59,19 +59,19 @@ function getRangeIntervals(max, min=0) {
return intervals;
}

function getIntervals(maxValue, minValue=0) {
function getChartIntervals(maxValue, minValue=0) {
let [normalMaxValue, exponent] = normalize(maxValue);
let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0;

// Allow only 7 significant digits
normalMaxValue = normalMaxValue.toFixed(6);

let intervals = getRangeIntervals(normalMaxValue, normalMinValue);
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
intervals = intervals.map(value => value * Math.pow(10, exponent));
return intervals;
}

export function calcIntervals(values, withMinimum=false) {
export function calcChartIntervals(values, withMinimum=false) {
//*** Where the magic happens ***

// Calculates best-fit y intervals from given values
@@ -84,7 +84,7 @@ export function calcIntervals(values, withMinimum=false) {
let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars

function getPositiveFirstIntervals(maxValue, absMinValue) {
let intervals = getIntervals(maxValue);
let intervals = getChartIntervals(maxValue);

let intervalSize = intervals[1] - intervals[0];

@@ -102,9 +102,9 @@ export function calcIntervals(values, withMinimum=false) {
if(maxValue >= 0 && minValue >= 0) {
exponent = normalize(maxValue)[1];
if(!withMinimum) {
intervals = getIntervals(maxValue);
intervals = getChartIntervals(maxValue);
} else {
intervals = getIntervals(maxValue, minValue);
intervals = getChartIntervals(maxValue, minValue);
}
}

@@ -142,9 +142,9 @@ export function calcIntervals(values, withMinimum=false) {

exponent = normalize(pseudoMaxValue)[1];
if(!withMinimum) {
intervals = getIntervals(pseudoMaxValue);
intervals = getChartIntervals(pseudoMaxValue);
} else {
intervals = getIntervals(pseudoMaxValue, pseudoMinValue);
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
}

intervals = intervals.reverse().map(d => d * (-1));
@@ -174,6 +174,18 @@ export function getZeroIndex(yPts) {
return zeroIndex;
}

export 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();
}

export function getIntervalSize(orderedArray) {
return orderedArray[1] - orderedArray[0];
}


Laden…
Abbrechen
Speichern