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 = []