447 lines
11 KiB
JavaScript
447 lines
11 KiB
JavaScript
import { makeSVGGroup } from '../utils/draw';
|
|
import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare } from '../utils/draw';
|
|
import { equilizeNoOfElements } from '../utils/draw-utils';
|
|
import { translateHoriLine, translateVertLine, animateRegion, animateBar,
|
|
animateDot, animatePath, animatePathStr } from '../utils/animate';
|
|
import { getMonthName } from '../utils/date-utils';
|
|
|
|
class ChartComponent {
|
|
constructor({
|
|
layerClass = '',
|
|
layerTransform = '',
|
|
constants,
|
|
|
|
getData,
|
|
makeElements,
|
|
animateElements
|
|
}) {
|
|
this.layerTransform = layerTransform;
|
|
this.constants = constants;
|
|
|
|
this.makeElements = makeElements;
|
|
this.getData = getData;
|
|
|
|
this.animateElements = animateElements;
|
|
|
|
this.store = [];
|
|
this.labels = [];
|
|
|
|
this.layerClass = layerClass;
|
|
this.layerClass = typeof(this.layerClass) === 'function'
|
|
? this.layerClass() : this.layerClass;
|
|
|
|
this.refresh();
|
|
}
|
|
|
|
refresh(data) {
|
|
this.data = data || this.getData();
|
|
}
|
|
|
|
setup(parent) {
|
|
this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent);
|
|
}
|
|
|
|
make() {
|
|
this.render(this.data);
|
|
this.oldData = this.data;
|
|
}
|
|
|
|
render(data) {
|
|
this.store = this.makeElements(data);
|
|
|
|
this.layer.textContent = '';
|
|
this.store.forEach(element => {
|
|
this.layer.appendChild(element);
|
|
});
|
|
this.labels.forEach(element => {
|
|
this.layer.appendChild(element);
|
|
});
|
|
}
|
|
|
|
update(animate = true) {
|
|
this.refresh();
|
|
let animateElements = [];
|
|
if(animate) {
|
|
animateElements = this.animateElements(this.data) || [];
|
|
}
|
|
return animateElements;
|
|
}
|
|
}
|
|
|
|
let componentConfigs = {
|
|
donutSlices: {
|
|
layerClass: 'donut-slices',
|
|
makeElements(data) {
|
|
return data.sliceStrings.map((s, i) => {
|
|
let slice = makePath(s, 'donut-path', data.colors[i], 'none', data.strokeWidth);
|
|
slice.style.transition = 'transform .3s;';
|
|
return slice;
|
|
});
|
|
},
|
|
|
|
animateElements(newData) {
|
|
return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i]));
|
|
},
|
|
},
|
|
pieSlices: {
|
|
layerClass: 'pie-slices',
|
|
makeElements(data) {
|
|
return data.sliceStrings.map((s, i) =>{
|
|
let slice = makePath(s, 'pie-path', 'none', data.colors[i]);
|
|
slice.style.transition = 'transform .3s;';
|
|
return slice;
|
|
});
|
|
},
|
|
|
|
animateElements(newData) {
|
|
return this.store.map((slice, i) =>
|
|
animatePathStr(slice, newData.sliceStrings[i])
|
|
);
|
|
}
|
|
},
|
|
percentageBars: {
|
|
layerClass: 'percentage-bars',
|
|
makeElements(data) {
|
|
return data.xPositions.map((x, i) =>{
|
|
let y = 0;
|
|
let bar = percentageBar(x, y, data.widths[i],
|
|
this.constants.barHeight, this.constants.barDepth, data.colors[i]);
|
|
return bar;
|
|
});
|
|
},
|
|
|
|
animateElements(newData) {
|
|
if(newData) return [];
|
|
}
|
|
},
|
|
yAxis: {
|
|
layerClass: 'y axis',
|
|
makeElements(data) {
|
|
return data.positions.map((position, i) =>
|
|
yLine(position, data.labels[i], this.constants.width,
|
|
{mode: this.constants.mode, pos: this.constants.pos, shortenNumbers: this.constants.shortenNumbers})
|
|
);
|
|
},
|
|
|
|
animateElements(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
|
|
});
|
|
|
|
return this.store.map((line, i) => {
|
|
return translateHoriLine(
|
|
line, newPos[i], oldPos[i]
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
xAxis: {
|
|
layerClass: 'x axis',
|
|
makeElements(data) {
|
|
return data.positions.map((position, i) =>
|
|
xLine(position, data.calcLabels[i], this.constants.height,
|
|
{mode: this.constants.mode, pos: this.constants.pos})
|
|
);
|
|
},
|
|
|
|
animateElements(newData) {
|
|
let newPos = newData.positions;
|
|
let newLabels = newData.calcLabels;
|
|
let oldPos = this.oldData.positions;
|
|
let oldLabels = this.oldData.calcLabels;
|
|
|
|
[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
|
|
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
|
|
|
|
this.render({
|
|
positions: oldPos,
|
|
calcLabels: newLabels
|
|
});
|
|
|
|
return this.store.map((line, i) => {
|
|
return translateVertLine(
|
|
line, newPos[i], oldPos[i]
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
yMarkers: {
|
|
layerClass: 'y-markers',
|
|
makeElements(data) {
|
|
return data.map(m =>
|
|
yMarker(m.position, m.label, this.constants.width,
|
|
{labelPos: m.options.labelPos, mode: 'span', lineType: 'dashed'})
|
|
);
|
|
},
|
|
animateElements(newData) {
|
|
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
|
|
|
|
let newPos = newData.map(d => d.position);
|
|
let newLabels = newData.map(d => d.label);
|
|
let newOptions = newData.map(d => d.options);
|
|
|
|
let oldPos = this.oldData.map(d => d.position);
|
|
|
|
this.render(oldPos.map((pos, i) => {
|
|
return {
|
|
position: oldPos[i],
|
|
label: newLabels[i],
|
|
options: newOptions[i]
|
|
};
|
|
}));
|
|
|
|
return this.store.map((line, i) => {
|
|
return translateHoriLine(
|
|
line, newPos[i], oldPos[i]
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
yRegions: {
|
|
layerClass: 'y-regions',
|
|
makeElements(data) {
|
|
return data.map(r =>
|
|
yRegion(r.startPos, r.endPos, this.constants.width,
|
|
r.label, {labelPos: r.options.labelPos})
|
|
);
|
|
},
|
|
animateElements(newData) {
|
|
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
|
|
|
|
let newPos = newData.map(d => d.endPos);
|
|
let newLabels = newData.map(d => d.label);
|
|
let newStarts = newData.map(d => d.startPos);
|
|
let newOptions = newData.map(d => d.options);
|
|
|
|
let oldPos = this.oldData.map(d => d.endPos);
|
|
let oldStarts = this.oldData.map(d => d.startPos);
|
|
|
|
this.render(oldPos.map((pos, i) => {
|
|
return {
|
|
startPos: oldStarts[i],
|
|
endPos: oldPos[i],
|
|
label: newLabels[i],
|
|
options: newOptions[i]
|
|
};
|
|
}));
|
|
|
|
let animateElements = [];
|
|
|
|
this.store.map((rectGroup, i) => {
|
|
animateElements = animateElements.concat(animateRegion(
|
|
rectGroup, newStarts[i], newPos[i], oldPos[i]
|
|
));
|
|
});
|
|
|
|
return animateElements;
|
|
}
|
|
},
|
|
|
|
heatDomain: {
|
|
layerClass: function() { return 'heat-domain domain-' + this.constants.index; },
|
|
makeElements(data) {
|
|
let {index, colWidth, rowHeight, squareSize, radius, xTranslate} = this.constants;
|
|
let monthNameHeight = -12;
|
|
let x = xTranslate, y = 0;
|
|
|
|
this.serializedSubDomains = [];
|
|
|
|
data.cols.map((week, weekNo) => {
|
|
if(weekNo === 1) {
|
|
this.labels.push(
|
|
makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(),
|
|
{
|
|
fontSize: 9
|
|
}
|
|
)
|
|
);
|
|
}
|
|
week.map((day, i) => {
|
|
if(day.fill) {
|
|
let data = {
|
|
'data-date': day.yyyyMmDd,
|
|
'data-value': day.dataValue,
|
|
'data-day': i
|
|
};
|
|
let square = heatSquare('day', x, y, squareSize, radius, day.fill, data);
|
|
this.serializedSubDomains.push(square);
|
|
}
|
|
y += rowHeight;
|
|
});
|
|
y = 0;
|
|
x += colWidth;
|
|
});
|
|
|
|
return this.serializedSubDomains;
|
|
},
|
|
|
|
animateElements(newData) {
|
|
if(newData) return [];
|
|
}
|
|
},
|
|
|
|
barGraph: {
|
|
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; },
|
|
makeElements(data) {
|
|
let c = this.constants;
|
|
this.unitType = 'bar';
|
|
this.units = data.yPositions.map((y, j) => {
|
|
return datasetBar(
|
|
data.xPositions[j],
|
|
y,
|
|
data.barWidth,
|
|
c.color,
|
|
data.labels[j],
|
|
j,
|
|
data.offsets[j],
|
|
{
|
|
zeroLine: data.zeroLine,
|
|
barsWidth: data.barsWidth,
|
|
minHeight: c.minHeight
|
|
}
|
|
);
|
|
});
|
|
return this.units;
|
|
},
|
|
animateElements(newData) {
|
|
let newXPos = newData.xPositions;
|
|
let newYPos = newData.yPositions;
|
|
let newOffsets = newData.offsets;
|
|
let newLabels = newData.labels;
|
|
|
|
let oldXPos = this.oldData.xPositions;
|
|
let oldYPos = this.oldData.yPositions;
|
|
let oldOffsets = this.oldData.offsets;
|
|
let oldLabels = this.oldData.labels;
|
|
|
|
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
|
|
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
|
|
[oldOffsets, newOffsets] = equilizeNoOfElements(oldOffsets, newOffsets);
|
|
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
|
|
|
|
this.render({
|
|
xPositions: oldXPos,
|
|
yPositions: oldYPos,
|
|
offsets: oldOffsets,
|
|
labels: newLabels,
|
|
|
|
zeroLine: this.oldData.zeroLine,
|
|
barsWidth: this.oldData.barsWidth,
|
|
barWidth: this.oldData.barWidth,
|
|
});
|
|
|
|
let animateElements = [];
|
|
|
|
this.store.map((bar, i) => {
|
|
animateElements = animateElements.concat(animateBar(
|
|
bar, newXPos[i], newYPos[i], newData.barWidth, newOffsets[i],
|
|
{zeroLine: newData.zeroLine}
|
|
));
|
|
});
|
|
|
|
return animateElements;
|
|
}
|
|
},
|
|
|
|
lineGraph: {
|
|
layerClass: function() { return 'dataset-units dataset-line dataset-' + this.constants.index; },
|
|
makeElements(data) {
|
|
let c = this.constants;
|
|
this.unitType = 'dot';
|
|
this.paths = {};
|
|
if(!c.hideLine) {
|
|
this.paths = getPaths(
|
|
data.xPositions,
|
|
data.yPositions,
|
|
c.color,
|
|
{
|
|
heatline: c.heatline,
|
|
regionFill: c.regionFill,
|
|
spline: c.spline
|
|
},
|
|
{
|
|
svgDefs: c.svgDefs,
|
|
zeroLine: data.zeroLine
|
|
}
|
|
);
|
|
}
|
|
|
|
this.units = [];
|
|
if(!c.hideDots) {
|
|
this.units = 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.units);
|
|
},
|
|
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 = [];
|
|
|
|
if(Object.keys(this.paths).length) {
|
|
animateElements = animateElements.concat(animatePath(
|
|
this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline));
|
|
}
|
|
|
|
if(this.units.length) {
|
|
this.units.map((dot, i) => {
|
|
animateElements = animateElements.concat(animateDot(
|
|
dot, newXPos[i], newYPos[i]));
|
|
});
|
|
}
|
|
|
|
return animateElements;
|
|
}
|
|
}
|
|
};
|
|
|
|
export function getComponent(name, constants, getData) {
|
|
let keys = Object.keys(componentConfigs).filter(k => name.includes(k));
|
|
let config = componentConfigs[keys[0]];
|
|
Object.assign(config, {
|
|
constants: constants,
|
|
getData: getData
|
|
});
|
|
return new ChartComponent(config);
|
|
}
|