apexcharts
Version:
A JavaScript Chart Library
472 lines (404 loc) • 12.7 kB
JavaScript
// @ts-check
import Scatter from './../charts/Scatter'
import Graphics from './Graphics'
import Filters from './Filters'
/**
* ApexCharts DataLabels Class for drawing dataLabels on Axes based Charts.
*
* @module DataLabels
**/
class DataLabels {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext | null} ctx
*/
constructor(w, ctx = null) {
this.w = w
this.ctx = ctx // only used for new Scatter(w, ctx) in bubble chart path
}
// When there are many datalabels to be printed, and some of them overlaps each other in the same series, this method will take care of that
// Also, when datalabels exceeds the drawable area and get clipped off, we need to adjust and move some pixels to make them visible again
/**
* @param {number} x
* @param {number} y
* @param {any} val
* @param {number} i
* @param {number} dataPointIndex
* @param {boolean} alwaysDrawDataLabel
* @param {string} fontSize
*/
dataLabelsCorrection(
x,
y,
val,
i,
dataPointIndex,
alwaysDrawDataLabel,
fontSize,
) {
const w = this.w
const graphics = new Graphics(this.w)
let drawnextLabel = false //
const textRects = /** @type {any} */ (graphics).getTextRects(val, fontSize)
const width = textRects.width
const height = textRects.height
if (y < 0) y = 0
if (y > w.layout.gridHeight + height) y = w.layout.gridHeight + height / 2
// first value in series, so push an empty array
if (
typeof /** @type {any} */ (w.globals).dataLabelsRects[i] === 'undefined'
) {
;/** @type {any} */ (w.globals).dataLabelsRects[i] = []
}
// then start pushing actual rects in that sub-array
;/** @type {any} */ (w.globals).dataLabelsRects[i].push({
x,
y,
width,
height,
})
const len = /** @type {any} */ (w.globals).dataLabelsRects[i].length - 2
const lastDrawnIndex =
typeof w.globals.lastDrawnDataLabelsIndexes[i] !== 'undefined'
? w.globals.lastDrawnDataLabelsIndexes[i][
w.globals.lastDrawnDataLabelsIndexes[i].length - 1
]
: 0
if (
typeof (/** @type {any} */ (w.globals.dataLabelsRects[i])[len]) !==
'undefined'
) {
const lastDataLabelRect = /** @type {any} */ (
w.globals.dataLabelsRects[i]
)[lastDrawnIndex]
if (
// next label forward and x not intersecting
x > lastDataLabelRect.x + lastDataLabelRect.width ||
y > lastDataLabelRect.y + lastDataLabelRect.height ||
y + height < lastDataLabelRect.y ||
x + width < lastDataLabelRect.x // next label is going to be drawn backwards
) {
// the 2 indexes don't override, so OK to draw next label
drawnextLabel = true
}
}
if (dataPointIndex === 0 || alwaysDrawDataLabel) {
drawnextLabel = true
}
return {
x,
y,
textRects,
drawnextLabel,
}
}
/** @param {{type: any, pos: any, i: any, j: any, isRangeStart: any, strokeWidth?: any}} opts */
drawDataLabel({ type, pos, i, j, isRangeStart, strokeWidth = 2 }) {
// this method handles line, area, bubble, scatter charts as those charts contains markers/points which have pre-defined x/y positions
// all other charts like radar / bars / heatmaps will define their own drawDataLabel routine
const w = this.w
const graphics = new Graphics(this.w)
const dataLabelsConfig = w.config.dataLabels
let x = 0
let y = 0
let dataPointIndex = j
let elDataLabelsWrap = null
const seriesCollapsed = w.globals.collapsedSeriesIndices.indexOf(i) !== -1
if (seriesCollapsed || !dataLabelsConfig.enabled || !Array.isArray(pos.x)) {
return elDataLabelsWrap
}
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels',
})
for (let q = 0; q < pos.x.length; q++) {
x = pos.x[q] + dataLabelsConfig.offsetX
y = pos.y[q] + dataLabelsConfig.offsetY + strokeWidth
if (!isNaN(x)) {
// a small hack as we have 2 points for the first val to connect it
if (j === 1 && q === 0) dataPointIndex = 0
if (j === 1 && q === 1) dataPointIndex = 1
let val = w.seriesData.series[i][dataPointIndex]
if (type === 'rangeArea') {
if (isRangeStart) {
val = w.rangeData.seriesRangeStart[i][dataPointIndex]
} else {
val = w.rangeData.seriesRangeEnd[i][dataPointIndex]
}
}
let text = ''
/**
* @param {any} v
*/
const getText = (v) => {
return w.config.dataLabels.formatter(v, {
seriesIndex: i,
dataPointIndex,
w,
})
}
if (w.config.chart.type === 'bubble') {
val = w.seriesData.seriesZ[i][dataPointIndex]
text = getText(val)
y = pos.y[q]
const scatter = new Scatter(
this.w,
/** @type {import('../types/internal').ChartContext} */ (this.ctx),
)
const centerTextInBubbleCoords = scatter.centerTextInBubble(y)
y = centerTextInBubbleCoords.y
} else {
if (typeof val !== 'undefined') {
text = getText(val)
}
}
let textAnchor = w.config.dataLabels.textAnchor
if (w.globals.isSlopeChart) {
if (dataPointIndex === 0) {
textAnchor = 'end'
} else if (
dataPointIndex ===
/** @type {Record<string,any>} */ (w.config.series[i]).data.length - 1
) {
textAnchor = 'start'
} else {
textAnchor = 'middle'
}
}
this.plotDataLabelsText({
x,
y,
text,
i,
j: dataPointIndex,
parent: elDataLabelsWrap,
offsetCorrection: true,
dataLabelsConfig: w.config.dataLabels,
textAnchor,
})
}
}
return elDataLabelsWrap
}
/**
* @param {Record<string, any>} opts
*/
plotDataLabelsText(opts) {
const w = this.w
const graphics = new Graphics(this.w)
let {
x,
y,
i,
j,
text,
textAnchor,
fontSize,
parent,
dataLabelsConfig,
color,
alwaysDrawDataLabel,
offsetCorrection,
className,
} = opts
let dataLabelText = null
if (Array.isArray(w.config.dataLabels.enabledOnSeries)) {
if (w.config.dataLabels.enabledOnSeries.indexOf(i) < 0) {
return dataLabelText
}
}
let correctedLabels = {
x,
y,
drawnextLabel: true,
textRects: null,
}
if (offsetCorrection) {
correctedLabels = this.dataLabelsCorrection(
x,
y,
text,
i,
j,
alwaysDrawDataLabel,
parseInt(
/** @type {any} */ (dataLabelsConfig).style.fontSize,
10,
).toString(),
)
}
// when zoomed, we don't need to correct labels offsets,
// but if normally, labels get cropped, correct them
if (!w.interact.zoomed) {
x = correctedLabels.x
y = correctedLabels.y
}
if (correctedLabels.textRects) {
// fixes #2264
const barPad = w.globals.barPadForNumericAxis || 0
if (
x <
-(barPad + 20) -
/** @type {any} */ (correctedLabels.textRects).width ||
x >
w.layout.gridWidth +
/** @type {any} */ (correctedLabels.textRects).width +
barPad +
30
) {
// datalabels fall outside drawing area, so draw a blank label
text = ''
}
}
let dataLabelColor = w.globals.dataLabels.style.colors[i]
if (
((w.config.chart.type === 'bar' || w.config.chart.type === 'rangeBar') &&
w.config.plotOptions.bar.distributed) ||
w.config.dataLabels.distributed
) {
dataLabelColor = w.globals.dataLabels.style.colors[j]
}
if (typeof dataLabelColor === 'function') {
dataLabelColor = /** @type {any} */ (dataLabelColor)({
series: w.seriesData.series,
seriesIndex: i,
dataPointIndex: j,
w,
})
}
if (color) {
dataLabelColor = color
}
let offX = dataLabelsConfig.offsetX
let offY = dataLabelsConfig.offsetY
if (w.config.chart.type === 'bar' || w.config.chart.type === 'rangeBar') {
// for certain chart types, we handle offsets while calculating datalabels pos
// why? because bars/column may have negative values and based on that
// offsets becomes reversed
offX = 0
offY = 0
}
if (w.globals.isSlopeChart) {
if (j !== 0) {
offX = dataLabelsConfig.offsetX * -2 + 5
}
if (
j !== 0 &&
j !== /** @type {Record<string,any>} */ (w.config.series[i]).data.length - 1
) {
offX = 0
}
}
if (correctedLabels.drawnextLabel) {
if (textAnchor === 'middle') {
if (x === w.layout.gridWidth) {
// last label - might get cropped
// fixes https://github.com/apexcharts/apexcharts.js/issues/5036
textAnchor = 'end'
}
}
dataLabelText = graphics.drawText({
x: x + offX,
y: y + offY,
foreColor: dataLabelColor,
textAnchor: textAnchor || dataLabelsConfig.textAnchor,
text,
fontSize: fontSize || dataLabelsConfig.style.fontSize,
fontFamily: dataLabelsConfig.style.fontFamily,
fontWeight: dataLabelsConfig.style.fontWeight || 'normal',
})
dataLabelText.attr({
class: className || 'apexcharts-datalabel',
cx: x,
cy: y,
})
if (dataLabelsConfig.dropShadow.enabled) {
const textShadow = dataLabelsConfig.dropShadow
const filters = new Filters(this.w)
filters.dropShadow(dataLabelText, textShadow)
}
parent.add(dataLabelText)
if (typeof w.globals.lastDrawnDataLabelsIndexes[i] === 'undefined') {
w.globals.lastDrawnDataLabelsIndexes[i] = []
}
w.globals.lastDrawnDataLabelsIndexes[i].push(j)
}
return dataLabelText
}
/**
* @param {Element} el
* @param {{x: number, y: number, width: number, height: number}} coords
*/
addBackgroundToDataLabel(el, coords) {
const w = this.w
const bCnf = w.config.dataLabels.background
const paddingH = bCnf.padding
const paddingV = bCnf.padding / 2
const width = coords.width
const height = coords.height
const graphics = new Graphics(this.w)
const elRect = graphics.drawRect(
coords.x - paddingH,
coords.y - paddingV / 2,
width + paddingH * 2,
height + paddingV,
bCnf.borderRadius,
w.config.chart.background === 'transparent' || !w.config.chart.background
? '#fff'
: w.config.chart.background,
bCnf.opacity,
bCnf.borderWidth,
bCnf.borderColor,
)
if (bCnf.dropShadow.enabled) {
const filters = new Filters(this.w)
filters.dropShadow(elRect, bCnf.dropShadow)
}
return elRect
}
dataLabelsBackground() {
const w = this.w
if (w.config.chart.type === 'bubble') return
const elDataLabels = w.dom.baseEl.querySelectorAll(
'.apexcharts-datalabels text',
)
for (let i = 0; i < elDataLabels.length; i++) {
const el = elDataLabels[i]
const coords = /** @type {SVGGraphicsElement} */ (el).getBBox()
let elRect = null
if (coords.width && coords.height) {
elRect = this.addBackgroundToDataLabel(el, coords)
}
if (elRect) {
el.parentNode?.insertBefore(elRect.node, el)
const background =
w.config.dataLabels.background.backgroundColor ||
el.getAttribute('fill')
const shouldAnim =
w.config.chart.animations.enabled &&
!w.globals.resized &&
!w.globals.dataChanged
if (shouldAnim) {
elRect.animate().attr({ fill: background })
} else {
elRect.attr({ fill: background })
}
el.setAttribute('fill', w.config.dataLabels.background.foreColor)
}
}
}
bringForward() {
const w = this.w
const elDataLabelsNodes = w.dom.baseEl.querySelectorAll(
'.apexcharts-datalabels',
)
const elSeries = w.dom.baseEl.querySelector(
'.apexcharts-plot-series:last-child',
)
for (let i = 0; i < elDataLabelsNodes.length; i++) {
if (elSeries) {
elSeries.insertBefore(elDataLabelsNodes[i], elSeries.nextSibling)
}
}
}
}
export default DataLabels