Feat: Adding multi y-Axis support and configurable labes

This commit is contained in:
Kaleb White 2021-11-12 18:57:02 -08:00
parent 10de973608
commit 539bc50883
6 changed files with 1054 additions and 737 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -31,14 +31,31 @@ export default class AxisChart extends BaseChart {
configure(options) {
super.configure(options);
const { axisOptions = {} } = options;
const { xAxis, yAxis } = axisOptions || {};
options.axisOptions = options.axisOptions || {};
options.tooltipOptions = options.tooltipOptions || {};
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = options.axisOptions.xIsSeries || 0;
this.config.shortenYAxisNumbers = options.axisOptions.shortenYAxisNumbers || 0;
this.config.xAxisMode = xAxis ? xAxis.xAxisMode : axisOptions.xAxisMode || 'span';
// this will pass an array
// lets determine if we need two yAxis based on if there is length
// to the yAxis array
if (yAxis && yAxis.length) {
this.config.yAxisConfig = yAxis.map((item) => {
return {
yAxisMode: item.yAxisMode,
id: item.id,
position: item.position,
title: item.title
};
});
} else {
this.config.yAxisMode = axisOptions.yAxisMode || 'span';
}
this.config.xIsSeries = axisOptions.xIsSeries || 0;
this.config.shortenYAxisNumbers = axisOptions.shortenYAxisNumbers || 0;
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY;
@ -83,18 +100,61 @@ export default class AxisChart extends BaseChart {
};
}
calcYAxisParameters(dataValues, withMinimum = 'false') {
const yPts = calcChartIntervals(dataValues, withMinimum);
const scaleMultiplier = this.height / getValueRange(yPts);
const intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
const zeroLine = this.height - (getZeroIndex(yPts) * intervalHeight);
calcYAxisParameters(dataValues, withMinimum = 'false') {
let yPts, scaleMultiplier, intervalHeight, zeroLine, positions;
// if we have an object we have multiple yAxisParameters.
if (dataValues instanceof Array) {
yPts = calcChartIntervals(dataValues, withMinimum);
scaleMultiplier = this.height / getValueRange(yPts);
intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
zeroLine = this.height - getZeroIndex(yPts) * intervalHeight;
this.state.yAxis = {
labels: yPts,
positions: yPts.map(d => zeroLine - d * scaleMultiplier),
positions: yPts.map((d) => zeroLine - d * scaleMultiplier),
scaleMultiplier: scaleMultiplier,
zeroLine: zeroLine,
zeroLine: zeroLine
};
} else {
this.state.yAxis = [];
for (let key in dataValues) {
const dataValue = dataValues[key];
yPts = calcChartIntervals(dataValue, withMinimum);
scaleMultiplier = this.height / getValueRange(yPts);
intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
zeroLine = this.height - getZeroIndex(yPts) * intervalHeight;
positions = yPts.map((d) => zeroLine - d * scaleMultiplier);
const yAxisConfigObject =
this.config.yAxisConfig.find((item) => key === item.id) || [];
const yAxisAlignment = yAxisConfigObject
? yAxisConfigObject.position
: 'right';
if (this.state.yAxis.length) {
const yPtsArray = [];
const firstArr = this.state.yAxis[0];
// we need to loop through original positions.
firstArr.positions.forEach((pos) => {
yPtsArray.push(Math.ceil(pos / scaleMultiplier));
});
yPts = yPtsArray.reverse();
zeroLine = this.height - getZeroIndex(yPts) * intervalHeight;
positions = firstArr.positions;
}
this.state.yAxis.push({
axisID: key || 'left-axis',
labels: yPts,
title: yAxisConfigObject.title,
pos: yAxisAlignment,
scaleMultiplier,
zeroLine,
positions
});
}
}
// Dependent if above changes
this.calcDatasetPoints();
@ -104,21 +164,39 @@ export default class AxisChart extends BaseChart {
calcDatasetPoints() {
let s = this.state;
let scaleAll = values => values.map(val => scale(val, s.yAxis));
let scaleAll = (values, id) => {
return values.map((val) => {
let { yAxis } = s;
if (yAxis instanceof Array) {
yAxis = yAxis.length > 1 ? yAxis.find((axis) => id === axis.axisID) : s.yAxis[0];
}
return scale(val, yAxis);
});
};
s.barChartIndex = 1;
s.datasets = this.data.datasets.map((d, i) => {
let values = d.values;
let cumulativeYs = d.cumulativeYs || [];
return {
name: d.name && d.name.replace(/<|>|&/g, (char) => char == '&' ? '&amp;' : char == '<' ? '&lt;' : '&gt;'),
name:
d.name &&
d.name.replace(/<|>|&/g, (char) =>
char == '&' ? '&amp;' : char == '<' ? '&lt;' : '&gt;'
),
index: i,
barIndex: d.chartType === 'bar' ? s.barChartIndex++ : s.barChartIndex,
chartType: d.chartType,
values: values,
yPositions: scaleAll(values),
yPositions: scaleAll(values, d.axisID),
id: d.axisID,
cumulativeYs: cumulativeYs,
cumulativeYPos: scaleAll(cumulativeYs),
cumulativeYPos: scaleAll(cumulativeYs, d.axisID)
};
});
}
@ -163,44 +241,71 @@ export default class AxisChart extends BaseChart {
getAllYValues() {
let key = 'values';
let multiAxis = this.config.yAxisConfig ? true : false;
let allValueLists = multiAxis ? {} : [];
if(this.barOptions.stacked) {
key = 'cumulativeYs';
let groupBy = (arr, property) => {
return arr.reduce((acc, cur) => {
acc[cur[property]] = [...(acc[cur[property]] || []), cur];
return acc;
}, {});
};
let generateCumulative = (arr) => {
let cumulative = new Array(this.state.datasetLength).fill(0);
this.data.datasets.map((d, i) => {
let values = this.data.datasets[i].values;
d[key] = cumulative = cumulative.map((c, i) => c + values[i]);
arr.forEach((d, i) => {
let values = arr[i].values;
d[key] = cumulative = cumulative.map((c, i) => {
return c + values[i];
});
});
};
if (this.barOptions.stacked) {
key = 'cumulativeYs';
// we need to filter out the different yAxis ID's here.
if (multiAxis) {
const groupedDataSets = groupBy(this.data.datasets, 'axisID');
// const dataSetsByAxis = this.data.dd
for (var axisID in groupedDataSets) {
generateCumulative(groupedDataSets[axisID]);
}
} else {
generateCumulative(this.data.datasets);
}
}
// this is the trouble maker, we don't want to merge all
// datasets since we are trying to run two yAxis.
if (multiAxis) {
this.data.datasets.forEach((d) => {
// if the array exists already just push more data into it.
// otherwise create a new array into the object.
allValueLists[d.axisID || key]
? allValueLists[d.axisID || key].push(...d[key])
: (allValueLists[d.axisID || key] = [...d[key]]);
});
} else {
allValueLists = this.data.datasets.map((d) => {
return d[key];
});
}
let allValueLists = this.data.datasets.map(d => d[key]);
if(this.data.yMarkers) {
allValueLists.push(this.data.yMarkers.map(d => d.value));
if (this.data.yMarkers && !multiAxis) {
allValueLists.push(this.data.yMarkers.map((d) => d.value));
}
if(this.data.yRegions) {
this.data.yRegions.map(d => {
if (this.data.yRegions && !multiAxis) {
this.data.yRegions.map((d) => {
allValueLists.push([d.end, d.start]);
});
}
return [].concat(...allValueLists);
return multiAxis ? allValueLists : [].concat(...allValueLists);
}
setupComponents() {
let componentConfigs = [
[
'yAxis',
{
mode: this.config.yAxisMode,
width: this.width,
shortenNumbers: this.config.shortenYAxisNumbers
// pos: 'right'
},
function() {
return this.state.yAxis;
}.bind(this)
],
[
'xAxis',
{
@ -229,11 +334,43 @@ export default class AxisChart extends BaseChart {
],
];
// if we have multiple yAxisConfigs we need to update the yAxisDefault
// components to multiple yAxis components.
if (this.config.yAxisConfig && this.config.yAxisConfig.length) {
this.config.yAxisConfig.forEach((yAxis) => {
componentConfigs.push([
'yAxis',
{
mode: this.config.yAxisMode,
width: this.width,
shortenNumbers: this.config.shortenYAxisNumbers,
pos: yAxis.position || 'left'
},
function () {
return this.state.yAxis;
}.bind(this)
]);
});
} else {
componentConfigs.push([
'yAxis',
{
mode: this.config.yAxisMode,
width: this.width,
shortenNumbers: this.config.shortenYAxisNumbers
},
function () {
return this.state.yAxis;
}.bind(this)
]);
}
let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
let barsConfigs = barDatasets.map(d => {
let index = d.index;
let barIndex = d.barIndex || index;
return [
'barGraph' + '-' + d.index,
{
@ -247,29 +384,41 @@ export default class AxisChart extends BaseChart {
},
function() {
let s = this.state;
let { yAxis } = s;
let d = s.datasets[index];
let { id = 'left-axis' } = d;
let stacked = this.barOptions.stacked;
let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;
let barsWidth = s.unitWidth * (1 - spaceRatio);
let barWidth = barsWidth/(stacked ? 1 : barDatasets.length);
let barWidth = barsWidth / (stacked ? 1 : barDatasets.length);
let xPositions = s.xAxis.positions.map(x => x - barsWidth/2);
if(!stacked) {
xPositions = xPositions.map(p => p + barWidth * index);
// if there are multiple yAxis we need to return the yAxis with the
// proper ID.
if (yAxis instanceof Array) {
// if the person only configured one yAxis in the array return the first.
yAxis = yAxis.length > 1 ? yAxis.find((axis) => id === axis.axisID) : s.yAxis[0];
}
let xPositions = s.xAxis.positions.map((x) => x - barsWidth / 2);
if (!stacked) {
xPositions = xPositions.map((p) => {
return p + barWidth * barIndex - barWidth;
});
}
let labels = new Array(s.datasetLength).fill('');
if(this.config.valuesOverPoints) {
if(stacked && d.index === s.datasets.length - 1) {
if (this.config.valuesOverPoints) {
if (stacked && d.index === s.datasets.length - 1) {
labels = d.cumulativeYs;
} else {
labels = d.values;
}
}
let offsets = new Array(s.datasetLength).fill(0);
if(stacked) {
if (stacked) {
offsets = d.yPositions.map((y, j) => y - d.cumulativeYPos[j]);
}
@ -280,7 +429,7 @@ export default class AxisChart extends BaseChart {
// values: d.values,
labels: labels,
zeroLine: s.yAxis.zeroLine,
zeroLine: yAxis.zeroLine,
barsWidth: barsWidth,
barWidth: barWidth,
};
@ -288,7 +437,7 @@ export default class AxisChart extends BaseChart {
];
});
let lineConfigs = lineDatasets.map(d => {
let lineConfigs = lineDatasets.map((d) => {
let index = d.index;
return [
'lineGraph' + '-' + d.index,
@ -303,13 +452,21 @@ export default class AxisChart extends BaseChart {
hideLine: this.lineOptions.hideLine,
// same for all datasets
valuesOverPoints: this.config.valuesOverPoints,
valuesOverPoints: this.config.valuesOverPoints
},
function() {
function () {
let s = this.state;
let d = s.datasets[index];
let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine
? s.yAxis.positions[0] : s.yAxis.zeroLine;
// if we have more than one yindex lets map the values
const yAxis = s.yAxis.length
? s.yAxis.find((axis) => d.id === axis.axisID) || s.yAxis[0]
: s.yAxis;
let minLine =
yAxis.positions[0] < yAxis.zeroLine
? yAxis.positions[0]
: yAxis.zeroLine;
return {
xPositions: s.xAxis.positions,
@ -318,7 +475,7 @@ export default class AxisChart extends BaseChart {
values: d.values,
zeroLine: minLine,
radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE,
radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE
};
}.bind(this)
];

View File

@ -1,5 +1,5 @@
import { makeSVGGroup } from '../utils/draw';
import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare } from '../utils/draw';
import { makeText, generateAxisLabel, 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';
@ -50,8 +50,12 @@ class ChartComponent {
this.store = this.makeElements(data);
this.layer.textContent = '';
this.store.forEach(element => {
this.layer.appendChild(element);
this.store.forEach((element) => {
element.length
? element.forEach((el) => {
this.layer.appendChild(el);
})
: this.layer.appendChild(element);
});
this.labels.forEach(element => {
this.layer.appendChild(element);
@ -117,13 +121,72 @@ let componentConfigs = {
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})
let elements = [];
if (data.length) {
data.forEach((item, i) => {
item.positions.map((position, i) => {
elements.push(
yLine(position, item.labels[i], this.constants.width, {
mode: this.constants.mode,
pos: item.pos || this.constants.pos,
shortenNumbers: this.constants.shortenNumbers
})
);
});
// we need to make yAxis titles if they are defined
if (item.title) {
elements.push(
generateAxisLabel({
title: item.title,
position: item.pos,
height: item.zeroLine,
width: this.constants.width
})
);
}
});
return elements;
}
return data.positions.map((position, i) => {
return yLine(position, data.labels[i], this.constants.width, {
mode: this.constants.mode,
pos: this.constants.pos,
shortenNumbers: this.constants.shortenNumbers
});
});
},
animateElements(newData) {
const animateMultipleElements = (oldData, newData) => {
let newPos = newData.positions;
let newLabels = newData.labels;
let oldPos = oldData.positions;
let oldLabels = 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]);
});
};
// we will need to animate both axis if we have more than one.
// so check if the oldData is an array of values.
if (this.oldData instanceof Array) {
return this.oldData.forEach((old, i) => {
animateMultipleElements(old, newData[i]);
});
}
let newPos = newData.positions;
let newLabels = newData.labels;
let oldPos = this.oldData.positions;
@ -138,9 +201,7 @@ let componentConfigs = {
});
return this.store.map((line, i) => {
return translateHoriLine(
line, newPos[i], oldPos[i]
);
return translateHoriLine(line, newPos[i], oldPos[i]);
});
}
},

View File

@ -63,13 +63,15 @@ export function zeroDataPrep(realData) {
let zeroData = {
labels: realData.labels.slice(0, -1),
datasets: realData.datasets.map(d => {
datasets: realData.datasets.map((d) => {
const { axisID } = d;
return {
axisID,
name: '',
values: zeroArray.slice(0, -1),
chartType: d.chartType
};
}),
})
};
if(realData.yMarkers) {

View File

@ -1,4 +1,9 @@
import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils';
import {
getBarHeightAndYAttr,
truncateString,
shortenLargeNumber,
getSplineCurvePointsStr
} from './draw-utils';
import { getStringWidth, isValidNumber } from './helpers';
import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants';
import { lightenDarkenColor } from './colors';
@ -11,32 +16,32 @@ const BASE_LINE_COLOR = '#dadada';
const FONT_FILL = '#555b51';
function $(expr, con) {
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
return typeof expr === 'string' ? (con || document).querySelector(expr) : expr || null;
}
export function createSVG(tag, o) {
var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
var element = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var i in o) {
var val = o[i];
if (i === "inside") {
if (i === 'inside') {
$(val).appendChild(element);
}
else if (i === "around") {
} else if (i === 'around') {
var ref = $(val);
ref.parentNode.insertBefore(element, ref);
element.appendChild(ref);
} else if (i === "styles") {
if(typeof val === "object") {
Object.keys(val).map(prop => {
} else if (i === 'styles') {
if (typeof val === 'object') {
Object.keys(val).map((prop) => {
element.style[prop] = val[prop];
});
}
} else {
if(i === "className") { i = "class"; }
if(i === "innerHTML") {
if (i === 'className') {
i = 'class';
}
if (i === 'innerHTML') {
element['textContent'] = val;
} else {
element.setAttribute(i, val);
@ -60,9 +65,9 @@ function renderVerticalGradient(svgDefElem, gradientId) {
function setGradientStop(gradElem, offset, color, opacity) {
return createSVG('stop', {
'inside': gradElem,
'style': `stop-color: ${color}`,
'offset': offset,
inside: gradElem,
style: `stop-color: ${color}`,
offset: offset,
'stop-opacity': opacity
});
}
@ -78,28 +83,34 @@ export function makeSVGContainer(parent, className, width, height) {
export function makeSVGDefs(svgContainer) {
return createSVG('defs', {
inside: svgContainer,
inside: svgContainer
});
}
export function makeSVGGroup(className, transform='', parent=undefined) {
export function makeSVGGroup(className, transform = '', parent = undefined) {
let args = {
className: className,
transform: transform
};
if(parent) args.inside = parent;
if (parent) args.inside = parent;
return createSVG('g', args);
}
export function wrapInSVGGroup(elements, className='') {
export function wrapInSVGGroup(elements, className = '') {
let g = createSVG('g', {
className: className
});
elements.forEach(e => g.appendChild(e));
elements.forEach((e) => g.appendChild(e));
return g;
}
export function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=2) {
export function makePath(
pathStr,
className = '',
stroke = 'none',
fill = 'none',
strokeWidth = 2
) {
return createSVG('path', {
className: className,
d: pathStr,
@ -111,7 +122,14 @@ export function makePath(pathStr, className='', stroke='none', fill='none', stro
});
}
export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
export function makeArcPathStr(
startPosition,
endPosition,
center,
radius,
clockWise = 1,
largeArc = 0
) {
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
return `M${center.x} ${center.y}
@ -120,9 +138,20 @@ export function makeArcPathStr(startPosition, endPosition, center, radius, clock
${arcEndX} ${arcEndY} z`;
}
export function makeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
export function makeCircleStr(
startPosition,
endPosition,
center,
radius,
clockWise = 1,
largeArc = 0
) {
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, center.y * 2, center.y + endPosition.y];
let [arcEndX, midArc, arcEndY] = [
center.x + endPosition.x,
center.y * 2,
center.y + endPosition.y
];
return `M${center.x} ${center.y}
L${arcStartX} ${arcStartY}
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
@ -132,7 +161,14 @@ export function makeCircleStr(startPosition, endPosition, center, radius, clockW
${arcEndX} ${arcEndY} z`;
}
export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
export function makeArcStrokePathStr(
startPosition,
endPosition,
center,
radius,
clockWise = 1,
largeArc = 0
) {
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
@ -141,9 +177,20 @@ export function makeArcStrokePathStr(startPosition, endPosition, center, radius,
${arcEndX} ${arcEndY}`;
}
export function makeStrokeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
export function makeStrokeCircleStr(
startPosition,
endPosition,
center,
radius,
clockWise = 1,
largeArc = 0
) {
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, radius * 2 + arcStartY, center.y + startPosition.y];
let [arcEndX, midArc, arcEndY] = [
center.x + endPosition.x,
radius * 2 + arcStartY,
center.y + startPosition.y
];
return `M${arcStartX} ${arcStartY}
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
@ -154,23 +201,29 @@ export function makeStrokeCircleStr(startPosition, endPosition, center, radius,
}
export function makeGradient(svgDefElem, color, lighter = false) {
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
let gradientId =
'path-fill-gradient' + '-' + color + '-' + (lighter ? 'lighter' : 'default');
let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
let opacities = [1, 0.6, 0.2];
if(lighter) {
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]);
setGradientStop(gradientDef, '0%', color, opacities[0]);
setGradientStop(gradientDef, '50%', color, opacities[1]);
setGradientStop(gradientDef, '100%', color, opacities[2]);
return gradientId;
}
export function percentageBar(x, y, width, height,
depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') {
export function percentageBar(
x,
y,
width,
height,
depth = PERCENTAGE_BAR_DEFAULT_DEPTH,
fill = 'none'
) {
let args = {
className: 'percentage-bar',
x: x,
@ -179,18 +232,18 @@ export function percentageBar(x, y, width, height,
height: height,
fill: fill,
styles: {
'stroke': lightenDarkenColor(fill, -25),
stroke: lightenDarkenColor(fill, -25),
// Diabolically good: https://stackoverflow.com/a/9000859
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray
'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`,
'stroke-width': depth
},
}
};
return createSVG("rect", args);
return createSVG('rect', args);
}
export function heatSquare(className, x, y, size, radius, fill='none', data={}) {
export function heatSquare(className, x, y, size, radius, fill = 'none', data = {}) {
let args = {
className: className,
x: x,
@ -201,14 +254,14 @@ export function heatSquare(className, x, y, size, radius, fill='none', data={})
fill: fill
};
Object.keys(data).map(key => {
Object.keys(data).map((key) => {
args[key] = data[key];
});
return createSVG("rect", args);
return createSVG('rect', args);
}
export function legendBar(x, y, size, fill='none', label, truncate=false) {
export function legendBar(x, y, size, fill = 'none', label, truncate = false) {
label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
let args = {
@ -223,8 +276,8 @@ export function legendBar(x, y, size, fill='none', label, truncate=false) {
className: 'legend-dataset-text',
x: 0,
y: 0,
dy: (FONT_SIZE * 2) + 'px',
'font-size': (FONT_SIZE * 1.2) + 'px',
dy: FONT_SIZE * 2 + 'px',
'font-size': FONT_SIZE * 1.2 + 'px',
'text-anchor': 'start',
fill: FONT_FILL,
innerHTML: label
@ -233,13 +286,13 @@ export function legendBar(x, y, size, fill='none', label, truncate=false) {
let group = createSVG('g', {
transform: `translate(${x}, ${y})`
});
group.appendChild(createSVG("rect", args));
group.appendChild(createSVG('rect', args));
group.appendChild(text);
return group;
}
export function legendDot(x, y, size, fill='none', label, truncate=false) {
export function legendDot(x, y, size, fill = 'none', label, truncate = false) {
label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
let args = {
@ -253,9 +306,9 @@ export function legendDot(x, y, size, fill='none', label, truncate=false) {
className: 'legend-dataset-text',
x: 0,
y: 0,
dx: (FONT_SIZE) + 'px',
dy: (FONT_SIZE/3) + 'px',
'font-size': (FONT_SIZE * 1.2) + 'px',
dx: FONT_SIZE + 'px',
dy: FONT_SIZE / 3 + 'px',
'font-size': FONT_SIZE * 1.2 + 'px',
'text-anchor': 'start',
fill: FONT_FILL,
innerHTML: label
@ -264,7 +317,7 @@ export function legendDot(x, y, size, fill='none', label, truncate=false) {
let group = createSVG('g', {
transform: `translate(${x}, ${y})`
});
group.appendChild(createSVG("circle", args));
group.appendChild(createSVG('circle', args));
group.appendChild(text);
return group;
@ -272,7 +325,7 @@ export function legendDot(x, y, size, fill='none', label, truncate=false) {
export function makeText(className, x, y, content, options = {}) {
let fontSize = options.fontSize || FONT_SIZE;
let dy = options.dy !== undefined ? options.dy : (fontSize / 2);
let dy = options.dy !== undefined ? options.dy : fontSize / 2;
let fill = options.fill || FONT_FILL;
let textAnchor = options.textAnchor || 'start';
return createSVG('text', {
@ -287,8 +340,8 @@ export function makeText(className, x, y, content, options = {}) {
});
}
function makeVertLine(x, label, y1, y2, options={}) {
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
function makeVertLine(x, label, y1, y2, options = {}) {
if (!options.stroke) options.stroke = BASE_LINE_COLOR;
let l = createSVG('line', {
className: 'line-vertical ' + options.className,
x1: 0,
@ -306,11 +359,11 @@ function makeVertLine(x, label, y1, y2, options={}) {
dy: FONT_SIZE + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label + ""
innerHTML: label + ''
});
let line = createSVG('g', {
transform: `translate(${ x }, 0)`
transform: `translate(${x}, 0)`
});
line.appendChild(l);
@ -319,13 +372,16 @@ function makeVertLine(x, label, y1, y2, options={}) {
return line;
}
function makeHoriLine(y, label, x1, x2, options={}) {
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.lineType) options.lineType = '';
function makeHoriLine(y, label, x1, x2, options = {}) {
if (!options.stroke) options.stroke = BASE_LINE_COLOR;
if (!options.lineType) options.lineType = '';
if (!options.alignment) options.alignment = 'left';
if (options.shortenNumbers) label = shortenLargeNumber(label);
let className = 'line-horizontal ' + options.className +
(options.lineType === "dashed" ? "dashed": "");
let className =
'line-horizontal ' +
options.className +
(options.lineType === 'dashed' ? 'dashed' : '');
let l = createSVG('line', {
className: className,
@ -339,12 +395,12 @@ function makeHoriLine(y, label, x1, x2, options={}) {
});
let text = createSVG('text', {
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
x: options.alignment === 'left' ? x1 - LABEL_MARGIN : x2 + LABEL_MARGIN * 4,
y: 0,
dy: (FONT_SIZE / 2 - 2) + 'px',
dy: FONT_SIZE / 2 - 2 + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': x1 < x2 ? 'end' : 'start',
innerHTML: label+""
innerHTML: label + ''
});
let line = createSVG('g', {
@ -352,8 +408,8 @@ function makeHoriLine(y, label, x1, x2, options={}) {
'stroke-opacity': 1
});
if(text === 0 || text === '0') {
line.style.stroke = "rgba(27, 31, 35, 0.6)";
if (text === 0 || text === '0') {
line.style.stroke = 'rgba(27, 31, 35, 0.6)';
}
line.appendChild(l);
@ -362,44 +418,69 @@ function makeHoriLine(y, label, x1, x2, options={}) {
return line;
}
export function yLine(y, label, width, options={}) {
export function generateAxisLabel(options) {
if (!options.title) return;
const x = options.position === 'left' ? LABEL_MARGIN : options.width;
// - getStringWidth(options.title, 5);
const rotation =
options.position === 'right'
? `rotate(90, ${options.width}, ${options.height / 2})`
: `rotate(270, 0, ${options.height / 2})`;
const labelSvg = createSVG('text', {
className: 'chart-label',
x: x - getStringWidth(options.title, 5) / 2,
y: options.height / 2 - LABEL_MARGIN,
dy: FONT_SIZE / -2 + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'start',
transform: rotation,
innerHTML: options.title + ''
});
return labelSvg;
}
export function yLine(y, label, width, options = {}) {
if (!isValidNumber(y)) y = 0;
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 = '';
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') {
if (options.mode === 'tick' && options.pos === 'right') {
x1 = width + AXIS_TICK_LENGTH;
x2 = width;
}
// let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
x1 += options.offset;
x2 += options.offset;
x1 += offset;
x2 += offset;
return makeHoriLine(y, label, x1, x2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType,
alignment: options.pos,
shortenNumbers: options.shortenNumbers
});
}
export function xLine(x, label, height, options={}) {
export function xLine(x, label, height, options = {}) {
if (!isValidNumber(x)) x = 0;
if(!options.pos) options.pos = 'bottom';
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 = '';
if (!options.pos) options.pos = 'bottom';
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 = '';
// Draw X axis line in span/tick mode with optional label
// y2(span)
@ -415,7 +496,7 @@ export function xLine(x, label, height, options={}) {
let y1 = height + AXIS_TICK_LENGTH;
let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;
if(options.mode === 'tick' && options.pos === 'top') {
if (options.mode === 'tick' && options.pos === 'top') {
// top axis ticks
y1 = -1 * AXIS_TICK_LENGTH;
y2 = 0;
@ -428,19 +509,21 @@ export function xLine(x, label, height, options={}) {
});
}
export function yMarker(y, label, width, options={}) {
if(!options.labelPos) options.labelPos = 'right';
let x = options.labelPos === 'left' ? LABEL_MARGIN
export function yMarker(y, label, width, options = {}) {
if (!options.labelPos) options.labelPos = 'right';
let x =
options.labelPos === 'left'
? LABEL_MARGIN
: width - getStringWidth(label, 5) - LABEL_MARGIN;
let labelSvg = createSVG('text', {
className: 'chart-label',
x: x,
y: 0,
dy: (FONT_SIZE / -2) + 'px',
dy: FONT_SIZE / -2 + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'start',
innerHTML: label+""
innerHTML: label + ''
});
let line = makeHoriLine(y, '', 0, width, {
@ -454,7 +537,7 @@ export function yMarker(y, label, width, options={}) {
return line;
}
export function yRegion(y1, y2, width, label, options={}) {
export function yRegion(y1, y2, width, label, options = {}) {
// return a group
let height = y1 - y2;
@ -472,18 +555,20 @@ export function yRegion(y1, y2, width, label, options={}) {
height: height
});
if(!options.labelPos) options.labelPos = 'right';
let x = options.labelPos === 'left' ? LABEL_MARGIN
: width - getStringWidth(label+"", 4.5) - LABEL_MARGIN;
if (!options.labelPos) options.labelPos = 'right';
let x =
options.labelPos === 'left'
? LABEL_MARGIN
: width - getStringWidth(label + '', 4.5) - LABEL_MARGIN;
let labelSvg = createSVG('text', {
className: 'chart-label',
x: x,
y: 0,
dy: (FONT_SIZE / -2) + 'px',
dy: FONT_SIZE / -2 + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'start',
innerHTML: label+""
innerHTML: label + ''
});
let region = createSVG('g', {
@ -496,11 +581,20 @@ export function yRegion(y1, y2, width, label, options={}) {
return region;
}
export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
export function datasetBar(
x,
yTop,
width,
color,
label = '',
index = 0,
offset = 0,
meta = {}
) {
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
y -= offset;
if(height === 0) {
if (height === 0) {
height = meta.minHeight;
y -= meta.minHeight;
}
@ -521,18 +615,18 @@ export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, m
height: height
});
label += "";
label += '';
if(!label && !label.length) {
if (!label && !label.length) {
return rect;
} else {
rect.setAttribute('y', 0);
rect.setAttribute('x', 0);
let text = createSVG('text', {
className: 'data-point-value',
x: width/2,
x: width / 2,
y: 0,
dy: (FONT_SIZE / 2 * -1) + 'px',
dy: (FONT_SIZE / 2) * -1 + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
@ -549,7 +643,7 @@ export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, m
}
}
export function datasetDot(x, y, radius, color, label='', index=0) {
export function datasetDot(x, y, radius, color, label = '', index = 0) {
let dot = createSVG('circle', {
style: `fill: ${color}`,
'data-point-index': index,
@ -558,9 +652,9 @@ export function datasetDot(x, y, radius, color, label='', index=0) {
r: radius
});
label += "";
label += '';
if(!label && !label.length) {
if (!label && !label.length) {
return dot;
} else {
dot.setAttribute('cy', 0);
@ -570,7 +664,7 @@ export function datasetDot(x, y, radius, color, label='', index=0) {
className: 'data-point-value',
x: 0,
y: 0,
dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
dy: (FONT_SIZE / 2) * -1 - radius + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
@ -587,18 +681,17 @@ export function datasetDot(x, y, radius, color, label='', index=0) {
}
}
export function getPaths(xList, yList, color, options={}, meta={}) {
let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
let pointsStr = pointsList.join("L");
export function getPaths(xList, yList, color, options = {}, meta = {}) {
let pointsList = yList.map((y, i) => xList[i] + ',' + y);
let pointsStr = pointsList.join('L');
// Spline
if (options.spline)
pointsStr = getSplineCurvePointsStr(xList, yList);
if (options.spline) pointsStr = getSplineCurvePointsStr(xList, yList);
let path = makePath("M"+pointsStr, 'line-graph-path', color);
let path = makePath('M' + pointsStr, 'line-graph-path', color);
// HeatLine
if(options.heatline) {
if (options.heatline) {
let gradient_id = makeGradient(meta.svgDefs, color);
path.style.stroke = `url(#${gradient_id})`;
}
@ -608,10 +701,14 @@ export function getPaths(xList, yList, color, options={}, meta={}) {
};
// Region
if(options.regionFill) {
if (options.regionFill) {
let gradient_id_region = makeGradient(meta.svgDefs, color, true);
let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
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})`);
}
@ -619,9 +716,9 @@ export function getPaths(xList, yList, color, options={}, meta={}) {
}
export let makeOverlay = {
'bar': (unit) => {
bar: (unit) => {
let transformValue;
if(unit.nodeName !== 'rect') {
if (unit.nodeName !== 'rect') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
@ -629,15 +726,15 @@ export let makeOverlay = {
overlay.style.fill = '#000000';
overlay.style.opacity = '0.4';
if(transformValue) {
if (transformValue) {
overlay.setAttribute('transform', transformValue);
}
return overlay;
},
'dot': (unit) => {
dot: (unit) => {
let transformValue;
if(unit.nodeName !== 'circle') {
if (unit.nodeName !== 'circle') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
@ -648,15 +745,15 @@ export let makeOverlay = {
overlay.setAttribute('fill', fill);
overlay.style.opacity = '0.6';
if(transformValue) {
if (transformValue) {
overlay.setAttribute('transform', transformValue);
}
return overlay;
},
'heat_square': (unit) => {
heat_square: (unit) => {
let transformValue;
if(unit.nodeName !== 'circle') {
if (unit.nodeName !== 'circle') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
@ -667,7 +764,7 @@ export let makeOverlay = {
overlay.setAttribute('fill', fill);
overlay.style.opacity = '0.6';
if(transformValue) {
if (transformValue) {
overlay.setAttribute('transform', transformValue);
}
return overlay;
@ -675,57 +772,57 @@ export let makeOverlay = {
};
export let updateOverlay = {
'bar': (unit, overlay) => {
bar: (unit, overlay) => {
let transformValue;
if(unit.nodeName !== 'rect') {
if (unit.nodeName !== 'rect') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
let attributes = ['x', 'y', 'width', 'height'];
Object.values(unit.attributes)
.filter(attr => attributes.includes(attr.name) && attr.specified)
.map(attr => {
.filter((attr) => attributes.includes(attr.name) && attr.specified)
.map((attr) => {
overlay.setAttribute(attr.name, attr.nodeValue);
});
if(transformValue) {
if (transformValue) {
overlay.setAttribute('transform', transformValue);
}
},
'dot': (unit, overlay) => {
dot: (unit, overlay) => {
let transformValue;
if(unit.nodeName !== 'circle') {
if (unit.nodeName !== 'circle') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
let attributes = ['cx', 'cy'];
Object.values(unit.attributes)
.filter(attr => attributes.includes(attr.name) && attr.specified)
.map(attr => {
.filter((attr) => attributes.includes(attr.name) && attr.specified)
.map((attr) => {
overlay.setAttribute(attr.name, attr.nodeValue);
});
if(transformValue) {
if (transformValue) {
overlay.setAttribute('transform', transformValue);
}
},
'heat_square': (unit, overlay) => {
heat_square: (unit, overlay) => {
let transformValue;
if(unit.nodeName !== 'circle') {
if (unit.nodeName !== 'circle') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
let attributes = ['cx', 'cy'];
Object.values(unit.attributes)
.filter(attr => attributes.includes(attr.name) && attr.specified)
.map(attr => {
.filter((attr) => attributes.includes(attr.name) && attr.specified)
.map((attr) => {
overlay.setAttribute(attr.name, attr.nodeValue);
});
if(transformValue) {
if (transformValue) {
overlay.setAttribute('transform', transformValue);
}
},
}
};