apexcharts
Version:
A JavaScript Chart Library
566 lines (482 loc) • 16.1 kB
JavaScript
import CoreUtils from '../modules/CoreUtils'
import Bar from './Bar'
import Fill from '../modules/Fill'
import Graphics from '../modules/Graphics'
/**
* ApexCharts BarStacked Class responsible for drawing both Stacked Columns and Bars.
*
* @module BarStacked
* The whole calculation for stacked bar/column is different from normal bar/column,
* hence it makes sense to derive a new class for it extending most of the props of Parent Bar
**/
class BarStacked extends Bar {
draw (series, seriesIndex) {
let w = this.w
this.graphics = new Graphics(this.ctx)
this.fill = new Fill(this.ctx)
this.bar = new Bar(this.ctx, this.xyRatios)
const coreUtils = new CoreUtils(this.ctx, w)
this.series = coreUtils.getLogSeries(series)
series = this.series
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
this.series = series
this.initVariables(series)
if (w.config.chart.stackType === '100%') {
this.series = w.globals.seriesPercent.slice()
series = this.series
}
this.totalItems = 0
this.prevY = [] // y position on chart
this.prevX = [] // x position on chart
this.prevYF = [] // y position including shapes on chart
this.prevXF = [] // x position including shapes on chart
this.prevYVal = [] // y values (series[i][j]) in columns
this.prevXVal = [] // x values (series[i][j]) in bars
this.xArrj = [] // xj indicates x position on graph in bars
this.xArrjF = [] // xjF indicates bar's x position + endingshape's positions in bars
this.xArrjVal = [] // x val means the actual series's y values in horizontal/bars
this.yArrj = [] // yj indicates y position on graph in columns
this.yArrjF = [] // yjF indicates bar's y position + endingshape's positions in columns
this.yArrjVal = [] // y val means the actual series's y values in columns
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length > 0) {
this.totalItems += series[sl].length
}
}
this.zeroSerieses = []
this.endingShapeOnSeriesNumber = series.length - 1 // which series to draw ending shape on
this.checkZeroSeries({series})
let ret = this.graphics.group({
class: 'apexcharts-bar-series apexcharts-plot-series'
})
ret.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
let x = 0
let y = 0
for (let i = 0, bc = 0; i < series.length; i++, bc++) {
let pathTo, pathFrom
let xDivision // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
let yDivision // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
let zeroH // zeroH is the baseline where 0 meets y axis
let zeroW // zeroW is the baseline where 0 meets x axis
let xArrValues = []
let yArrValues = []
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
// el to which series will be drawn
let elSeries = this.graphics.group({
class: `apexcharts-series ${w.globals.seriesNames[realIndex].toString().replace(/ /g, '-')
}`,
'rel': i + 1,
'data:realIndex': realIndex
})
// eldatalabels
let elDataLabelsWrap = this.graphics.group({
class: 'apexcharts-datalabels'
})
let strokeWidth = 0
let barHeight = 0
let barWidth = 0
let initPositions = this.initialPositions(x, y, xDivision, yDivision, zeroH, zeroW)
y = initPositions.y
barHeight = initPositions.barHeight
yDivision = initPositions.yDivision
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
this.yArrj = []
this.yArrjF = []
this.yArrjVal = []
this.xArrj = []
this.xArrjF = []
this.xArrjVal = []
// if (!this.horizontal) {
// this.xArrj.push(x + barWidth / 2)
// }
for (let j = 0; j < w.globals.dataPoints; j++) {
if (w.config.stroke.show) {
if (this.isNullValue) {
strokeWidth = 0
} else {
strokeWidth = Array.isArray(this.strokeWidth) ? this.strokeWidth[realIndex] : this.strokeWidth
}
}
let paths = null
if (this.isHorizontal) {
paths = this.drawBarPaths({
indexes: {i, j, realIndex, bc},
barHeight,
strokeWidth,
pathTo,
pathFrom,
zeroW,
x,
y,
yDivision,
elSeries
})
} else {
paths = this.drawColumnPaths({
indexes: {i, j, realIndex, bc},
x,
y,
xDivision,
pathTo,
pathFrom,
barWidth,
zeroH,
strokeWidth,
elSeries
})
}
pathTo = paths.pathTo
pathFrom = paths.pathFrom
y = paths.y
x = paths.x
xArrValues.push(x)
yArrValues.push(y)
let seriesNumber = w.config.plotOptions.bar.distributed ? j : i
let fillColor = null
if (this.barOptions.colors.ranges.length > 0) {
const colorRange = this.barOptions.colors.ranges
colorRange.map((range, index) => {
if (series[i][j] >= range.from && series[i][j] <= range.to) {
fillColor = range.color
}
})
}
let pathFill = this.fill.fillPath(elSeries, {
seriesNumber: this.barOptions.distributed
? seriesNumber
: realIndex,
color: fillColor
})
elSeries = this.renderSeries({ realIndex, pathFill, j, i, pathFrom, pathTo, strokeWidth, elSeries, x, y, series, barHeight, barWidth, elDataLabelsWrap, type: 'bar', visibleSeries: 0 })
}
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = xArrValues
w.globals.seriesYvalues[realIndex] = yArrValues
// push all current y values array to main PrevY Array
this.prevY.push(this.yArrj)
this.prevYF.push(this.yArrjF)
this.prevYVal.push(this.yArrjVal)
this.prevX.push(this.xArrj)
this.prevXF.push(this.xArrjF)
this.prevXVal.push(this.xArrjVal)
ret.add(elSeries)
}
return ret
}
initialPositions (x, y, xDivision, yDivision, zeroH, zeroW) {
let w = this.w
let barHeight, barWidth
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) / 100
zeroW = this.baseLineInvertedY + w.globals.padHorizontal
// initial y position is half of barHeight * half of number of Bars
y = (yDivision - barHeight) / 2
} else {
// width divided into equal parts
xDivision = w.globals.gridWidth / w.globals.dataPoints
barWidth = xDivision
if (w.globals.isXNumeric) {
// max barwidth should be equal to minXDiff to avoid overlap
xDivision = this.minXDiff / this.xRatio
barWidth = xDivision / this.seriesLen * parseInt(this.barOptions.columnWidth) / 100
} else {
barWidth = (barWidth * parseInt(w.config.plotOptions.bar.columnWidth)) / 100
}
zeroH = this.baseLineY[this.yaxisIndex] + 1
// initial x position is one third of barWidth
x = w.globals.padHorizontal + (xDivision - barWidth) / 2
}
return {
x, y, yDivision, xDivision, barHeight, barWidth, zeroH, zeroW
}
}
drawBarPaths ({
indexes,
barHeight,
strokeWidth,
pathTo,
pathFrom,
zeroW,
x,
y,
yDivision,
elSeries
}) {
let w = this.w
let barYPosition = y
let barXPosition
let i = indexes.i
let j = indexes.j
let realIndex = indexes.realIndex
let bc = indexes.bc
let prevBarW = 0
for (let k = 0; k < this.prevXF.length; k++) {
prevBarW = prevBarW + this.prevXF[k][j]
}
if (i > 0) {
let bXP = zeroW
if (this.prevXVal[i - 1][j] < 0) {
if (this.series[i][j] >= 0) {
bXP = this.prevX[i - 1][j] + prevBarW
} else {
bXP = this.prevX[i - 1][j]
}
} else if (this.prevXVal[i - 1][j] >= 0) {
if (this.series[i][j] >= 0) {
bXP = this.prevX[i - 1][j]
} else {
bXP = this.prevX[i - 1][j] - prevBarW
}
}
barXPosition = bXP
} else {
// the first series will not have prevX values
barXPosition = zeroW
}
if (this.series[i][j] === null) {
x = barXPosition
} else {
x = barXPosition + this.series[i][j] / this.invertedYRatio
}
let endingShapeOpts = {
barHeight,
strokeWidth,
invertedYRatio: this.invertedYRatio,
barYPosition,
x
}
let endingShape = this.bar.barEndingShape(
w,
endingShapeOpts,
this.series,
i,
j
)
if (this.series.length > 1 && i !== this.endingShapeOnSeriesNumber) {
// revert back to flat shape if not last series
endingShape.path = this.graphics.line(
endingShape.newX,
barYPosition + barHeight - strokeWidth
)
}
this.xArrj.push(endingShape.newX)
this.xArrjF.push(Math.abs(barXPosition - endingShape.newX))
this.xArrjVal.push(this.series[i][j])
pathTo = this.graphics.move(barXPosition, barYPosition)
pathFrom = this.graphics.move(barXPosition, barYPosition)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.bar.getPathFrom(realIndex, j, false)
}
pathTo =
pathTo +
this.graphics.line(endingShape.newX, barYPosition) +
endingShape.path +
this.graphics.line(barXPosition, barYPosition + barHeight - strokeWidth) +
this.graphics.line(barXPosition, barYPosition)
pathFrom =
pathFrom +
this.graphics.line(barXPosition, barYPosition) +
this.graphics.line(barXPosition, barYPosition + barHeight - strokeWidth) +
this.graphics.line(barXPosition, barYPosition + barHeight - strokeWidth) +
this.graphics.line(barXPosition, barYPosition + barHeight - strokeWidth) +
this.graphics.line(barXPosition, barYPosition)
if (
w.config.plotOptions.bar.colors.backgroundBarColors.length > 0 &&
i === 0
) {
if (
bc >= w.config.plotOptions.bar.colors.backgroundBarColors.length
) {
bc = 0
}
let bcolor = w.config.plotOptions.bar.colors.backgroundBarColors[bc]
let rect = this.graphics.drawRect(
0,
barYPosition,
w.globals.gridWidth,
barHeight,
0,
bcolor,
w.config.plotOptions.bar.colors.backgroundBarOpacity
)
elSeries.add(rect)
rect.node.classList.add('apexcharts-backgroundBar')
}
y = y + yDivision
return {
pathTo,
pathFrom,
x,
y
}
}
drawColumnPaths ({
indexes,
x,
y,
xDivision,
pathTo,
pathFrom,
barWidth,
zeroH,
strokeWidth,
elSeries
}) {
let w = this.w
let i = indexes.i
let j = indexes.j
let realIndex = indexes.realIndex
let bc = indexes.bc
if (w.globals.isXNumeric) {
let seriesVal = w.globals.seriesX[i][j]
if (!seriesVal) seriesVal = 0
x = ((seriesVal - w.globals.minX) / this.xRatio) - barWidth / 2
}
let barXPosition = x
let barYPosition
let prevBarH = 0
for (let k = 0; k < this.prevYF.length; k++) {
prevBarH = prevBarH + this.prevYF[k][j]
}
if ((i > 0 && !w.globals.isXNumeric) || (i > 0 && w.globals.isXNumeric && w.globals.seriesX[i - 1][j] === w.globals.seriesX[i][j])) {
let bYP
let prevYValue = this.prevY[i - 1][j]
if (this.prevYVal[i - 1][j] < 0) {
if (this.series[i][j] >= 0) {
bYP = prevYValue - prevBarH
} else {
bYP = prevYValue
}
} else {
if (this.series[i][j] >= 0) {
bYP = prevYValue
} else {
bYP = prevYValue + prevBarH
}
}
barYPosition = bYP
} else {
// the first series will not have prevY values, also if the prev index's series X doesn't matches the current index's series X, then start from zero
barYPosition = w.globals.gridHeight - zeroH
}
if (this.series[i][j] === null) {
y = barYPosition - this.series[i][j] / this.yRatio[this.yaxisIndex]
} else {
y = (barYPosition - this.series[i][j] / this.yRatio[this.yaxisIndex])
}
let endingShapeOpts = {
barWidth,
strokeWidth,
yRatio: this.yRatio[this.yaxisIndex],
barXPosition,
y
}
let endingShape = this.bar.barEndingShape(
w,
endingShapeOpts,
this.series,
i,
j
)
if (this.series.length > 1 && i !== this.endingShapeOnSeriesNumber) {
/* if(this.zeroSerieses) {} */
// revert back to flat shape if not last series
endingShape.path = this.graphics.line(
barXPosition + barWidth - strokeWidth,
endingShape.newY
)
}
this.yArrj.push(endingShape.newY)
this.yArrjF.push(Math.abs(barYPosition - endingShape.newY))
this.yArrjVal.push(this.series[i][j])
pathTo = this.graphics.move(barXPosition, barYPosition)
pathFrom = this.graphics.move(barXPosition, barYPosition)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.bar.getPathFrom(realIndex, j, false)
}
pathTo =
pathTo +
this.graphics.line(barXPosition, endingShape.newY) +
endingShape.path +
this.graphics.line(barXPosition + barWidth - strokeWidth, barYPosition) +
this.graphics.line(barXPosition, barYPosition)
pathFrom =
pathFrom +
this.graphics.line(barXPosition, barYPosition) +
this.graphics.line(barXPosition + barWidth - strokeWidth, barYPosition) +
this.graphics.line(barXPosition + barWidth - strokeWidth, barYPosition) +
this.graphics.line(barXPosition + barWidth - strokeWidth, barYPosition) +
this.graphics.line(barXPosition, barYPosition)
if (
w.config.plotOptions.bar.colors.backgroundBarColors.length > 0 &&
i === 0
) {
if (
bc >= w.config.plotOptions.bar.colors.backgroundBarColors.length
) {
bc = 0
}
let bcolor = w.config.plotOptions.bar.colors.backgroundBarColors[bc]
let rect = this.graphics.drawRect(
barXPosition,
0,
barWidth,
w.globals.gridHeight,
0,
bcolor,
w.config.plotOptions.bar.colors.backgroundBarOpacity
)
elSeries.add(rect)
rect.node.classList.add('apexcharts-backgroundBar')
}
x = x + xDivision
return {
pathTo,
pathFrom,
x: w.globals.isXNumeric ? x - xDivision : x,
y
}
}
/*
* When user clicks on legends, the collapsed series will be filled with [0,0,0,...,0]
* We need to make sure, that the last series is not [0,0,0,...,0]
* as we need to draw shapes on the last series (for stacked bars/columns only)
* Hence, we are collecting all inner arrays in series which has [0,0,0...,0]
**/
checkZeroSeries ({series}) {
let w = this.w
for (let zs = 0; zs < series.length; zs++) {
let total = 0
for (
let zsj = 0;
zsj < series[w.globals.maxValsInArrayIndex].length;
zsj++
) {
total += series[zs][zsj]
}
if (total === 0) {
this.zeroSerieses.push(zs)
}
}
// After getting all zeroserieses, we need to ensure whether endingshapeonSeries is not in that zeroseries array
for (let s = series.length - 1; s >= 0; s--) {
if (
this.zeroSerieses.indexOf(s) > -1 &&
s === this.endingShapeOnSeriesNumber
) {
this.endingShapeOnSeriesNumber -= 1
}
}
}
}
export default BarStacked