apexcharts
Version:
A JavaScript Chart Library
827 lines (727 loc) • 23.9 kB
JavaScript
import Fill from '../../../modules/Fill'
import Graphics from '../../../modules/Graphics'
import Series from '../../../modules/Series'
import Utils from '../../../utils/Utils'
export default class Helpers {
constructor(barCtx) {
this.w = barCtx.w
this.barCtx = barCtx
}
initVariables(series) {
const w = this.w
this.barCtx.series = series
this.barCtx.totalItems = 0
this.barCtx.seriesLen = 0
this.barCtx.visibleI = -1 // visible Series
this.barCtx.visibleItems = 1 // number of visible bars after user zoomed in/out
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length > 0) {
this.barCtx.seriesLen = this.barCtx.seriesLen + 1
this.barCtx.totalItems += series[sl].length
}
if (w.globals.isXNumeric) {
// get max visible items
for (let j = 0; j < series[sl].length; j++) {
if (
w.globals.seriesX[sl][j] > w.globals.minX &&
w.globals.seriesX[sl][j] < w.globals.maxX
) {
this.barCtx.visibleItems++
}
}
} else {
this.barCtx.visibleItems = w.globals.dataPoints
}
}
this.arrBorderRadius = this.createBorderRadiusArr(w.globals.series)
if (Utils.isSafari()) {
// https://github.com/apexcharts/apexcharts.js/issues/4996
// to temporarily fix the above issue, border radius is disabled
this.arrBorderRadius = this.arrBorderRadius.map((brArr) =>
brArr.map((_) => 'none')
)
}
if (this.barCtx.seriesLen === 0) {
// A small adjustment when combo charts are used
this.barCtx.seriesLen = 1
}
this.barCtx.zeroSerieses = []
if (!w.globals.comboCharts) {
this.checkZeroSeries({ series })
}
}
initialPositions(realIndex) {
let w = this.w
let x, y, yDivision, xDivision, barHeight, barWidth, zeroH, zeroW
let dataPoints = w.globals.dataPoints
if (this.barCtx.isRangeBar) {
// timeline rangebar chart
dataPoints = w.globals.labels.length
}
let seriesLen = this.barCtx.seriesLen
if (w.config.plotOptions.bar.rangeBarGroupRows) {
seriesLen = 1
}
if (this.barCtx.isHorizontal) {
// height divided into equal parts
yDivision = w.globals.gridHeight / dataPoints
barHeight = yDivision / seriesLen
if (w.globals.isXNumeric) {
yDivision = w.globals.gridHeight / this.barCtx.totalItems
barHeight = yDivision / this.barCtx.seriesLen
}
barHeight =
(barHeight * parseInt(this.barCtx.barOptions.barHeight, 10)) / 100
if (String(this.barCtx.barOptions.barHeight).indexOf('%') === -1) {
barHeight = parseInt(this.barCtx.barOptions.barHeight, 10)
}
zeroW =
this.barCtx.baseLineInvertedY +
w.globals.padHorizontal +
(this.barCtx.isReversed ? w.globals.gridWidth : 0) -
(this.barCtx.isReversed ? this.barCtx.baseLineInvertedY * 2 : 0)
if (this.barCtx.isFunnel) {
zeroW = w.globals.gridWidth / 2
}
y = (yDivision - barHeight * this.barCtx.seriesLen) / 2
} else {
// width divided into equal parts
xDivision = w.globals.gridWidth / this.barCtx.visibleItems
if (w.config.xaxis.convertedCatToNumeric) {
xDivision = w.globals.gridWidth / w.globals.dataPoints
}
barWidth =
((xDivision / seriesLen) *
parseInt(this.barCtx.barOptions.columnWidth, 10)) /
100
if (w.globals.isXNumeric) {
// max barwidth should be equal to minXDiff to avoid overlap
let xRatio = this.barCtx.xRatio
if (
w.globals.minXDiff &&
w.globals.minXDiff !== 0.5 &&
w.globals.minXDiff / xRatio > 0
) {
xDivision = w.globals.minXDiff / xRatio
}
barWidth =
((xDivision / seriesLen) *
parseInt(this.barCtx.barOptions.columnWidth, 10)) /
100
if (barWidth < 1) {
barWidth = 1
}
}
if (String(this.barCtx.barOptions.columnWidth).indexOf('%') === -1) {
barWidth = parseInt(this.barCtx.barOptions.columnWidth, 10)
}
zeroH =
w.globals.gridHeight -
this.barCtx.baseLineY[this.barCtx.translationsIndex] -
(this.barCtx.isReversed ? w.globals.gridHeight : 0) +
(this.barCtx.isReversed
? this.barCtx.baseLineY[this.barCtx.translationsIndex] * 2
: 0)
if (w.globals.isXNumeric) {
const xForNumericX = this.barCtx.getBarXForNumericXAxis({
x,
j: 0,
realIndex,
barWidth,
})
x = xForNumericX.x
} else {
x =
w.globals.padHorizontal +
Utils.noExponents(xDivision - barWidth * this.barCtx.seriesLen) / 2
}
}
w.globals.barHeight = barHeight
w.globals.barWidth = barWidth
return {
x,
y,
yDivision,
xDivision,
barHeight,
barWidth,
zeroH,
zeroW,
}
}
initializeStackedPrevVars(ctx) {
const w = ctx.w
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
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 = []
})
}
getPathFillColor(series, i, j, realIndex) {
const w = this.w
let fill = this.barCtx.ctx.fill
let fillColor = null
let seriesNumber = this.barCtx.barOptions.distributed ? j : i
let useRangeColor = false
if (this.barCtx.barOptions.colors.ranges.length > 0) {
const colorRange = this.barCtx.barOptions.colors.ranges
colorRange.map((range) => {
if (series[i][j] >= range.from && series[i][j] <= range.to) {
fillColor = range.color
useRangeColor = true
}
})
}
let pathFill = fill.fillPath({
seriesNumber: this.barCtx.barOptions.distributed
? seriesNumber
: realIndex,
dataPointIndex: j,
color: fillColor,
value: series[i][j],
fillConfig: w.config.series[i].data[j]?.fill,
fillType: w.config.series[i].data[j]?.fill?.type
? w.config.series[i].data[j]?.fill.type
: Array.isArray(w.config.fill.type)
? w.config.fill.type[realIndex]
: w.config.fill.type,
})
return {
color: pathFill,
useRangeColor,
}
}
getStrokeWidth(i, j, realIndex) {
let strokeWidth = 0
const w = this.w
if (
typeof this.barCtx.series[i][j] === 'undefined' ||
this.barCtx.series[i][j] === null
) {
this.barCtx.isNullValue = true
} else {
this.barCtx.isNullValue = false
}
if (w.config.stroke.show) {
if (!this.barCtx.isNullValue) {
strokeWidth = Array.isArray(this.barCtx.strokeWidth)
? this.barCtx.strokeWidth[realIndex]
: this.barCtx.strokeWidth
}
}
return strokeWidth
}
createBorderRadiusArr(series) {
const w = this.w
const alwaysApplyRadius =
!this.w.config.chart.stacked || w.config.plotOptions.bar.borderRadius <= 0
const numSeries = series.length
const numColumns = series[0]?.length | 0
const output = Array.from({ length: numSeries }, () =>
Array(numColumns).fill(alwaysApplyRadius ? 'top' : 'none')
)
if (alwaysApplyRadius) return output
for (let j = 0; j < numColumns; j++) {
let positiveIndices = []
let negativeIndices = []
let nonZeroCount = 0
// Collect positive and negative indices
for (let i = 0; i < numSeries; i++) {
const value = series[i][j]
if (value > 0) {
positiveIndices.push(i)
nonZeroCount++
} else if (value < 0) {
negativeIndices.push(i)
nonZeroCount++
}
}
if (positiveIndices.length > 0 && negativeIndices.length === 0) {
// Only positive values in this column
if (positiveIndices.length === 1) {
// Single positive value
output[positiveIndices[0]][j] = 'both'
} else {
// Multiple positive values
const firstPositiveIndex = positiveIndices[0]
const lastPositiveIndex = positiveIndices[positiveIndices.length - 1]
for (let i of positiveIndices) {
if (i === firstPositiveIndex) {
output[i][j] = 'bottom'
} else if (i === lastPositiveIndex) {
output[i][j] = 'top'
} else {
output[i][j] = 'none'
}
}
}
} else if (negativeIndices.length > 0 && positiveIndices.length === 0) {
// Only negative values in this column
if (negativeIndices.length === 1) {
// Single negative value
output[negativeIndices[0]][j] = 'both'
} else {
// Multiple negative values
const highestNegativeIndex = Math.max(...negativeIndices)
const lowestNegativeIndex = Math.min(...negativeIndices)
for (let i of negativeIndices) {
if (i === highestNegativeIndex) {
output[i][j] = 'bottom' // Closest to axis
} else if (i === lowestNegativeIndex) {
output[i][j] = 'top' // Farthest from axis
} else {
output[i][j] = 'none'
}
}
}
} else if (positiveIndices.length > 0 && negativeIndices.length > 0) {
// Mixed positive and negative values
// Assign 'top' to the last positive bar
const lastPositiveIndex = positiveIndices[positiveIndices.length - 1]
for (let i of positiveIndices) {
if (i === lastPositiveIndex) {
output[i][j] = 'top'
} else {
output[i][j] = 'none'
}
}
// Assign 'bottom' to the highest negative index (closest to axis)
const highestNegativeIndex = Math.max(...negativeIndices)
for (let i of negativeIndices) {
if (i === highestNegativeIndex) {
output[i][j] = 'bottom'
} else {
output[i][j] = 'none'
}
}
} else if (nonZeroCount === 1) {
// Only one non-zero value (either positive or negative)
const index = positiveIndices[0] || negativeIndices[0]
output[index][j] = 'both'
}
}
return output
}
barBackground({ j, i, x1, x2, y1, y2, elSeries }) {
const w = this.w
const graphics = new Graphics(this.barCtx.ctx)
const sr = new Series(this.barCtx.ctx)
let activeSeriesIndex = sr.getActiveConfigSeriesIndex()
if (
this.barCtx.barOptions.colors.backgroundBarColors.length > 0 &&
activeSeriesIndex === i
) {
if (j >= this.barCtx.barOptions.colors.backgroundBarColors.length) {
j %= this.barCtx.barOptions.colors.backgroundBarColors.length
}
let bcolor = this.barCtx.barOptions.colors.backgroundBarColors[j]
let rect = graphics.drawRect(
typeof x1 !== 'undefined' ? x1 : 0,
typeof y1 !== 'undefined' ? y1 : 0,
typeof x2 !== 'undefined' ? x2 : w.globals.gridWidth,
typeof y2 !== 'undefined' ? y2 : w.globals.gridHeight,
this.barCtx.barOptions.colors.backgroundBarRadius,
bcolor,
this.barCtx.barOptions.colors.backgroundBarOpacity
)
elSeries.add(rect)
rect.node.classList.add('apexcharts-backgroundBar')
}
}
getColumnPaths({
barWidth,
barXPosition,
y1,
y2,
strokeWidth,
isReversed,
series,
seriesGroup,
realIndex,
i,
j,
w,
}) {
const graphics = new Graphics(this.barCtx.ctx)
strokeWidth = Array.isArray(strokeWidth)
? strokeWidth[realIndex]
: strokeWidth
if (!strokeWidth) strokeWidth = 0
let bW = barWidth
let bXP = barXPosition
if (w.config.series[realIndex].data[j]?.columnWidthOffset) {
bXP =
barXPosition - w.config.series[realIndex].data[j].columnWidthOffset / 2
bW = barWidth + w.config.series[realIndex].data[j].columnWidthOffset
}
// Center the stroke on the coordinates
let strokeCenter = strokeWidth / 2
const x1 = bXP + strokeCenter
const x2 = bXP + bW - strokeCenter
let direction = (series[i][j] >= 0 ? 1 : -1) * (isReversed ? -1 : 1)
// append tiny pixels to avoid exponentials (which cause issues in border-radius)
y1 += 0.001 - strokeCenter * direction
y2 += 0.001 + strokeCenter * direction
let pathTo = graphics.move(x1, y1)
let pathFrom = graphics.move(x1, y1)
const sl = graphics.line(x2, y1)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, false)
}
pathTo =
pathTo +
graphics.line(x1, y2) +
graphics.line(x2, y2) +
sl +
(w.config.plotOptions.bar.borderRadiusApplication === 'around' ||
this.arrBorderRadius[realIndex][j] === 'both'
? ' Z'
: ' z')
// the lines in pathFrom are repeated to equal it to the points of pathTo
// this is to avoid weird animation (bug in svg.js)
pathFrom =
pathFrom +
graphics.line(x1, y1) +
sl +
sl +
sl +
sl +
sl +
graphics.line(x1, y1) +
(w.config.plotOptions.bar.borderRadiusApplication === 'around' ||
this.arrBorderRadius[realIndex][j] === 'both'
? ' Z'
: ' z')
if (this.arrBorderRadius[realIndex][j] !== 'none') {
pathTo = graphics.roundPathCorners(
pathTo,
w.config.plotOptions.bar.borderRadius
)
}
if (w.config.chart.stacked) {
let _ctx = this.barCtx
_ctx = this.barCtx[seriesGroup]
_ctx.yArrj.push(y2 - strokeCenter * direction)
_ctx.yArrjF.push(Math.abs(y1 - y2 + strokeWidth * direction))
_ctx.yArrjVal.push(this.barCtx.series[i][j])
}
return {
pathTo,
pathFrom,
}
}
getBarpaths({
barYPosition,
barHeight,
x1,
x2,
strokeWidth,
isReversed,
series,
seriesGroup,
realIndex,
i,
j,
w,
}) {
const graphics = new Graphics(this.barCtx.ctx)
strokeWidth = Array.isArray(strokeWidth)
? strokeWidth[realIndex]
: strokeWidth
if (!strokeWidth) strokeWidth = 0
let bYP = barYPosition
let bH = barHeight
if (w.config.series[realIndex].data[j]?.barHeightOffset) {
bYP =
barYPosition - w.config.series[realIndex].data[j].barHeightOffset / 2
bH = barHeight + w.config.series[realIndex].data[j].barHeightOffset
}
// Center the stroke on the coordinates
let strokeCenter = strokeWidth / 2
const y1 = bYP + strokeCenter
const y2 = bYP + bH - strokeCenter
let direction = (series[i][j] >= 0 ? 1 : -1) * (isReversed ? -1 : 1)
// append tiny pixels to avoid exponentials (which cause issues in border-radius)
x1 += 0.001 + strokeCenter * direction
x2 += 0.001 - strokeCenter * direction
let pathTo = graphics.move(x1, y1)
let pathFrom = graphics.move(x1, y1)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, false)
}
const sl = graphics.line(x1, y2)
pathTo =
pathTo +
graphics.line(x2, y1) +
graphics.line(x2, y2) +
sl +
(w.config.plotOptions.bar.borderRadiusApplication === 'around' ||
this.arrBorderRadius[realIndex][j] === 'both'
? ' Z'
: ' z')
pathFrom =
pathFrom +
graphics.line(x1, y1) +
sl +
sl +
sl +
sl +
sl +
graphics.line(x1, y1) +
(w.config.plotOptions.bar.borderRadiusApplication === 'around' ||
this.arrBorderRadius[realIndex][j] === 'both'
? ' Z'
: ' z')
if (this.arrBorderRadius[realIndex][j] !== 'none') {
pathTo = graphics.roundPathCorners(
pathTo,
w.config.plotOptions.bar.borderRadius
)
}
if (w.config.chart.stacked) {
let _ctx = this.barCtx
_ctx = this.barCtx[seriesGroup]
_ctx.xArrj.push(x2 + strokeCenter * direction)
_ctx.xArrjF.push(Math.abs(x1 - x2 - strokeWidth * direction))
_ctx.xArrjVal.push(this.barCtx.series[i][j])
}
return {
pathTo,
pathFrom,
}
}
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.barCtx.zeroSerieses.push(zs)
}
}
}
getXForValue(value, zeroW, zeroPositionForNull = true) {
let xForVal = zeroPositionForNull ? zeroW : null
if (typeof value !== 'undefined' && value !== null) {
xForVal =
zeroW +
value / this.barCtx.invertedYRatio -
(this.barCtx.isReversed ? value / this.barCtx.invertedYRatio : 0) * 2
}
return xForVal
}
getYForValue(value, zeroH, translationsIndex, zeroPositionForNull = true) {
let yForVal = zeroPositionForNull ? zeroH : null
if (typeof value !== 'undefined' && value !== null) {
yForVal =
zeroH -
value / this.barCtx.yRatio[translationsIndex] +
(this.barCtx.isReversed
? value / this.barCtx.yRatio[translationsIndex]
: 0) *
2
}
return yForVal
}
getGoalValues(type, zeroW, zeroH, i, j, translationsIndex) {
const w = this.w
let goals = []
const pushGoal = (value, attrs) => {
goals.push({
[type]:
type === 'x'
? this.getXForValue(value, zeroW, false)
: this.getYForValue(value, zeroH, translationsIndex, false),
attrs,
})
}
if (
w.globals.seriesGoals[i] &&
w.globals.seriesGoals[i][j] &&
Array.isArray(w.globals.seriesGoals[i][j])
) {
w.globals.seriesGoals[i][j].forEach((goal) => {
pushGoal(goal.value, goal)
})
}
if (this.barCtx.barOptions.isDumbbell && w.globals.seriesRange.length) {
let colors = this.barCtx.barOptions.dumbbellColors
? this.barCtx.barOptions.dumbbellColors
: w.globals.colors
const commonAttrs = {
strokeHeight: type === 'x' ? 0 : w.globals.markers.size[i],
strokeWidth: type === 'x' ? w.globals.markers.size[i] : 0,
strokeDashArray: 0,
strokeLineCap: 'round',
strokeColor: Array.isArray(colors[i]) ? colors[i][0] : colors[i],
}
pushGoal(w.globals.seriesRangeStart[i][j], commonAttrs)
pushGoal(w.globals.seriesRangeEnd[i][j], {
...commonAttrs,
strokeColor: Array.isArray(colors[i]) ? colors[i][1] : colors[i],
})
}
return goals
}
drawGoalLine({
barXPosition,
barYPosition,
goalX,
goalY,
barWidth,
barHeight,
}) {
let graphics = new Graphics(this.barCtx.ctx)
const lineGroup = graphics.group({
className: 'apexcharts-bar-goals-groups',
})
lineGroup.node.classList.add('apexcharts-element-hidden')
this.barCtx.w.globals.delayedElements.push({
el: lineGroup.node,
})
lineGroup.attr(
'clip-path',
`url(#gridRectMarkerMask${this.barCtx.w.globals.cuid})`
)
let line = null
if (this.barCtx.isHorizontal) {
if (Array.isArray(goalX)) {
goalX.forEach((goal) => {
// Need a tiny margin of 1 each side so goals don't disappear at extremeties
if (goal.x >= -1 && goal.x <= graphics.w.globals.gridWidth + 1) {
let sHeight =
typeof goal.attrs.strokeHeight !== 'undefined'
? goal.attrs.strokeHeight
: barHeight / 2
let y = barYPosition + sHeight + barHeight / 2
line = graphics.drawLine(
goal.x,
y - sHeight * 2,
goal.x,
y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : undefined,
goal.attrs.strokeDashArray,
goal.attrs.strokeWidth ? goal.attrs.strokeWidth : 2,
goal.attrs.strokeLineCap
)
lineGroup.add(line)
}
})
}
} else {
if (Array.isArray(goalY)) {
goalY.forEach((goal) => {
// Need a tiny margin of 1 each side so goals don't disappear at extremeties
if (goal.y >= -1 && goal.y <= graphics.w.globals.gridHeight + 1) {
let sWidth =
typeof goal.attrs.strokeWidth !== 'undefined'
? goal.attrs.strokeWidth
: barWidth / 2
let x = barXPosition + sWidth + barWidth / 2
line = graphics.drawLine(
x - sWidth * 2,
goal.y,
x,
goal.y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : undefined,
goal.attrs.strokeDashArray,
goal.attrs.strokeHeight ? goal.attrs.strokeHeight : 2,
goal.attrs.strokeLineCap
)
lineGroup.add(line)
}
})
}
}
return lineGroup
}
drawBarShadow({ prevPaths, currPaths, color }) {
const w = this.w
const { x: prevX2, x1: prevX1, barYPosition: prevY1 } = prevPaths
const { x: currX2, x1: currX1, barYPosition: currY1 } = currPaths
const prevY2 = prevY1 + currPaths.barHeight
const graphics = new Graphics(this.barCtx.ctx)
const utils = new Utils()
const shadowPath =
graphics.move(prevX1, prevY2) +
graphics.line(prevX2, prevY2) +
graphics.line(currX2, currY1) +
graphics.line(currX1, currY1) +
graphics.line(prevX1, prevY2) +
(w.config.plotOptions.bar.borderRadiusApplication === 'around' ||
this.arrBorderRadius[realIndex][j] === 'both'
? ' Z'
: ' z')
return graphics.drawPath({
d: shadowPath,
fill: utils.shadeColor(0.5, Utils.rgb2hex(color)),
stroke: 'none',
strokeWidth: 0,
fillOpacity: 1,
classes: 'apexcharts-bar-shadow apexcharts-decoration-element',
})
}
getZeroValueEncounters({ i, j }) {
const w = this.w
let nonZeroColumns = 0
let zeroEncounters = 0
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 && val === 0) {
zeroEncounters++
}
})
return {
nonZeroColumns,
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 }
}
}