From b6a6874fabe17bfc13ee3eb517f5f57d3b9913cf Mon Sep 17 00:00:00 2001 From: Ross Johnson <159597299+rosco54@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:45:36 +1100 Subject: [PATCH] Some fixes to a related and recently merged post-release 3.48.0 PR. Updates to improve stacking of line-area type series to expand use cases, further leveraging the recent yaxis.seriesName as-an-array feature. Not really adding any new features but bridging some gaps that previously did not make sense to do using the existing config options. Generalization of series group naming to apply to line/area series in similar way to how applies to bar/column series. Remove an arbitrary x offset: X axisBorder was shifted too far right by 4px. Remove an arbitrary y offset: X axis was shifted down in relation to the Y axis ticks by 1px. Center bar/column stroke paths on svg x and y path coordinates: columns were offset from X axis ticks by a full stroke width, particularly for grouped columns. Charts configured with 'responsive.options' would trigger a config reset to w.globals.initialConfig but did not adjust or carry forward collapsed series status (series.data === []) if triggered, resulting in gaps in stacked bars and columns. --- src/charts/Bar.js | 9 +- src/charts/BarStacked.js | 112 +++++++--------- src/charts/BoxCandleStick.js | 4 + src/charts/Line.js | 22 +-- src/charts/RangeBar.js | 3 + src/charts/common/bar/DataLabels.js | 92 +++++++------ src/charts/common/bar/Helpers.js | 138 ++++++++++--------- src/modules/Core.js | 129 ++++++++++-------- src/modules/CoreUtils.js | 191 +++++++++++++++++++++++++++ src/modules/Data.js | 26 ++-- src/modules/Range.js | 40 +++--- src/modules/Responsive.js | 10 +- src/modules/Scales.js | 8 +- src/modules/axes/XAxis.js | 2 +- src/modules/dimensions/Dimensions.js | 2 +- src/modules/settings/Globals.js | 2 + 16 files changed, 524 insertions(+), 266 deletions(-) diff --git a/src/charts/Bar.js b/src/charts/Bar.js index fac10ba0e..0ab6835a4 100644 --- a/src/charts/Bar.js +++ b/src/charts/Bar.js @@ -52,6 +52,7 @@ class Bar { 'column', ]) + this.columnGroupIndices = [] const barSeriesIndices = ser.getBarSeriesIndices() const coreUtils = new CoreUtils(this.ctx) this.stackedSeriesTotals = coreUtils.getStackedSeriesTotals( @@ -109,6 +110,9 @@ class Bar { let realIndex = w.globals.comboCharts ? seriesIndex[i] : i + let {columnGroupIndex} = + this.barHelpers.getGroupIndex(realIndex) + // el to which series will be drawn let elSeries = graphics.group({ class: `apexcharts-series`, @@ -262,6 +266,7 @@ class Bar { pathFill, j, i, + columnGroupIndex, pathFrom: paths.pathFrom, pathTo: paths.pathTo, strokeWidth, @@ -295,7 +300,7 @@ class Bar { lineFill, j, i, - groupIndex, // required in grouped-stacked bars + columnGroupIndex, pathFrom, pathTo, strokeWidth, @@ -408,7 +413,7 @@ class Bar { j, series, realIndex, - groupIndex, + columnGroupIndex, barHeight, barWidth, barXPosition, diff --git a/src/charts/BarStacked.js b/src/charts/BarStacked.js index 653b8aa36..2afb7eb75 100644 --- a/src/charts/BarStacked.js +++ b/src/charts/BarStacked.js @@ -24,7 +24,9 @@ class BarStacked extends Bar { this.barHelpers.initVariables(series) if (w.config.chart.stackType === '100%') { - series = w.globals.seriesPercent.slice() + series = w.globals.comboCharts + ? seriesIndex.map((_) => w.globals.seriesPercent[_]) + : w.globals.seriesPercent.slice() } this.series = series @@ -43,28 +45,14 @@ class BarStacked extends Bar { let zeroH // zeroH is the baseline where 0 meets y axis let zeroW // zeroW is the baseline where 0 meets x axis - let groupIndex = -1 // groupIndex is the index of group buckets (group1, group2, ...) - this.groupCtx = this - - w.globals.seriesGroups.forEach((group, gIndex) => { - // w.config.series[i].name may be undefined, so use - // w.globals.seriesNames[i], which has auto-generated names for those - // series. w.globals.seriesGroups[] uses the same auto-gen naming, so - // these will match. - if (group.indexOf(w.globals.seriesNames[i]) > -1) { - groupIndex = gIndex - } - }) - - if (groupIndex !== -1) { - this.groupCtx = this[w.globals.seriesGroups[groupIndex]] - } + let realIndex = w.globals.comboCharts ? seriesIndex[i] : i + let {groupIndex, columnGroupIndex} = + this.barHelpers.getGroupIndex(realIndex) + this.groupCtx = this[w.globals.seriesGroups[groupIndex]] let xArrValues = [] let yArrValues = [] - let realIndex = w.globals.comboCharts ? seriesIndex[i] : i - let translationsIndex = 0 if (this.yRatio.length > 1) { this.yaxisIndex = w.globals.seriesYAxisReverseMap[realIndex][0] @@ -126,8 +114,8 @@ class BarStacked extends Bar { this.groupCtx.prevY.length === 1 && this.groupCtx.prevY[0].every((val) => isNaN(val)) ) { - this.groupCtx.prevY[0] = this.groupCtx.prevY[0].map((val) => zeroH) - this.groupCtx.prevYF[0] = this.groupCtx.prevYF[0].map((val) => 0) + this.groupCtx.prevY[0] = this.groupCtx.prevY[0].map(() => zeroH) + this.groupCtx.prevYF[0] = this.groupCtx.prevYF[0].map(() => 0) } for (let j = 0; j < w.globals.dataPoints; j++) { @@ -138,7 +126,7 @@ class BarStacked extends Bar { x, y, elSeries, - groupIndex, + columnGroupIndex, seriesGroup: w.globals.seriesGroups[groupIndex], } let paths = null @@ -186,7 +174,7 @@ class BarStacked extends Bar { pathFill, j, i, - groupIndex, + columnGroupIndex, pathFrom: paths.pathFrom, pathTo: paths.pathTo, strokeWidth, @@ -228,19 +216,18 @@ class BarStacked extends Bar { if (this.isHorizontal) { // height divided into equal parts yDivision = w.globals.gridHeight / w.globals.dataPoints - barHeight = yDivision - - barHeight = - (barHeight * parseInt(w.config.plotOptions.bar.barHeight, 10)) / 100 - if (String(w.config.plotOptions.bar.barHeight).indexOf('%') === -1) { - barHeight = parseInt(w.config.plotOptions.bar.barHeight, 10) + let userBarHeight = w.config.plotOptions.bar.barHeight + if (String(userBarHeight).indexOf('%') === -1) { + barHeight = parseInt(userBarHeight, 10) + } else { + barHeight = yDivision * parseInt(userBarHeight, 10) / 100 } zeroW = - this.baseLineInvertedY + - w.globals.padHorizontal + - (this.isReversed ? w.globals.gridWidth : 0) - - (this.isReversed ? this.baseLineInvertedY * 2 : 0) + w.globals.padHorizontal + + (this.isReversed + ? w.globals.gridWidth - this.baseLineInvertedY + : this.baseLineInvertedY) // initial y position is half of barHeight * half of number of Bars y = (yDivision - barHeight) / 2 @@ -250,30 +237,35 @@ class BarStacked extends Bar { barWidth = xDivision + let userColumnWidth = w.config.plotOptions.bar.columnWidth if (w.globals.isXNumeric && w.globals.dataPoints > 1) { - // the check (w.globals.dataPoints > 1) fixes apexcharts.js #1617 xDivision = w.globals.minXDiff / this.xRatio barWidth = (xDivision * parseInt(this.barOptions.columnWidth, 10)) / 100 + } else if (String(userColumnWidth).indexOf('%') === -1) { + barWidth = parseInt(userColumnWidth, 10) } else { - barWidth = - (barWidth * parseInt(w.config.plotOptions.bar.columnWidth, 10)) / 100 + barWidth *= parseInt(userColumnWidth, 10) / 100 } - if (String(w.config.plotOptions.bar.columnWidth).indexOf('%') === -1) { - barWidth = parseInt(w.config.plotOptions.bar.columnWidth, 10) - } zeroH = w.globals.gridHeight - this.baseLineY[translationsIndex] - - (this.isReversed ? w.globals.gridHeight : 0) + - (this.isReversed ? this.baseLineY[translationsIndex] * 2 : 0) + (this.isReversed ? w.globals.gridHeight : 0) - // initial x position is one third of barWidth + // initial x position is the left-most edge of the first bar relative to + // the left-most side of the grid area. x = w.globals.padHorizontal + (xDivision - barWidth) / 2 } - let subDivisions = - w.globals.barGroups.length ? w.globals.barGroups.length : 1 + // Up to this point, barWidth is the width that will accommodate all bars + // at each datapoint or category. + + // The crude subdivision here assumes the series within each group are + // stacked. If there is no stacking then the barWidth/barHeight is + // further divided later by the number of series in the group. So, eg, two + // groups of three series would become six bars side-by-side unstacked, + // or two bars stacked. + let subDivisions = w.globals.barGroups.length || 1 return { x, @@ -294,16 +286,17 @@ class BarStacked extends Bar { zeroW, x, y, - groupIndex, + columnGroupIndex, seriesGroup, yDivision, elSeries, }) { let w = this.w - let barYPosition = y + (groupIndex !== -1 ? groupIndex * barHeight : 0) + let barYPosition = y + columnGroupIndex * barHeight let barXPosition let i = indexes.i let j = indexes.j + let realIndex = indexes.realIndex let translationsIndex = indexes.translationsIndex let prevBarW = 0 @@ -312,9 +305,7 @@ class BarStacked extends Bar { } let gsi = i // an index to keep track of the series inside a group - if (seriesGroup) { - gsi = seriesGroup.indexOf(w.config.series[i].name) - } + gsi = seriesGroup.indexOf(w.config.series[realIndex].name) if (gsi > 0) { let bXP = zeroW @@ -378,6 +369,7 @@ class BarStacked extends Bar { pathTo: paths.pathTo, pathFrom: paths.pathFrom, goalX: this.barHelpers.getGoalValues('x', zeroW, null, i, j, translationsIndex), + barXPosition, barYPosition, x, y, @@ -391,7 +383,7 @@ class BarStacked extends Bar { xDivision, barWidth, zeroH, - groupIndex, + columnGroupIndex, seriesGroup, elSeries, }) { @@ -399,21 +391,17 @@ class BarStacked extends Bar { let i = indexes.i let j = indexes.j let bc = indexes.bc + let realIndex = indexes.realIndex let translationsIndex = indexes.translationsIndex if (w.globals.isXNumeric) { - let seriesVal = w.globals.seriesX[i][j] + let seriesVal = w.globals.seriesX[realIndex][j] if (!seriesVal) seriesVal = 0 - x = (seriesVal - w.globals.minX) / this.xRatio - barWidth / 2 - - if (w.globals.barGroups.length) { - x = - (seriesVal - w.globals.minX) / this.xRatio - - (barWidth / 2) * w.globals.barGroups.length - } + // TODO: move the barWidth factor to barXPosition + x = (seriesVal - w.globals.minX) / this.xRatio - barWidth / 2 * w.globals.barGroups.length } - let barXPosition = x + (groupIndex !== -1 ? groupIndex * barWidth : 0) + let barXPosition = x + columnGroupIndex * barWidth let barYPosition let prevBarH = 0 @@ -427,17 +415,17 @@ class BarStacked extends Bar { let gsi = i // an index to keep track of the series inside a group if (seriesGroup) { - gsi = seriesGroup.indexOf(w.globals.seriesNames[i]) + gsi = seriesGroup.indexOf(w.globals.seriesNames[realIndex]) } if ( (gsi > 0 && !w.globals.isXNumeric) || (gsi > 0 && w.globals.isXNumeric && - w.globals.seriesX[i - 1][j] === w.globals.seriesX[i][j]) + w.globals.seriesX[realIndex - 1][j] === w.globals.seriesX[realIndex][j]) ) { let bYP let prevYValue - const p = Math.min(this.yRatio.length + 1, i + 1) + const p = Math.min(this.yRatio.length + 1, realIndex + 1) if ( this.groupCtx.prevY[gsi - 1] !== undefined && this.groupCtx.prevY[gsi - 1].length diff --git a/src/charts/BoxCandleStick.js b/src/charts/BoxCandleStick.js index 5a06a0e6e..5053ab5b4 100644 --- a/src/charts/BoxCandleStick.js +++ b/src/charts/BoxCandleStick.js @@ -48,6 +48,9 @@ class BoxCandleStick extends Bar { let xArrj = [] // hold x values of current iterating series let realIndex = w.globals.comboCharts ? seriesIndex[i] : i + // As BoxCandleStick derives from Bar, we need this to render. + let {columnGroupIndex} = + this.barHelpers.getGroupIndex(realIndex) // el to which series will be drawn let elSeries = graphics.group({ @@ -161,6 +164,7 @@ class BoxCandleStick extends Bar { x, y, series, + columnGroupIndex, barHeight, barWidth, elDataLabelsWrap, diff --git a/src/charts/Line.js b/src/charts/Line.js index aa0069e5e..f85492b71 100644 --- a/src/charts/Line.js +++ b/src/charts/Line.js @@ -54,8 +54,11 @@ class Line { series = coreUtils.getLogSeries(series) this.yRatio = coreUtils.getLogYRatios(this.yRatio) + // We call draw() for each series group + this.prevSeriesY = [] - // push all series in an array, so we can draw in reverse order (for stacked charts) + // push all series in an array, so we can draw in reverse order + // (for stacked charts) let allSeries = [] for (let i = 0; i < series.length; i++) { @@ -224,8 +227,8 @@ class Line { } if (w.config.chart.stacked) { - for (let s = allSeries.length; s > 0; s--) { - ret.add(allSeries[s - 1]) + for (let s = allSeries.length - 1; s >= 0; s--) { + ret.add(allSeries[s]) } } else { for (let s = 0; s < allSeries.length; s++) { @@ -595,15 +598,16 @@ class Line { // for the next series, hence find the prevIndex of prev series // which is not collapsed - fixes apexcharts.js#1372 const prevIndex = (pi) => { - let pii = pi - for (let cpi = 0; cpi < w.globals.series.length; cpi++) { - if (w.globals.collapsedSeriesIndices.indexOf(pi) > -1) { + for (let pii = pi; pii > 0; pii--) { + if (w.globals.collapsedSeriesIndices.indexOf( + seriesIndex?.[pii] || pii + ) > -1) { pii-- - break + } else { + return pii } } - - return pii >= 0 ? pii : 0 + return 0 } lineYPosition = this.prevSeriesY[prevIndex(i - 1)][j + 1] } else { diff --git a/src/charts/RangeBar.js b/src/charts/RangeBar.js index 8e12f0c5d..b1588a6c9 100644 --- a/src/charts/RangeBar.js +++ b/src/charts/RangeBar.js @@ -34,6 +34,8 @@ class RangeBar extends Bar { zeroW // zeroW is the baseline where 0 meets x axis let realIndex = w.globals.comboCharts ? seriesIndex[i] : i + let {columnGroupIndex} = + this.barHelpers.getGroupIndex(realIndex) // el to which series will be drawn let elSeries = graphics.group({ @@ -212,6 +214,7 @@ class RangeBar extends Bar { barXPosition, barYPosition, barWidth, + columnGroupIndex, elDataLabelsWrap, elGoalsMarkers, visibleSeries: this.visibleI, diff --git a/src/charts/common/bar/DataLabels.js b/src/charts/common/bar/DataLabels.js index 5d98e170c..c2c8ee247 100644 --- a/src/charts/common/bar/DataLabels.js +++ b/src/charts/common/bar/DataLabels.js @@ -30,7 +30,7 @@ export default class BarDataLabels { i, j, realIndex, - groupIndex, + columnGroupIndex, series, barHeight, barWidth, @@ -46,12 +46,14 @@ export default class BarDataLabels { ? this.barCtx.strokeWidth[realIndex] : this.barCtx.strokeWidth - let bcx = x + parseFloat(barWidth * visibleSeries) - let bcy = y + parseFloat(barHeight * visibleSeries) - + let bcx + let bcy if (w.globals.isXNumeric && !w.globals.isBarHorizontal) { bcx = x + parseFloat(barWidth * (visibleSeries + 1)) bcy = y + parseFloat(barHeight * (visibleSeries + 1)) - strokeWidth + } else { + bcx = x + parseFloat(barWidth * visibleSeries) + bcy = y + parseFloat(barHeight * visibleSeries) } let dataLabels = null @@ -98,7 +100,7 @@ export default class BarDataLabels { i, j, realIndex, - groupIndex: !!groupIndex ? groupIndex : -1, + columnGroupIndex, renderedPath, bcx, bcy, @@ -184,7 +186,7 @@ export default class BarDataLabels { i, j, realIndex, - groupIndex, + columnGroupIndex, y, bcx, barWidth, @@ -203,23 +205,22 @@ export default class BarDataLabels { let totalDataLabelsY let totalDataLabelsX let totalDataLabelsAnchor = 'middle' + let totalDataLabelsBcx = bcx barHeight = Math.abs(barHeight) let vertical = w.config.plotOptions.bar.dataLabels.orientation === 'vertical' - const { zeroEncounters } = this.barCtx.barHelpers.getZeroValueEncounters({ - i, - j, - }) + const { zeroEncounters } = + this.barCtx.barHelpers.getZeroValueEncounters({i, j}) bcx = - bcx - strokeWidth / 2 + (groupIndex !== -1 ? groupIndex * barWidth : 0) + bcx - strokeWidth / 2 + columnGroupIndex * barWidth let dataPointsDividedWidth = w.globals.gridWidth / w.globals.dataPoints if (this.barCtx.isVerticalGroupedRangeBar) { - dataLabelsX = dataLabelsX + barWidth / 2 + dataLabelsX += barWidth / 2 } else { if (w.globals.isXNumeric) { dataLabelsX = bcx - barWidth / 2 + offX @@ -230,7 +231,7 @@ export default class BarDataLabels { zeroEncounters > 0 && w.config.plotOptions.bar.hideZeroBarsWhenGrouped ) { - dataLabelsX = dataLabelsX - barWidth * zeroEncounters + dataLabelsX -= barWidth * zeroEncounters } } @@ -244,7 +245,7 @@ export default class BarDataLabels { let newY = y if (this.barCtx.isReversed) { - newY = y - barHeight + (valIsNegative ? barHeight * 2 : 0) + newY = y + (valIsNegative ? barHeight : -barHeight) y = y - barHeight } @@ -302,7 +303,7 @@ export default class BarDataLabels { this.barCtx.lastActiveBarSerieIndex === realIndex && barTotalDataLabelsConfig.enabled ) { - const ADDITIONAL_OFFX = 18 + const ADDITIONAL_OFFY = 18 const graphics = new Graphics(this.barCtx.ctx) const totalLabeltextRects = graphics.getTextRects( @@ -316,17 +317,28 @@ export default class BarDataLabels { totalLabeltextRects.height / 2 - offY - barTotalDataLabelsConfig.offsetY + - ADDITIONAL_OFFX + ADDITIONAL_OFFY } else { totalDataLabelsY = newY + totalLabeltextRects.height + offY + barTotalDataLabelsConfig.offsetY - - ADDITIONAL_OFFX + ADDITIONAL_OFFY } - totalDataLabelsX = dataLabelsX + barTotalDataLabelsConfig.offsetX + totalDataLabelsX = + totalDataLabelsBcx + + (w.globals.isXNumeric + ? (barWidth * (w.globals.barGroups.length - 1) + - barWidth / 2 + ) + : -(barWidth * w.globals.barGroups.length + - barWidth / 2 + - strokeWidth * 2 + ) + ) + + barTotalDataLabelsConfig.offsetX } if (!w.config.chart.stacked) { @@ -355,7 +367,7 @@ export default class BarDataLabels { i, j, realIndex, - groupIndex, + columnGroupIndex, bcy, barHeight, barWidth, @@ -373,7 +385,7 @@ export default class BarDataLabels { barWidth = Math.abs(barWidth) - bcy = bcy + (groupIndex !== -1 ? groupIndex * barHeight : 0) + bcy += columnGroupIndex * barHeight let dataLabelsY = bcy - @@ -391,8 +403,9 @@ export default class BarDataLabels { let newX = x if (this.barCtx.isReversed) { - newX = x + barWidth - (valIsNegative ? barWidth * 2 : 0) + newX = x + (valIsNegative ? -barWidth : barWidth) x = w.globals.gridWidth - barWidth + totalDataLabelsAnchor = valIsNegative ? 'start' : 'end' } switch (barDataLabelsConfig.position) { @@ -436,7 +449,6 @@ export default class BarDataLabels { this.barCtx.lastActiveBarSerieIndex === realIndex && barTotalDataLabelsConfig.enabled ) { - const ADDITIONAL_OFFX = 15 const graphics = new Graphics(this.barCtx.ctx) const totalLabeltextRects = graphics.getTextRects( this.getStackedTotalDataLabel({ realIndex, j }), @@ -445,23 +457,26 @@ export default class BarDataLabels { if (valIsNegative) { totalDataLabelsX = newX - - strokeWidth + - Math.round(totalLabeltextRects.width / 2) - + strokeWidth - offX - - barTotalDataLabelsConfig.offsetX - - ADDITIONAL_OFFX + barTotalDataLabelsConfig.offsetX totalDataLabelsAnchor = 'end' } else { totalDataLabelsX = - newX - - strokeWidth - - Math.round(totalLabeltextRects.width / 2) + + newX + offX + barTotalDataLabelsConfig.offsetX + - ADDITIONAL_OFFX + (this.barCtx.isReversed + ? -(barWidth + strokeWidth) + : strokeWidth) } - totalDataLabelsY = dataLabelsY + barTotalDataLabelsConfig.offsetY + totalDataLabelsY = + dataLabelsY + - textRects.height / 2 + + totalLabeltextRects.height / 2 + + barTotalDataLabelsConfig.offsetY + + strokeWidth } if (!w.config.chart.stacked) { @@ -626,15 +641,14 @@ export default class BarDataLabels { this.barCtx.lastActiveBarSerieIndex === realIndex ) { totalDataLabelText = graphics.drawText({ - x: - x - - (!w.globals.isBarHorizontal && w.globals.seriesGroups.length - ? barWidth / w.globals.seriesGroups.length + // TODO: Add gap, visibleI + x: x + - (!w.globals.isBarHorizontal && w.globals.barGroups.length + ? barWidth * (w.globals.barGroups.length - 1) / 2 : 0), - y: - y - - (w.globals.isBarHorizontal && w.globals.seriesGroups.length - ? barHeight / w.globals.seriesGroups.length + y: y + - (w.globals.isBarHorizontal && w.globals.barGroups.length + ? barHeight * (w.globals.barGroups.length - 1) / 2 : 0), foreColor: barTotalDataLabelsConfig.style.color, text: val, diff --git a/src/charts/common/bar/Helpers.js b/src/charts/common/bar/Helpers.js index 2396d4e56..c3e2dc5a1 100644 --- a/src/charts/common/bar/Helpers.js +++ b/src/charts/common/bar/Helpers.js @@ -155,49 +155,31 @@ export default class Helpers { initializeStackedPrevVars(ctx) { const w = ctx.w - if (w.globals.hasSeriesGroups) { - w.globals.seriesGroups.forEach((group) => { - if (!ctx[group]) ctx[group] = {} - - ctx[group].prevY = [] - ctx[group].prevX = [] - ctx[group].prevYF = [] - ctx[group].prevXF = [] - ctx[group].prevYVal = [] - ctx[group].prevXVal = [] - }) - } else { - ctx.prevY = [] // y position on chart (in columns) - ctx.prevX = [] // x position on chart (in horz bars) - ctx.prevYF = [] // starting y and ending y (height) in columns - ctx.prevXF = [] // starting x and ending x (width) in bars - ctx.prevYVal = [] // y values (series[i][j]) in columns - ctx.prevXVal = [] // x values (series[i][j]) in bars - } + w.globals.seriesGroups.forEach((group) => { + if (!ctx[group]) ctx[group] = {} + + ctx[group].prevY = [] + ctx[group].prevX = [] + ctx[group].prevYF = [] + ctx[group].prevXF = [] + ctx[group].prevYVal = [] + ctx[group].prevXVal = [] + }) } initializeStackedXYVars(ctx) { const w = ctx.w - if (w.globals.hasSeriesGroups) { - w.globals.seriesGroups.forEach((group) => { - if (!ctx[group]) ctx[group] = {} + w.globals.seriesGroups.forEach((group) => { + if (!ctx[group]) ctx[group] = {} - ctx[group].xArrj = [] - ctx[group].xArrjF = [] - ctx[group].xArrjVal = [] - ctx[group].yArrj = [] - ctx[group].yArrjF = [] - ctx[group].yArrjVal = [] - }) - } else { - ctx.xArrj = [] // xj indicates x position on graph in bars - ctx.xArrjF = [] // xjF indicates bar's x position + x2 positions in bars - ctx.xArrjVal = [] // x val means the actual series's y values in horizontal/bars - ctx.yArrj = [] // yj indicates y position on graph in columns - ctx.yArrjF = [] // yjF indicates bar's y position + y2 positions in columns - ctx.yArrjVal = [] // y val means the actual series's y values in columns - } + ctx[group].xArrj = [] + ctx[group].xArrjF = [] + ctx[group].xArrjVal = [] + ctx[group].yArrj = [] + ctx[group].yArrjF = [] + ctx[group].yArrjVal = [] + }) } getPathFillColor(series, i, j, realIndex) { @@ -337,17 +319,20 @@ export default class Helpers { bW = barWidth + w.config.series[realIndex].data[j].columnWidthOffset } - const x1 = bXP - const x2 = bXP + bW + // Center the stroke on the coordinates + let strokeCenter = strokeWidth / 2 - // append tiny pixels to avoid exponentials (which cause issues in border-radius) - y1 += 0.001 - y2 += 0.001 + const x1 = bXP + strokeCenter + const x2 = bXP + bW - strokeCenter + // append tiny pixels to avoid exponentials (which cause issues in border-radius) + y1 += 0.001 - strokeCenter + y2 += 0.001 + strokeCenter + let pathTo = graphics.move(x1, y1) let pathFrom = graphics.move(x1, y1) - const sl = graphics.line(x2 - strokeWidth, y1) + const sl = graphics.line(x2, y1) if (w.globals.previousPaths.length > 0) { pathFrom = this.barCtx.getPreviousPath(realIndex, j, false) } @@ -355,8 +340,8 @@ export default class Helpers { pathTo = pathTo + graphics.line(x1, y2) + - graphics.line(x2 - strokeWidth, y2) + - graphics.line(x2 - strokeWidth, y1) + + graphics.line(x2, y2) + + graphics.line(x2, y1) + (w.config.plotOptions.bar.borderRadiusApplication === 'around' ? ' Z' : ' z') @@ -385,11 +370,9 @@ export default class Helpers { if (w.config.chart.stacked) { let _ctx = this.barCtx - if (w.globals.hasSeriesGroups && seriesGroup) { - _ctx = this.barCtx[seriesGroup] - } - _ctx.yArrj.push(y2) - _ctx.yArrjF.push(Math.abs(y1 - y2)) + _ctx = this.barCtx[seriesGroup] + _ctx.yArrj.push(y2 - strokeCenter) + _ctx.yArrjF.push(Math.abs(y1 - y2 + strokeWidth)) _ctx.yArrjVal.push(this.barCtx.series[i][j]) } @@ -426,12 +409,15 @@ export default class Helpers { bH = barHeight + w.config.series[realIndex].data[j].barHeightOffset } - const y1 = bYP - const y2 = bYP + bH + // Center the stroke on the coordinates + let strokeCenter = strokeWidth / 2 + + const y1 = bYP + strokeCenter + const y2 = bYP + bH - strokeCenter // append tiny pixels to avoid exponentials (which cause issues in border-radius) - x1 += 0.001 - x2 += 0.001 + x1 += 0.001 - strokeCenter + x2 += 0.001 + strokeCenter let pathTo = graphics.move(x1, y1) let pathFrom = graphics.move(x1, y1) @@ -440,11 +426,11 @@ export default class Helpers { pathFrom = this.barCtx.getPreviousPath(realIndex, j, false) } - const sl = graphics.line(x1, y2 - strokeWidth) + const sl = graphics.line(x1, y2) pathTo = pathTo + graphics.line(x2, y1) + - graphics.line(x2, y2 - strokeWidth) + + graphics.line(x2, y2) + sl + (w.config.plotOptions.bar.borderRadiusApplication === 'around' ? ' Z' @@ -472,11 +458,8 @@ export default class Helpers { if (w.config.chart.stacked) { let _ctx = this.barCtx - if (w.globals.hasSeriesGroups && seriesGroup) { - _ctx = this.barCtx[seriesGroup] - } - - _ctx.xArrj.push(x2) + _ctx = this.barCtx[seriesGroup] + _ctx.xArrj.push(x2 + strokeCenter) _ctx.xArrjF.push(Math.abs(x1 - x2)) _ctx.xArrjVal.push(this.barCtx.series[i][j]) } @@ -686,12 +669,17 @@ export default class Helpers { let nonZeroColumns = 0 let zeroEncounters = 0 - w.globals.seriesPercent.forEach((_s, _si) => { - if (_s[j]) { + let seriesIndices = + w.config.plotOptions.bar.horizontal + ? w.globals.series.map((_,_i) => _i) + : w.globals.columnSeries?.i.map((_i) => _i) || [] + + seriesIndices.forEach((_si) => { + let val = w.globals.seriesPercent[_si][j] + if (val) { nonZeroColumns++ } - - if (_si < i && _s[j] === 0) { + if (_si < i && val === 0) { zeroEncounters++ } }) @@ -701,4 +689,24 @@ export default class Helpers { zeroEncounters, } } + + getGroupIndex(seriesIndex) { + const w = this.w + // groupIndex is the index of group buckets (group1, group2, ...) + let groupIndex = w.globals.seriesGroups.findIndex((group) => + // w.config.series[i].name may be undefined, so use + // w.globals.seriesNames[i], which has default names for those + // series. w.globals.seriesGroups[] uses the same default naming. + group.indexOf(w.globals.seriesNames[seriesIndex]) > -1 + ) + // We need the column groups to be indexable as 0,1,2,... for their + // positioning relative to each other. + let cGI = this.barCtx.columnGroupIndices + let columnGroupIndex = cGI.indexOf(groupIndex) + if (columnGroupIndex < 0) { + cGI.push(groupIndex) + columnGroupIndex = cGI.length - 1 + } + return {groupIndex, columnGroupIndex} + } } diff --git a/src/modules/Core.js b/src/modules/Core.js index 2cab828a9..da9c7b6a8 100644 --- a/src/modules/Core.js +++ b/src/modules/Core.js @@ -180,85 +180,101 @@ export default class Core { let chartType = cnf.chart.type !== undefined ? cnf.chart.type : 'line' // Check if the user has specified a type for any series. + let nonComboType = null let comboCount = 0 - gl.series.map((serie, st) => { + gl.series.forEach((serie, st) => { // The default type for chart is "line" and the default for series is the // chart type, therefore, if the types of all series match the chart type, // this should not be considered a combo chart. - // Combo charts are explicitly excluded from stacking with the exception - // that series of type "bar" can be stacked if the user sets "stackOnlyBar" - // true. - if (typeof ser[st].type !== 'undefined') { - if (ser[st].type === 'column' || ser[st].type === 'bar') { + let seriesType = ser[st].type || chartType + switch (seriesType) { + case 'column': + case 'bar': columnSeries.series.push(serie) columnSeries.i.push(st) - w.globals.columnSeries = columnSeries.series - if (chartType !== 'bar') { - if (gl.series.length > 1 && cnf.plotOptions.bar.horizontal) { - // horizontal bars not supported in mixed charts, hence show a warning - console.warn( - 'Horizontal bars are not supported in a mixed/combo chart. Please turn off `plotOptions.bar.horizontal`' - ) - } - comboCount++ - } - } else if (ser[st].type === 'area') { + w.globals.columnSeries = columnSeries + break + case 'area': areaSeries.series.push(serie) areaSeries.i.push(st) - if (chartType !== ser[st].type) { - comboCount++ - } - } else if (ser[st].type === 'line') { + break + case 'line': lineSeries.series.push(serie) lineSeries.i.push(st) - if (chartType !== ser[st].type) { - comboCount++ - } - } else if (ser[st].type === 'scatter') { + break + case 'scatter': scatterSeries.series.push(serie) scatterSeries.i.push(st) - } else if (ser[st].type === 'bubble') { + break + case 'bubble': bubbleSeries.series.push(serie) bubbleSeries.i.push(st) - if (chartType !== ser[st].type) { - comboCount++ - } - } else if (ser[st].type === 'candlestick') { + break + case 'candlestick': candlestickSeries.series.push(serie) candlestickSeries.i.push(st) - if (chartType !== ser[st].type) { - comboCount++ - } - } else if (ser[st].type === 'boxPlot') { + break + case 'boxPlot': boxplotSeries.series.push(serie) boxplotSeries.i.push(st) - if (chartType !== ser[st].type) { - comboCount++ - } - } else if (ser[st].type === 'rangeBar') { + break + case 'rangeBar': rangeBarSeries.series.push(serie) rangeBarSeries.i.push(st) - if (chartType !== ser[st].type) { - comboCount++ - } - } else if (ser[st].type === 'rangeArea') { + break + case 'rangeArea': rangeAreaSeries.series.push(gl.seriesRangeStart[st]) rangeAreaSeries.seriesRangeEnd.push(gl.seriesRangeEnd[st]) rangeAreaSeries.i.push(st) - if (chartType !== ser[st].type) { - comboCount++ - } - } else { - // user has specified type, but it is not valid (other than line/area/column) + break + case 'heatmap': + case 'treemap': + case 'pie': + case 'donut': + case 'polarArea': + case 'radialBar': + case 'radar': + nonComboType = seriesType + break + default: + // user has specified an invalid type console.warn( - 'You have specified an unrecognized chart type. Available types for this property are line/area/column/bar/scatter/bubble/candlestick/boxPlot/rangeBar/rangeArea' + 'You have specified an unrecognized series type (', + seriesType, + ').' ) - } - } else { - lineSeries.series.push(serie) - lineSeries.i.push(st) + break + } + if (chartType !== seriesType && seriesType !== 'scatter') { + comboCount++ } }) + if (comboCount > 0) { + if (nonComboType !== null) { + console.warn( + 'Chart or series type ', + nonComboType, + ' can not appear with other chart or series types.' + ) + } + if (columnSeries.series.length > 0 + && cnf.plotOptions.bar.horizontal + ) { + // horizontal bars not supported in mixed charts + comboCount -= columnSeries.length + columnSeries = { + series: [], + i: [], + } + w.globals.columnSeries = { + series: [], + i: [], + } + console.warn( + 'Horizontal bars are not supported in a mixed/combo chart. Please turn off `plotOptions.bar.horizontal`' + ) + } + } gl.comboCharts ||= comboCount > 0 let line = new Line(this.ctx, xyRatios) @@ -270,8 +286,11 @@ export default class Core { let elGraph = [] if (gl.comboCharts) { + const coreUtils = new CoreUtils(this.ctx) if (areaSeries.series.length > 0) { - elGraph.push(line.draw(areaSeries.series, 'area', areaSeries.i)) + elGraph.push( + ...coreUtils.drawSeriesByGroup(areaSeries, gl.areaGroups, 'area', line) + ) } if (columnSeries.series.length > 0) { if (w.config.chart.stacked) { @@ -293,7 +312,9 @@ export default class Core { ) } if (lineSeries.series.length > 0) { - elGraph.push(line.draw(lineSeries.series, 'line', lineSeries.i)) + elGraph.push( + ...coreUtils.drawSeriesByGroup(lineSeries, gl.lineGroups, 'line', line) + ) } if (candlestickSeries.series.length > 0) { elGraph.push( diff --git a/src/modules/CoreUtils.js b/src/modules/CoreUtils.js index a350be56a..df25c2a20 100644 --- a/src/modules/CoreUtils.js +++ b/src/modules/CoreUtils.js @@ -116,6 +116,175 @@ class CoreUtils { return total } + setSeriesYAxisMappings() { + const gl = this.w.globals + const cnf = this.w.config + + // The old config method to map multiple series to a y axis is to + // include one yaxis config per series but set each yaxis seriesName to the + // same series name. This relies on indexing equivalence to map series to + // an axis: series[n] => yaxis[n]. This needs to be retained for compatibility. + // But we introduce an alternative that explicitly configures yaxis elements + // with the series that will be referenced to them (seriesName: []). This + // only requires including the yaxis elements that will be seen on the chart. + // Old way: + // ya: s + // 0: 0 + // 1: 1 + // 2: 1 + // 3: 1 + // 4: 1 + // Axes 0..4 are all scaled and all will be rendered unless the axes are + // show: false. If the chart is stacked, it's assumed that series 1..4 are + // the contributing series. This is not particularly intuitive. + // New way: + // ya: s + // 0: [0] + // 1: [1,2,3,4] + // If the chart is stacked, it can be assumed that any axis with multiple + // series is stacked. + // + // If this is an old chart and we are being backward compatible, it will be + // expected that each series is associated with it's corresponding yaxis + // through their indices, one-to-one. + // If yaxis.seriesName matches series.name, we have indices yi and si. + // A name match where yi != si is interpretted as yaxis[yi] and yaxis[si] + // will both be scaled to fit the combined series[si] and series[yi]. + // Consider series named: S0,S1,S2 and yaxes A0,A1,A2. + // + // Example 1: A0 and A1 scaled the same. + // A0.seriesName: S0 + // A1.seriesName: S0 + // A2.seriesName: S2 + // Then A1 <-> A0 + // + // Example 2: A0, A1 and A2 all scaled the same. + // A0.seriesName: S2 + // A1.seriesName: S0 + // A2.seriesName: S1 + // A0 <-> A2, A1 <-> A0, A2 <-> A1 --->>> A0 <-> A1 <-> A2 + + let axisSeriesMap = [] + let seriesYAxisReverseMap = [] + let unassignedSeriesIndices = [] + let seriesNameArrayStyle = + gl.series.length > cnf.yaxis.length + || cnf.yaxis.some((a) => Array.isArray(a.seriesName)) + + cnf.series.forEach((s, i) => { + unassignedSeriesIndices.push(i) + seriesYAxisReverseMap.push(null) + }) + cnf.yaxis.forEach((yaxe, yi) => { + axisSeriesMap[yi] = [] + }) + + let unassignedYAxisIndices = [] + + // here, we loop through the yaxis array and find the item which has "seriesName" property + cnf.yaxis.forEach((yaxe, yi) => { + let assigned = false + // Allow seriesName to be either a string (for backward compatibility), + // in which case, handle multiple yaxes referencing the same series. + // or an array of strings so that a yaxis can reference multiple series. + // Feature request #4237 + if (yaxe.seriesName) { + let seriesNames = [] + if (Array.isArray(yaxe.seriesName)) { + seriesNames = yaxe.seriesName + } else { + seriesNames.push(yaxe.seriesName) + } + seriesNames.forEach((name) => { + cnf.series.forEach((s, si) => { + if (s.name === name) { + let remove = si + if (yi === si || seriesNameArrayStyle) { + // New style, don't allow series to be double referenced + if (!seriesNameArrayStyle + || unassignedSeriesIndices.indexOf(si) > -1 + ) { + axisSeriesMap[yi].push([yi,si]) + } else { + console.warn( + "Series '" + + s.name + + "' referenced more than once in what looks like the new style." + + " That is, when using either seriesName: []," + + " or when there are more series than yaxes.") + } + } else { + // The series index refers to the target yaxis and the current + // yaxis index refers to the actual referenced series. + axisSeriesMap[si].push([si,yi]) + remove = yi + } + assigned = true + remove = unassignedSeriesIndices.indexOf(remove) + if (remove !== -1) { + unassignedSeriesIndices.splice(remove, 1) + } + } + }) + }) + } + if (!assigned) { + unassignedYAxisIndices.push(yi) + } + }) + axisSeriesMap = axisSeriesMap.map((yaxe, yi) => { + let ra = [] + yaxe.forEach((sa) => { + seriesYAxisReverseMap[sa[1]] = sa[0] + ra.push(sa[1]) + }) + return ra + }) + + // All series referenced directly by yaxes have been assigned to those axes. + // Any series so far unassigned will be assigned to any yaxes that have yet + // to reference series directly, one-for-one in order of appearance, with + // all left-over series assigned to either the last unassigned yaxis, or the + // last yaxis if all have assigned series. This captures the + // default single and multiaxis config options which simply includes zero, + // one or as many yaxes as there are series but do not reference them by name. + let lastUnassignedYAxis = cnf.yaxis.length - 1 + for (let i = 0; i < unassignedYAxisIndices.length; i++) { + lastUnassignedYAxis = unassignedYAxisIndices[i] + axisSeriesMap[lastUnassignedYAxis] = [] + if (unassignedSeriesIndices) { + let si = unassignedSeriesIndices[0] + unassignedSeriesIndices.shift() + axisSeriesMap[lastUnassignedYAxis].push(si) + seriesYAxisReverseMap[si] = lastUnassignedYAxis + } else { + break + } + } + + unassignedSeriesIndices.forEach((i) => { + axisSeriesMap[lastUnassignedYAxis].push(i) + seriesYAxisReverseMap[i] = lastUnassignedYAxis + }) + + // For the old-style seriesName-as-string-only, leave the zero-length yaxis + // array elements in for compatibility so that series.length == yaxes.length + // for multi axis charts. + gl.seriesYAxisMap = axisSeriesMap.map((x) => x) + gl.seriesYAxisReverseMap = seriesYAxisReverseMap.map((x) => x) + // Set default series group names + gl.seriesYAxisMap.forEach((axisSeries, ai) => { + axisSeries.forEach((si) => { + // series may be bare until loaded in realtime + if (cnf.series[si] && cnf.series[si].group === undefined) { + // A series with no group defined will be named after the axis that + // referenced it and thus form a group automatically. + cnf.series[si].group = 'apexcharts-axis-'.concat(ai.toString()) + } + }) + }) + } + isSeriesNull(index = null) { let r = [] if (index === null) { @@ -434,6 +603,28 @@ class CoreUtils { return options } + + // Series of the same group and type can be stacked together distinct from + // other series of the same type on the same axis. + drawSeriesByGroup(typeSeries, typeGroups, type, chartClass) { + let w = this.w + let graph = [] + if (typeSeries.series.length > 0) { + // draw each group separately + typeGroups.forEach((gn) => { + let gs = [] + let gi = [] + typeSeries.i.forEach((i, ii) => { + if (w.config.series[i].group === gn) { + gs.push(typeSeries.series[ii]) + gi.push(i) + } + }) + gs.length > 0 && graph.push(chartClass.draw(gs, type, gi)) + }) + } + return graph + } } export default CoreUtils diff --git a/src/modules/Data.js b/src/modules/Data.js index 84f449875..dc4f29935 100644 --- a/src/modules/Data.js +++ b/src/modules/Data.js @@ -412,18 +412,20 @@ export default class Data { } }) - gl.hasSeriesGroups = ser[0]?.group - if (gl.hasSeriesGroups) { - let buckets = [] - let groups = [...new Set(ser.map((s) => s.group))] - ser.forEach((s, i) => { - let index = groups.indexOf(s.group) - if (!buckets[index]) buckets[index] = [] - - buckets[index].push(gl.seriesNames[i]) - }) - gl.seriesGroups = buckets - } + this.coreUtils.setSeriesYAxisMappings() + // At this point, every series that didn't have a user defined group name + // has been given a name according to the yaxis the series is referenced by. + // This fits the existing behaviour where all series associated with an axis + // are defacto presented as a single group. It is now formalised. + let buckets = [] + let groups = [...new Set(cnf.series.map((s) => s.group))] + cnf.series.forEach((s, i) => { + let index = groups.indexOf(s.group) + if (!buckets[index]) buckets[index] = [] + + buckets[index].push(gl.seriesNames[i]) + }) + gl.seriesGroups = buckets const handleDates = () => { for (let j = 0; j < xlabels.length; j++) { diff --git a/src/modules/Range.js b/src/modules/Range.js index 2aff8dddf..aeee84362 100644 --- a/src/modules/Range.js +++ b/src/modules/Range.js @@ -327,30 +327,40 @@ class Range { }) } - // for multi y-axis we need different scales for each if (gl.isMultipleYAxis) { this.scales.scaleMultipleYAxes() gl.minY = lowestYInAllSeries } else { - gl.barGroups = [] - cnf.series.forEach((s) => { - if ((!s.type && cnf.chart.type === 'bar') - || s.type === 'bar' - || s.type === 'column' - ) { - gl.barGroups.push(s.group ? s.group : 'axis-0') - } - }) - gl.barGroups = gl.barGroups.filter((v,i,a) => a.indexOf(v) === i) this.scales.setYScaleForIndex(0, gl.minY, gl.maxY) gl.minY = gl.yAxisScale[0].niceMin gl.maxY = gl.yAxisScale[0].niceMax - gl.minYArr[0] = gl.yAxisScale[0].niceMin - gl.maxYArr[0] = gl.yAxisScale[0].niceMax - gl.seriesYAxisMap = [gl.series.map((x, i) => i)] - gl.seriesYAxisReverseMap = gl.series.map((x, i) => 0) + gl.minYArr[0] = gl.minY + gl.maxYArr[0] = gl.maxY } + gl.barGroups = [] + gl.lineGroups = [] + gl.areaGroups = [] + cnf.series.forEach((s) => { + let type = s.type || cnf.chart.type + switch (type) { + case 'bar': + case 'column': + gl.barGroups.push(s.group) + break + case 'line': + gl.lineGroups.push(s.group) + break + case 'area': + gl.areaGroups.push(s.group) + break + } + }) + // Uniquify the group names in each stackable chart type. + gl.barGroups = gl.barGroups.filter((v,i,a) => a.indexOf(v) === i) + gl.lineGroups = gl.lineGroups.filter((v,i,a) => a.indexOf(v) === i) + gl.areaGroups = gl.areaGroups.filter((v,i,a) => a.indexOf(v) === i) + return { minY: gl.minY, maxY: gl.maxY, diff --git a/src/modules/Responsive.js b/src/modules/Responsive.js index 2081a025a..8ae015cdb 100644 --- a/src/modules/Responsive.js +++ b/src/modules/Responsive.js @@ -37,9 +37,14 @@ export default class Responsive { const width = window.innerWidth > 0 ? window.innerWidth : screen.width if (width > largestBreakpoint) { + let initialConfig = Utils.clone(w.globals.initialConfig) + // Retain state of series in case any have been collapsed + // (indicated by series.data === [], these series' will be zeroed later + // enabling stacking to work correctly) + initialConfig.series = Utils.clone(w.config.series) let options = CoreUtils.extendArrayProps( config, - w.globals.initialConfig, + initialConfig, w ) newOptions = Utils.extend(options, newOptions) @@ -48,7 +53,8 @@ export default class Responsive { } else { for (let i = 0; i < res.length; i++) { if (width < res[i].breakpoint) { - newOptions = CoreUtils.extendArrayProps(config, res[i].options, w) + let options = CoreUtils.extendArrayProps(config, res[i].options, w) + newOptions = Utils.extend(options, newOptions) newOptions = Utils.extend(w.config, newOptions) this.overrideResponsiveOptions(newOptions) } diff --git a/src/modules/Scales.js b/src/modules/Scales.js index dca8a1fe3..15e997f6e 100644 --- a/src/modules/Scales.js +++ b/src/modules/Scales.js @@ -830,12 +830,12 @@ export default class Scales { maxY = Math.max(maxY, Math.max.apply(null, posSeries[gni])) }) } else { - // We don't expect multiple groups per yaxis for line-like - // series, but we allow it anyway. groupNames.forEach((gn, gni) => { - minY = Math.min(lowestY, Math.min.apply(null, sumSeries[gni])) - maxY = Math.max(highestY, Math.max.apply(null, sumSeries[gni])) + lowestY = Math.min(lowestY, Math.min.apply(null, sumSeries[gni])) + highestY = Math.max(highestY, Math.max.apply(null, sumSeries[gni])) }) + minY = lowestY + maxY = highestY } if (minY === Number.MIN_VALUE && maxY === Number.MIN_VALUE) { // No series data diff --git a/src/modules/axes/XAxis.js b/src/modules/axes/XAxis.js index 954178ece..c6f1e7ee2 100644 --- a/src/modules/axes/XAxis.js +++ b/src/modules/axes/XAxis.js @@ -31,7 +31,7 @@ export default class XAxis { if (w.config.xaxis.position === 'top') { this.offY = 0 } else { - this.offY = w.globals.gridHeight + 1 + this.offY = w.globals.gridHeight } this.offY = this.offY + w.config.xaxis.axisBorder.offsetY this.isCategoryBarHorizontal = diff --git a/src/modules/dimensions/Dimensions.js b/src/modules/dimensions/Dimensions.js index 5a0437764..dbddd6b92 100644 --- a/src/modules/dimensions/Dimensions.js +++ b/src/modules/dimensions/Dimensions.js @@ -87,7 +87,7 @@ export default class Dimensions { gl.translateX + this.gridPad.left + this.xPadLeft + - (barWidth > 0 ? barWidth + 4 : 0) + (barWidth > 0 ? barWidth : 0) gl.translateY = gl.translateY + this.gridPad.top } diff --git a/src/modules/settings/Globals.js b/src/modules/settings/Globals.js index 62fbcb36c..d35221cfa 100644 --- a/src/modules/settings/Globals.js +++ b/src/modules/settings/Globals.js @@ -29,6 +29,8 @@ export default class Globals { gl.hasXaxisGroups = false gl.groups = [] gl.barGroups = [] + gl.lineGroups = [] + gl.areaGroups = [] gl.hasSeriesGroups = false gl.seriesGroups = [] gl.categoryLabels = []