apexcharts
Version:
A JavaScript Chart Library
944 lines (864 loc) • 30.3 kB
JavaScript
// @ts-check
import Graphics from '../../../modules/Graphics'
import DataLabels from '../../../modules/DataLabels'
import { resolveDataLabelOffset } from '../../../modules/helpers/DataLabelOffset'
import { datumKey } from '../../../modules/animations/LengthTransition'
export default class BarDataLabels {
/**
* @param {import('../../../charts/Bar').default} barCtx
*/
constructor(barCtx) {
this.w = barCtx.w
this.barCtx = barCtx
this.totalFormatter =
this.w.config.plotOptions.bar.dataLabels.total.formatter
if (!this.totalFormatter) {
this.totalFormatter = this.w.config.dataLabels.formatter
}
}
/** handleBarDataLabels is used to calculate the positions for the data-labels
* It also sets the element's data attr for bars and calls drawCalculatedBarDataLabels()
* After calculating, it also calls the function to draw data labels
* @memberof Bar
* @param {Record<string, any>} opts - bar properties used throughout the bar drawing function
* @return {object} dataLabels node-element which you can append later
**/
handleBarDataLabels(opts) {
const {
x,
y,
y1,
y2,
i,
j,
realIndex,
columnGroupIndex,
series,
barHeight,
barWidth,
barXPosition,
barYPosition,
visibleSeries,
} = opts
const w = this.w
const graphics = new Graphics(this.barCtx.w)
const strokeWidth = Array.isArray(this.barCtx.strokeWidth)
? this.barCtx.strokeWidth[realIndex]
: this.barCtx.strokeWidth
let bcx
let bcy
if (w.axisFlags.isXNumeric && !w.globals.isBarHorizontal) {
bcx = x + barWidth * (visibleSeries + 1)
bcy = y + barHeight * (visibleSeries + 1) - strokeWidth
} else {
bcx = x + barWidth * visibleSeries
bcy = y + barHeight * visibleSeries
}
let dataLabels = null
let totalDataLabels = null
let dataLabelsX = x
let dataLabelsY = y
let dataLabelsPos = /** @type {any} */ ({})
const dataLabelsConfig = w.config.dataLabels
const barDataLabelsConfig = this.barCtx.barOptions.dataLabels
const barTotalDataLabelsConfig = this.barCtx.barOptions.dataLabels.total
if (
typeof barYPosition !== 'undefined' &&
(this.barCtx.isRangeBar || this.barCtx.isPyramid)
) {
// Both rangeBar and pyramid have per-segment y/height that doesn't
// follow the equal-step `yDivision` cadence the default bar
// dataLabelsY formula assumes. Anchor positioning at the actual
// barYPosition (segment top) instead.
bcy = barYPosition
dataLabelsY = barYPosition
}
if (
typeof barXPosition !== 'undefined' &&
this.barCtx.isVerticalGroupedRangeBar
) {
bcx = barXPosition
dataLabelsX = barXPosition
}
// offsets may be a function evaluated per data point, so labels of a
// series with few records can be nudged individually to avoid overlapping
const offX = resolveDataLabelOffset(
dataLabelsConfig.offsetX,
w,
realIndex,
j,
)
const offY = resolveDataLabelOffset(
dataLabelsConfig.offsetY,
w,
realIndex,
j,
)
let textRects = {
width: 0,
height: 0,
}
if (w.config.dataLabels.enabled) {
// Use realIndex (the full-config series index), not the local loop index
// `i` which is the compacted bar-subset index in a combo chart. Otherwise
// the label is measured/formatted from a different series than the value
// actually drawn (drawCalculatedDataLabels below uses realIndex).
const yLabel = w.seriesData.series[realIndex][j]
// Measure in the SAME font the label will be drawn in. Data labels
// default to fontWeight 600, and the default measurement is taken at
// 'regular', which under-reports the width by 3-7% on these strings. That
// shortfall is not cosmetic: every overflow clamp below is driven by
// `textRects.width`, so an under-measured label is clamped to a position
// that still runs off the chart.
textRects = graphics.getTextRects(
w.config.dataLabels.formatter
? w.config.dataLabels.formatter(yLabel, {
...w,
seriesIndex: realIndex,
dataPointIndex: j,
w,
})
: w.formatters.yLabelFormatters[0](yLabel),
parseFloat(dataLabelsConfig.style.fontSize).toString(),
dataLabelsConfig.style.fontFamily,
undefined,
true,
dataLabelsConfig.style.fontWeight,
)
}
const params = {
x,
y,
i,
j,
realIndex,
columnGroupIndex,
bcx,
bcy,
barHeight,
barWidth,
textRects,
strokeWidth,
dataLabelsX,
dataLabelsY,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
offX,
offY,
}
if (this.barCtx.isHorizontal) {
dataLabelsPos = this.calculateBarsDataLabelsPosition(params)
} else {
dataLabelsPos = this.calculateColumnsDataLabelsPosition(params)
}
dataLabels = this.drawCalculatedDataLabels({
x: dataLabelsPos.dataLabelsX,
y: dataLabelsPos.dataLabelsY,
val: this.barCtx.isRangeBar
? [y1, y2]
: w.config.chart.stackType === '100%'
? series[realIndex][j]
: w.seriesData.series[realIndex][j],
i: realIndex,
j,
barWidth,
barHeight,
textRects,
dataLabelsConfig,
})
if (w.config.chart.stacked && barTotalDataLabelsConfig.enabled) {
totalDataLabels = this.drawTotalDataLabels({
x: dataLabelsPos.totalDataLabelsX,
y: dataLabelsPos.totalDataLabelsY,
barWidth,
barHeight,
realIndex,
j,
textAnchor: dataLabelsPos.totalDataLabelsAnchor,
val: this.getStackedTotalDataLabel({ realIndex, j }),
rawVal: this.getStackedTotalValue({ realIndex, j }),
dataLabelsConfig,
barTotalDataLabelsConfig,
})
}
return {
dataLabelsPos,
dataLabels,
totalDataLabels,
}
}
/**
* True when this chart stacks in more than one group, so totals have to be
* resolved per group rather than across the whole data point. A single-group
* chart keeps every old code path exactly as it was. See #4173.
*/
hasMultipleSeriesGroups() {
return this.w.labelData.seriesGroups.length > 1
}
/**
* The series group `realIndex` belongs to, and that group's own stacking
* state, or null when totals are chart-wide (the single-group case).
* @param {number} realIndex
* @returns {{groupIndex: number, group: string[]} | null}
*/
getTotalGroupContext(realIndex) {
if (!this.hasMultipleSeriesGroups()) return null
const groupIndex = this.barCtx.barHelpers.getSeriesGroupIndex(realIndex)
if (groupIndex < 0) return null
return { groupIndex, group: this.w.labelData.seriesGroups[groupIndex] }
}
/**
* Whether `realIndex` is the series that should draw the stacked total.
*
* That is the series capping the stack. With grouped stacks each group has
* its own cap, so gating on the chart-wide `lastActiveBarSerieIndex` drew a
* single total for the last group only. See #4173.
* @param {number} realIndex
*/
drawsStackedTotal(realIndex) {
const byGroup = this.barCtx.lastActiveBarSerieIndexByGroup
const ctx = this.getTotalGroupContext(realIndex)
if (ctx && byGroup && byGroup.length > ctx.groupIndex) {
return byGroup[ctx.groupIndex] === realIndex
}
return this.barCtx.lastActiveBarSerieIndex === realIndex
}
/**
* The raw (unformatted) stacked total at this data point. Split out of
* getStackedTotalDataLabel so the label transition can count the total up
* from its previous number and re-run the formatter itself each frame.
* @param {{realIndex: any, j: any}} opts
*/
getStackedTotalValue({ realIndex, j }) {
const w = this.w
// With grouped stacks the total is the sum of this group's series only;
// `stackedSeriesTotals` sums every series at this data point, which mixed
// unrelated groups into one number. See #4173.
const ctx = this.getTotalGroupContext(realIndex)
const byGroups = w.seriesData.stackedSeriesTotalsByGroups
return ctx && byGroups && byGroups[ctx.groupIndex]
? byGroups[ctx.groupIndex][j]
: this.barCtx.stackedSeriesTotals[j]
}
/** @param {{realIndex: any, j: any}} opts */
getStackedTotalDataLabel({ realIndex, j }) {
const w = this.w
let val = this.getStackedTotalValue({ realIndex, j })
if (this.totalFormatter) {
val = this.totalFormatter(val, {
...w,
seriesIndex: realIndex,
dataPointIndex: j,
w,
})
}
return val
}
/**
* @param {Record<string, any>} opts
*/
calculateColumnsDataLabelsPosition(opts) {
const w = this.w
let {
i,
j,
realIndex,
y,
bcx,
barWidth,
barHeight,
textRects,
dataLabelsX,
dataLabelsY,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
strokeWidth,
offX,
offY,
} = opts
let totalDataLabelsY
let totalDataLabelsX
const totalDataLabelsAnchor = 'middle'
const totalDataLabelsBcx = bcx
barHeight = Math.abs(barHeight)
const vertical =
w.config.plotOptions.bar.dataLabels.orientation === 'vertical'
const { zeroEncounters } = this.barCtx.barHelpers.getZeroValueEncounters({
i,
j,
})
bcx = bcx - strokeWidth / 2
const dataPointsDividedWidth = w.layout.gridWidth / w.globals.dataPoints
if (this.barCtx.isVerticalGroupedRangeBar) {
dataLabelsX += barWidth / 2
} else {
if (w.axisFlags.isXNumeric) {
dataLabelsX = bcx - barWidth / 2 + offX
} else {
dataLabelsX = bcx - dataPointsDividedWidth + barWidth / 2 + offX
}
if (
!w.config.chart.stacked &&
zeroEncounters > 0 &&
w.config.plotOptions.bar.hideZeroBarsWhenGrouped
) {
dataLabelsX -= barWidth * zeroEncounters
}
}
if (vertical) {
const offsetDLX = 2
dataLabelsX =
dataLabelsX + textRects.height / 2 - strokeWidth / 2 - offsetDLX
}
const valIsNegative = w.seriesData.series[i][j] < 0
let newY = y
if (this.barCtx.isReversed) {
newY = y + (valIsNegative ? barHeight : -barHeight)
}
switch (barDataLabelsConfig.position) {
case 'center':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY - barHeight / 2 + offY
} else {
dataLabelsY = newY + barHeight / 2 - offY
}
} else {
if (valIsNegative) {
dataLabelsY = newY - barHeight / 2 + textRects.height / 2 + offY
} else {
dataLabelsY = newY + barHeight / 2 + textRects.height / 2 - offY
}
}
break
case 'bottom':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY - barHeight + offY
} else {
dataLabelsY = newY + barHeight - offY
}
} else {
if (valIsNegative) {
dataLabelsY =
newY - barHeight + textRects.height + strokeWidth + offY
} else {
dataLabelsY =
newY + barHeight - textRects.height / 2 + strokeWidth - offY
}
}
break
case 'top':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY + offY
} else {
dataLabelsY = newY - offY
}
} else {
if (valIsNegative) {
dataLabelsY = newY - textRects.height / 2 - offY
} else {
dataLabelsY = newY + textRects.height + offY
}
}
break
}
let lowestPrevY = newY
// Only this series' own group: scanning every group took the extremum
// across the whole cluster, so a group's total floated above the tallest
// *other* group's stack instead of its own. See #4173.
const totalGroupCtx = this.getTotalGroupContext(realIndex)
const prevYGroups = totalGroupCtx
? [totalGroupCtx.group]
: w.labelData.seriesGroups
/**
* @param {string[]} sg
*/
prevYGroups.forEach((/** @type {any} */ sg) => {
/**
* @param {any[]} arr
*/
;/** @type {any} */ (this.barCtx)[sg.join(',')]?.prevY.forEach(
(/** @type {any} */ arr) => {
if (valIsNegative) {
lowestPrevY = Math.max(arr[j], lowestPrevY)
} else {
lowestPrevY = Math.min(arr[j], lowestPrevY)
}
},
)
})
if (this.drawsStackedTotal(realIndex) && barTotalDataLabelsConfig.enabled) {
const ADDITIONAL_OFFY = 18
const graphics = new Graphics(this.barCtx.w)
const totalLabeltextRects = graphics.getTextRects(
this.getStackedTotalDataLabel({ realIndex, j }),
dataLabelsConfig.fontSize,
)
if (valIsNegative) {
totalDataLabelsY =
lowestPrevY -
totalLabeltextRects.height / 2 -
offY -
barTotalDataLabelsConfig.offsetY +
ADDITIONAL_OFFY
} else {
totalDataLabelsY =
lowestPrevY +
totalLabeltextRects.height +
offY +
barTotalDataLabelsConfig.offsetY -
ADDITIONAL_OFFY
}
// width divided into equal parts
const xDivision = dataPointsDividedWidth
// Centre the total over the bar it totals. `totalDataLabelsBcx` is
// already this series' bar position, so this is the same centring
// `dataLabelsX` uses, with the total's own offsetX.
//
// For a single group this is algebraically identical to the previous
// barGroups.length-based expression (which collapses to
// `bcx + barWidth / 2 - xDivision` when the length is 1), so ungrouped
// charts are unaffected; with several groups it now tracks each group's
// bar instead of the centre of the whole cluster. See #4173.
totalDataLabelsX =
totalDataLabelsBcx +
(w.axisFlags.isXNumeric
? -barWidth / 2
: barWidth / 2 - xDivision) +
barTotalDataLabelsConfig.offsetX
}
if (!w.config.chart.stacked) {
if (dataLabelsY < 0) {
dataLabelsY = 0 + strokeWidth
} else if (dataLabelsY + textRects.height / 3 > w.layout.gridHeight) {
dataLabelsY = w.layout.gridHeight - strokeWidth
}
}
return {
bcx,
bcy: y,
dataLabelsX,
dataLabelsY,
totalDataLabelsX,
totalDataLabelsY,
totalDataLabelsAnchor,
}
}
/**
* @param {Record<string, any>} opts
*/
calculateBarsDataLabelsPosition(opts) {
const w = this.w
let {
x,
i,
j,
realIndex,
bcy,
barHeight,
barWidth,
textRects,
dataLabelsX,
strokeWidth,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
offX,
offY,
} = opts
const dataPointsDividedHeight = w.layout.gridHeight / w.globals.dataPoints
const { zeroEncounters } = this.barCtx.barHelpers.getZeroValueEncounters({
i,
j,
})
barWidth = Math.abs(barWidth)
let dataLabelsY
if (this.barCtx.isPyramid) {
// For pyramid we want the *visual* center of the rendered text to
// sit on the segment's vertical center. `textRects.centerOffset`
// (measured live by Graphics.getTextRects) is the signed distance
// from the SVG `y` attribute (alphabetic baseline) to the bbox
// center for THIS exact font/size/family — negative for typical
// fonts where the ascender dominates the descender. Subtracting it
// lands the bbox center exactly on segment center, no matter the
// font.
const centerOffset = textRects.centerOffset ?? 0
dataLabelsY = bcy + barHeight / 2 + offY - centerOffset
} else {
dataLabelsY =
bcy -
(this.barCtx.isRangeBar ? 0 : dataPointsDividedHeight) +
barHeight / 2 +
textRects.height / 2 +
offY -
3
}
if (
!w.config.chart.stacked &&
zeroEncounters > 0 &&
w.config.plotOptions.bar.hideZeroBarsWhenGrouped
) {
dataLabelsY -= barHeight * zeroEncounters
}
let totalDataLabelsX
let totalDataLabelsY
let totalDataLabelsAnchor = 'start'
const valIsNegative = w.seriesData.series[i][j] < 0
let newX = x
if (this.barCtx.isReversed) {
newX = x + (valIsNegative ? -barWidth : barWidth)
totalDataLabelsAnchor = valIsNegative ? 'start' : 'end'
}
if (this.barCtx.isPyramid) {
// Pyramid segments are horizontally centered on the plot area's
// midline (their geometry is a triangle expanding outward from
// gridWidth/2). The standard horizontal-bar formulas anchor x to
// the segment's right edge minus a value-proportional barWidth,
// which only matches normal left-rooted bars — for pyramid that
// produces a left-shifted label. Just center on the midline.
dataLabelsX = w.layout.gridWidth / 2 + offX
} else {
switch (barDataLabelsConfig.position) {
case 'center':
if (valIsNegative) {
dataLabelsX = newX + barWidth / 2 - offX
} else {
dataLabelsX =
Math.max(textRects.width / 2, newX - barWidth / 2) + offX
}
break
case 'bottom':
if (valIsNegative) {
dataLabelsX = newX + barWidth - strokeWidth - offX
} else {
dataLabelsX = newX - barWidth + strokeWidth + offX
}
break
case 'top':
if (valIsNegative) {
dataLabelsX = newX - strokeWidth - offX
} else {
dataLabelsX = newX - strokeWidth + offX
}
break
}
}
let lowestPrevX = newX
// This series' own group only — see the matching note in the column path.
const totalGroupCtx = this.getTotalGroupContext(realIndex)
const prevXGroups = totalGroupCtx
? [totalGroupCtx.group]
: w.labelData.seriesGroups
/**
* @param {string[]} sg
*/
prevXGroups.forEach((/** @type {any} */ sg) => {
/**
* @param {any[]} arr
*/
;/** @type {any} */ (this.barCtx)[sg.join(',')]?.prevX.forEach(
(/** @type {any} */ arr) => {
if (valIsNegative) {
lowestPrevX = Math.min(arr[j], lowestPrevX)
} else {
lowestPrevX = Math.max(arr[j], lowestPrevX)
}
},
)
})
if (this.drawsStackedTotal(realIndex) && barTotalDataLabelsConfig.enabled) {
const graphics = new Graphics(this.barCtx.w)
const totalLabeltextRects = graphics.getTextRects(
this.getStackedTotalDataLabel({ realIndex, j }),
dataLabelsConfig.fontSize,
)
if (valIsNegative) {
totalDataLabelsX =
lowestPrevX - strokeWidth - offX - barTotalDataLabelsConfig.offsetX
totalDataLabelsAnchor = 'end'
} else {
totalDataLabelsX =
lowestPrevX +
offX +
barTotalDataLabelsConfig.offsetX +
(this.barCtx.isReversed ? -(barWidth + strokeWidth) : strokeWidth)
}
totalDataLabelsY =
dataLabelsY -
textRects.height / 2 +
totalLabeltextRects.height / 2 +
barTotalDataLabelsConfig.offsetY +
strokeWidth
// Recentre across the cluster only when one label stands for every
// group. With a label per group each already sits on its own bar's
// `dataLabelsY`, so shifting by the cluster height would drag it off the
// bar it belongs to. See #4173.
if (w.globals.barGroups.length > 1 && !totalGroupCtx) {
totalDataLabelsY =
totalDataLabelsY - (w.globals.barGroups.length / 2) * (barHeight / 2)
}
}
if (!w.config.chart.stacked) {
// Keep the label inside the plot area.
//
// This has to reason about the anchor the label will RENDER with, not the
// configured one. `drawCalculatedDataLabels` swaps start<->end for
// negative values on a horizontal bar, so a `textAnchor:'end'` label on a
// negative series actually grows to the RIGHT of `dataLabelsX`. The
// previous version branched on the configured anchor and so guarded the
// opposite edge to the one the text was about to cross, which let long
// labels run clean off the chart (a `position:'center'` +
// `textAnchor:'end'` label on a negative series was clipped by the SVG
// edge, and its `textAnchor:'start'` mirror escaped to the left).
const flipped =
valIsNegative && dataLabelsConfig.textAnchor !== 'middle'
? dataLabelsConfig.textAnchor === 'start'
? 'end'
: 'start'
: dataLabelsConfig.textAnchor
// How much of the text sits either side of `dataLabelsX`. A vertical
// label is rotated about that point, so it is the text HEIGHT that spans
// horizontally, half to each side, whatever the anchor.
let spanLeft
if (barDataLabelsConfig.orientation === 'vertical') {
spanLeft = textRects.height / 2
} else if (flipped === 'end') {
spanLeft = textRects.width
} else if (flipped === 'middle') {
spanLeft = textRects.width / 2
} else {
spanLeft = 0
}
const span =
barDataLabelsConfig.orientation === 'vertical'
? textRects.height
: textRects.width
const spanRight = span - spanLeft
// Only shift a label that does not fit; one wider than the plot area
// cannot be satisfied at both edges, so favour the left.
if (dataLabelsX + spanRight > w.layout.gridWidth - strokeWidth) {
dataLabelsX = w.layout.gridWidth - spanRight - strokeWidth
}
if (dataLabelsX - spanLeft < strokeWidth) {
dataLabelsX = spanLeft + strokeWidth
}
}
return {
bcx: x,
bcy,
dataLabelsX,
dataLabelsY,
totalDataLabelsX,
totalDataLabelsY,
totalDataLabelsAnchor,
}
}
/** @param {{x: any, y: any, val: any, i: any, j: any, textRects: any, barHeight: any, barWidth: any, dataLabelsConfig: any}} opts */
drawCalculatedDataLabels({
x,
y,
val,
i, // = realIndex
j,
textRects,
barHeight,
barWidth,
dataLabelsConfig,
}) {
const w = this.w
let rotate = 'rotate(0)'
if (w.config.plotOptions.bar.dataLabels.orientation === 'vertical')
rotate = `rotate(-90, ${x}, ${y})`
const dataLabels = new DataLabels(this.barCtx.w, this.barCtx.ctx)
const graphics = new Graphics(this.barCtx.w)
const formatter = dataLabelsConfig.formatter
let elDataLabelsWrap = null
const isSeriesCollapsed = w.globals.collapsedSeriesIndices.indexOf(i) > -1
// A series collapsing in THIS update keeps painting its bars for the length
// of the exit tween (see the fill guard in Bar.renderSeries), so its labels
// have to ride along, otherwise the slice sits there unlabelled the whole
// way down while every other slice keeps its number.
const isSeriesCollapsing =
(w.globals.collapsingSeriesIndices || []).indexOf(i) > -1
if (isSeriesCollapsing) {
// Its series has already been zero-filled, so `val` is 0 (or blank) even
// though the bar on screen is still at its old height. Label it with what
// it was until it has finished shrinking away.
const prev = w.globals.prevDataLabels?.get(`${i}::${datumKey(w, i, j)}`)
if (prev && isFinite(prev.val)) val = prev.val
}
if (dataLabelsConfig.enabled && (!isSeriesCollapsed || isSeriesCollapsing)) {
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels',
transform: rotate,
})
// Stamp datum identity + raw value so the opt-in data-label transition
// (ride to new position + count-up) can match a label to its previous
// frame across a data update. Only scalar (non-range) values count up.
const dlCfg = w.config.dataLabels
if (dlCfg.animate?.enabled || dlCfg.countUp?.enabled) {
elDataLabelsWrap.node.setAttribute(
'data:dlKey',
`${i}::${datumKey(w, i, j)}`,
)
elDataLabelsWrap.node.setAttribute('data:dlJ', String(j))
if (typeof val === 'number' && isFinite(val)) {
elDataLabelsWrap.node.setAttribute('data:dlVal', String(val))
}
}
let text = ''
if (typeof val !== 'undefined') {
text = formatter(val, {
...w,
seriesIndex: i,
dataPointIndex: j,
w,
})
}
if (!val && w.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
text = ''
}
const valIsNegative = w.seriesData.series[i][j] < 0
const position = w.config.plotOptions.bar.dataLabels.position
if (w.config.plotOptions.bar.dataLabels.orientation === 'vertical') {
if (position === 'top') {
if (valIsNegative) dataLabelsConfig.textAnchor = 'end'
else dataLabelsConfig.textAnchor = 'start'
}
if (position === 'center') {
dataLabelsConfig.textAnchor = 'middle'
}
if (position === 'bottom') {
if (valIsNegative) dataLabelsConfig.textAnchor = 'end'
else dataLabelsConfig.textAnchor = 'start'
}
}
if (
this.barCtx.isRangeBar &&
this.barCtx.barOptions.dataLabels.hideOverflowingLabels
) {
// hide the datalabel if it cannot fit into the rect
const txRect = graphics.getTextRects(
text,
parseFloat(dataLabelsConfig.style.fontSize).toString(),
)
if (barWidth < txRect.width) {
text = ''
}
}
if (
w.config.chart.stacked &&
this.barCtx.barOptions.dataLabels.hideOverflowingLabels &&
// A collapsing series is measured against its NEW extent, which is
// already zero, so this would blank a label whose bar is still at full
// height on screen. It starts out fitting and fades away with the mark.
!isSeriesCollapsing
) {
// if there is not enough space to draw the label in the bar/column rect, check hideOverflowingLabels property to prevent overflowing on wrong rect
// Note: This issue is only seen in stacked charts
if (this.barCtx.isHorizontal) {
if (textRects.width / 1.6 > Math.abs(barWidth)) {
text = ''
}
} else {
if (textRects.height / 1.6 > Math.abs(barHeight)) {
text = ''
}
}
}
const modifiedDataLabelsConfig = {
...dataLabelsConfig,
}
if (this.barCtx.isHorizontal) {
if (val < 0) {
if (dataLabelsConfig.textAnchor === 'start') {
modifiedDataLabelsConfig.textAnchor = 'end'
} else if (dataLabelsConfig.textAnchor === 'end') {
modifiedDataLabelsConfig.textAnchor = 'start'
}
}
}
dataLabels.plotDataLabelsText({
x,
y,
text,
i,
j,
parent: elDataLabelsWrap,
dataLabelsConfig: modifiedDataLabelsConfig,
alwaysDrawDataLabel: true,
offsetCorrection: true,
})
}
return elDataLabelsWrap
}
/** @param {{ x?: any, y?: any, val?: any, rawVal?: any, realIndex?: any, j?: any, textAnchor?: any, barWidth?: any, barHeight?: any, dataLabelsConfig?: any, barTotalDataLabelsConfig?: any }} opts */
drawTotalDataLabels({
x,
y,
val,
rawVal,
realIndex,
j,
textAnchor,
barTotalDataLabelsConfig,
}) {
const graphics = new Graphics(this.barCtx.w)
let totalDataLabelText
if (
barTotalDataLabelsConfig.enabled &&
typeof x !== 'undefined' &&
typeof y !== 'undefined' &&
this.drawsStackedTotal(realIndex)
) {
totalDataLabelText = graphics.drawText({
x: x,
y: y,
foreColor: barTotalDataLabelsConfig.style.color,
text: val,
textAnchor,
fontFamily: barTotalDataLabelsConfig.style.fontFamily,
fontSize: barTotalDataLabelsConfig.style.fontSize,
fontWeight: barTotalDataLabelsConfig.style.fontWeight,
})
// The total is a SIBLING of the per-segment label groups (it hangs off
// the series-wide `.apexcharts-datalabels` wrap), and it tracks the top
// of the whole stack rather than any one segment, so it carries its own
// key, anchor and raw value and gets its own tween. See
// DataLabelTransition.
totalDataLabelText.attr({
class: 'apexcharts-datalabel-total',
cx: x,
cy: y,
})
const dlCfg = this.w.config.dataLabels
if (dlCfg.animate?.enabled || dlCfg.countUp?.enabled) {
// Keyed by GROUP, not by the series that draws it. The total is drawn
// by the topmost ACTIVE series, so hiding the last series changes the
// drawer (and its realIndex); a realIndex-based key then matches
// nothing from the previous frame and the ride silently degrades to a
// snap - but only when the LAST series is toggled, which is exactly how
// it escaped. Group membership comes from config and survives legend
// toggles. The drawing series still rides along in its own attribute
// for the formatter's seriesIndex.
const { groupIndex } = this.barCtx.barHelpers.getGroupIndex(realIndex)
totalDataLabelText.node.setAttribute(
'data:dlTotalKey',
`${groupIndex}::${datumKey(this.w, realIndex, j)}`,
)
totalDataLabelText.node.setAttribute(
'data:dlTotalSeries',
String(realIndex),
)
if (typeof rawVal === 'number' && isFinite(rawVal)) {
totalDataLabelText.node.setAttribute('data:dlTotalVal', String(rawVal))
}
}
}
return totalDataLabelText
}
}