apexcharts
Version:
A JavaScript Chart Library
732 lines (626 loc) • 22.4 kB
JavaScript
// @ts-check
import Graphics from '../Graphics'
import AxesUtils from './AxesUtils'
import { BrowserAPIs } from '../../ssr/BrowserAPIs.js'
import { SVGNS } from '../../svg/math'
/**
* ApexCharts XAxis Class for drawing X-Axis.
*
* @module XAxis
**/
export default class XAxis {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
* @param {any} [elgrid]
*/
constructor(w, ctx, elgrid) {
this.w = w
this.ctx = ctx // needed: xAxisLabelClick event callback passes ctx as chart instance
this.elgrid = elgrid
this.axesUtils = new AxesUtils(w, {
theme: ctx.theme,
timeScale: ctx.timeScale,
})
this.xaxisLabels = w.labelData.labels.slice()
if (w.labelData.timescaleLabels.length > 0 && !w.globals.isBarHorizontal) {
// timeline labels are there and chart is not rangeabr timeline
this.xaxisLabels = w.labelData.timescaleLabels.slice()
}
if (w.config.xaxis.overwriteCategories) {
this.xaxisLabels = w.config.xaxis.overwriteCategories
}
this.drawnLabels = []
this.drawnLabelsRects = []
if (w.config.xaxis.position === 'top') {
this.offY = 0
} else {
this.offY = w.layout.gridHeight
}
this.offY = this.offY + w.config.xaxis.axisBorder.offsetY
this.isCategoryBarHorizontal =
w.config.chart.type === 'bar' && w.config.plotOptions.bar.horizontal
this.xaxisFontSize = w.config.xaxis.labels.style.fontSize
this.xaxisFontFamily = w.config.xaxis.labels.style.fontFamily
this.xaxisForeColors = w.config.xaxis.labels.style.colors
this.xaxisBorderWidth = w.config.xaxis.axisBorder.width
if (this.isCategoryBarHorizontal) {
this.xaxisBorderWidth = w.config.yaxis[0].axisBorder.width.toString()
}
if (String(this.xaxisBorderWidth).indexOf('%') > -1) {
this.xaxisBorderWidth =
(w.layout.gridWidth * parseInt(this.xaxisBorderWidth, 10)) / 100
} else {
this.xaxisBorderWidth = parseInt(this.xaxisBorderWidth, 10)
}
this.xaxisBorderHeight = w.config.xaxis.axisBorder.height
// For bars, we will only consider single y xais,
// as we are not providing multiple yaxis for bar charts
this.yaxis = w.config.yaxis[0]
}
drawXaxis() {
const w = this.w
const graphics = new Graphics(this.w)
const elXaxis = graphics.group({
class: 'apexcharts-xaxis',
transform: `translate(${w.config.xaxis.offsetX}, ${w.config.xaxis.offsetY})`,
})
const elXaxisTexts = graphics.group({
class: 'apexcharts-xaxis-texts-g',
transform: `translate(${w.layout.translateXAxisX}, ${w.layout.translateXAxisY})`,
})
elXaxis.add(elXaxisTexts)
let labels = []
for (let i = 0; i < this.xaxisLabels.length; i++) {
labels.push(this.xaxisLabels[i])
}
/**
* @param {number} i
* @param {number} colWidth
*/
this.drawXAxisLabelAndGroup(
true,
graphics,
elXaxisTexts,
labels,
w.axisFlags.isXNumeric,
(/** @type {any} */ i, /** @type {any} */ colWidth) => colWidth,
)
if (w.labelData.hasXaxisGroups) {
const labelsGroup = w.labelData.groups
labels = []
for (let i = 0; i < labelsGroup.length; i++) {
labels.push(labelsGroup[i].title)
}
const overwriteStyles = /** @type {any} */ ({})
if (w.config.xaxis.group.style) {
overwriteStyles.xaxisFontSize = w.config.xaxis.group.style.fontSize
overwriteStyles.xaxisFontFamily = w.config.xaxis.group.style.fontFamily
overwriteStyles.xaxisForeColors = w.config.xaxis.group.style.colors
overwriteStyles.fontWeight = w.config.xaxis.group.style.fontWeight
overwriteStyles.cssClass = w.config.xaxis.group.style.cssClass
}
/**
* @param {number} i
* @param {number} colWidth
*/
this.drawXAxisLabelAndGroup(
false,
graphics,
elXaxisTexts,
labels,
false,
(/** @type {any} */ i, /** @type {any} */ colWidth) =>
labelsGroup[i].cols * colWidth,
overwriteStyles,
)
}
if (w.config.xaxis.title.text !== undefined) {
const elXaxisTitle = graphics.group({
class: 'apexcharts-xaxis-title',
})
const elXAxisTitleText = graphics.drawText({
x: w.layout.gridWidth / 2 + w.config.xaxis.title.offsetX,
y:
this.offY +
parseFloat(this.xaxisFontSize) +
(w.config.xaxis.position === 'bottom'
? w.layout.xAxisLabelsHeight
: -w.layout.xAxisLabelsHeight - 10) +
w.config.xaxis.title.offsetY,
text: w.config.xaxis.title.text,
textAnchor: 'middle',
fontSize: w.config.xaxis.title.style.fontSize,
fontFamily: w.config.xaxis.title.style.fontFamily,
fontWeight: w.config.xaxis.title.style.fontWeight,
foreColor: w.config.xaxis.title.style.color,
cssClass:
'apexcharts-xaxis-title-text ' + w.config.xaxis.title.style.cssClass,
})
elXaxisTitle.add(elXAxisTitleText)
elXaxis.add(elXaxisTitle)
}
if (w.config.xaxis.axisBorder.show) {
const offX = w.globals.barPadForNumericAxis
const elHorzLine = graphics.drawLine(
w.globals.padHorizontal + w.config.xaxis.axisBorder.offsetX - offX,
this.offY,
this.xaxisBorderWidth + offX,
this.offY,
w.config.xaxis.axisBorder.color,
0,
this.xaxisBorderHeight,
)
if (this.elgrid && this.elgrid.elGridBorders && w.config.grid.show) {
this.elgrid.elGridBorders.add(elHorzLine)
} else {
elXaxis.add(elHorzLine)
}
}
return elXaxis
}
/**
* @param {Record<string, any>} [overwriteStyles]
* @param {boolean} isLeafGroup
* @param {import('../Graphics').default} graphics
* @param {any} elXaxisTexts
* @param {any[]} labels
* @param {boolean} isXNumeric
* @param {any} colWidthCb
*/
drawXAxisLabelAndGroup(
isLeafGroup,
graphics,
elXaxisTexts,
labels,
isXNumeric,
colWidthCb,
overwriteStyles = {},
) {
const drawnLabels = []
const drawnLabelsRects = []
const w = this.w
const xaxisFontSize = overwriteStyles.xaxisFontSize || this.xaxisFontSize
const xaxisFontFamily =
overwriteStyles.xaxisFontFamily || this.xaxisFontFamily
const xaxisForeColors =
overwriteStyles.xaxisForeColors || this.xaxisForeColors
const fontWeight =
overwriteStyles.fontWeight || w.config.xaxis.labels.style.fontWeight
const cssClass =
overwriteStyles.cssClass || w.config.xaxis.labels.style.cssClass
let colWidth
// initial x Position (keep adding column width in the loop)
let xPos = w.globals.padHorizontal
const labelsLen = labels.length
/**
* labelsLen can be different (whether you are drawing x-axis labels or x-axis group labels)
* hence, we introduce dataPoints to be consistent.
* Also, in datetime/numeric xaxis, dataPoints can be misleading, so we resort to labelsLen for such xaxis type
*/
let dataPoints =
w.config.xaxis.type === 'category' ? w.globals.dataPoints : labelsLen
// when all series are collapsed, fixes #3381
if (dataPoints === 0 && labelsLen > dataPoints) dataPoints = labelsLen
if (isXNumeric) {
const len = Math.max(
Number(w.config.xaxis.tickAmount) || 1,
dataPoints > 1 ? dataPoints - 1 : dataPoints,
)
colWidth = w.layout.gridWidth / Math.min(len, labelsLen - 1)
xPos = xPos + colWidthCb(0, colWidth) / 2 + w.config.xaxis.labels.offsetX
} else {
colWidth = w.layout.gridWidth / dataPoints
xPos = xPos + colWidthCb(0, colWidth) + w.config.xaxis.labels.offsetX
}
for (let i = 0; i <= labelsLen - 1; i++) {
let x = xPos - colWidthCb(i, colWidth) / 2 + w.config.xaxis.labels.offsetX
if (
i === 0 &&
labelsLen === 1 &&
colWidth / 2 === xPos &&
dataPoints === 1
) {
// single datapoint
x = w.layout.gridWidth / 2
}
let label = this.axesUtils.getLabel(
labels,
w.labelData.timescaleLabels,
x,
i,
drawnLabels,
xaxisFontSize,
isLeafGroup,
)
let offsetYCorrection = 28
if (w.layout.rotateXLabels && isLeafGroup) {
offsetYCorrection = 22
}
if (w.config.xaxis.title.text && w.config.xaxis.position === 'top') {
offsetYCorrection += parseFloat(w.config.xaxis.title.style.fontSize) + 2
}
if (!isLeafGroup) {
offsetYCorrection =
offsetYCorrection +
parseFloat(xaxisFontSize) +
(w.layout.xAxisLabelsHeight - w.layout.xAxisGroupLabelsHeight) +
(w.layout.rotateXLabels ? 10 : 0)
}
const isCategoryTickAmounts =
typeof w.config.xaxis.tickAmount !== 'undefined' &&
w.config.xaxis.tickAmount !== 'dataPoints' &&
w.config.xaxis.type !== 'datetime'
if (isCategoryTickAmounts) {
label = this.axesUtils.checkLabelBasedOnTickamount(i, label, labelsLen)
} else {
label = this.axesUtils.checkForOverflowingLabels(
i,
label,
labelsLen,
drawnLabels,
drawnLabelsRects,
)
}
const getCatForeColor = () => {
return isLeafGroup && w.config.xaxis.convertedCatToNumeric
? xaxisForeColors[w.globals.minX + i - 1]
: xaxisForeColors[i]
}
if (w.config.xaxis.labels.show) {
const elText = graphics.drawText({
x: label.x,
y:
this.offY +
w.config.xaxis.labels.offsetY +
offsetYCorrection -
(w.config.xaxis.position === 'top'
? w.layout.xAxisHeight + w.config.xaxis.axisTicks.height - 2
: 0),
text: label.text,
textAnchor: 'middle',
fontWeight: label.isBold ? 600 : fontWeight,
fontSize: xaxisFontSize,
fontFamily: xaxisFontFamily,
foreColor: Array.isArray(xaxisForeColors)
? getCatForeColor()
: xaxisForeColors,
isPlainText: false,
cssClass:
(isLeafGroup
? 'apexcharts-xaxis-label '
: 'apexcharts-xaxis-group-label ') + cssClass,
})
elXaxisTexts.add(elText)
/**
* @param {Event} e
*/
elText.on('click', (/** @type {any} */ e) => {
if (typeof w.config.chart.events.xAxisLabelClick === 'function') {
const opts = Object.assign({}, w, {
labelIndex: i,
})
w.config.chart.events.xAxisLabelClick(e, this.ctx, opts)
}
})
if (isLeafGroup) {
const elTooltipTitle = BrowserAPIs.createElementNS(SVGNS, 'title')
elTooltipTitle.textContent = Array.isArray(label.text)
? label.text.join(' ')
: label.text
elText.node.appendChild(elTooltipTitle)
if (label.text !== '') {
drawnLabels.push(label.text)
drawnLabelsRects.push(label)
}
}
}
if (i < labelsLen - 1) {
xPos = xPos + colWidthCb(i + 1, colWidth)
}
}
}
// this actually becomes the vertical axis (for bar charts)
/**
* @param {number} realIndex
*/
drawXaxisInversed(realIndex) {
const w = this.w
const graphics = new Graphics(this.w)
const translateYAxisX = w.config.yaxis[0].opposite
? w.globals.translateYAxisX[realIndex]
: 0
const elYaxis = graphics.group({
class: 'apexcharts-yaxis apexcharts-xaxis-inversed',
rel: realIndex,
})
const elYaxisTexts = graphics.group({
class: 'apexcharts-yaxis-texts-g apexcharts-xaxis-inversed-texts-g',
transform: 'translate(' + translateYAxisX + ', 0)',
})
elYaxis.add(elYaxisTexts)
const labels = []
if (w.config.yaxis[realIndex].show) {
for (let i = 0; i < this.xaxisLabels.length; i++) {
labels.push(this.xaxisLabels[i])
}
}
const colHeight = w.layout.gridHeight / labels.length
// initial x Position (keep adding column width in the loop)
let yPos = -(colHeight / 2.2)
const lbFormatter = w.formatters.yLabelFormatters[0]
const ylabels = w.config.yaxis[0].labels
if (ylabels.show) {
for (let i = 0; i <= labels.length - 1; i++) {
let label = typeof labels[i] === 'undefined' ? '' : labels[i]
label = lbFormatter(label, {
seriesIndex: realIndex,
dataPointIndex: i,
w,
})
const yColors = this.axesUtils.getYAxisForeColor(
ylabels.style.colors,
realIndex,
)
const getForeColor = () => {
return Array.isArray(yColors) ? yColors[i] : yColors
}
let multiY = 0
if (Array.isArray(label)) {
multiY = (label.length / 2) * parseInt(ylabels.style.fontSize, 10)
}
let offsetX = ylabels.offsetX - 15
let textAnchor = 'end'
if (this.yaxis.opposite) {
textAnchor = 'start'
}
if (w.config.yaxis[0].labels.align === 'left') {
offsetX = ylabels.offsetX
textAnchor = 'start'
} else if (w.config.yaxis[0].labels.align === 'center') {
offsetX = ylabels.offsetX
textAnchor = 'middle'
} else if (w.config.yaxis[0].labels.align === 'right') {
textAnchor = 'end'
}
const elLabel = graphics.drawText({
x: offsetX,
y: yPos + colHeight + ylabels.offsetY - multiY,
text: label,
textAnchor,
foreColor: getForeColor(),
fontSize: ylabels.style.fontSize,
fontFamily: ylabels.style.fontFamily,
fontWeight: ylabels.style.fontWeight,
isPlainText: false,
cssClass: 'apexcharts-yaxis-label ' + ylabels.style.cssClass,
maxWidth: ylabels.maxWidth,
})
elYaxisTexts.add(elLabel)
/**
* @param {Event} e
*/
elLabel.on('click', (/** @type {any} */ e) => {
if (typeof w.config.chart.events.xAxisLabelClick === 'function') {
const opts = Object.assign({}, w, {
labelIndex: i,
})
w.config.chart.events.xAxisLabelClick(e, this.ctx, opts)
}
})
const elTooltipTitle = BrowserAPIs.createElementNS(SVGNS, 'title')
elTooltipTitle.textContent = Array.isArray(label)
? label.join(' ')
: label
elLabel.node.appendChild(elTooltipTitle)
if (w.config.yaxis[realIndex].labels.rotate !== 0) {
const labelRotatingCenter = graphics.rotateAroundCenter(elLabel.node)
elLabel.node.setAttribute(
'transform',
`rotate(${w.config.yaxis[realIndex].labels.rotate} 0 ${labelRotatingCenter.y})`,
)
}
yPos = yPos + colHeight
}
}
if (w.config.yaxis[0].title.text !== undefined) {
const elXaxisTitle = graphics.group({
class: 'apexcharts-yaxis-title apexcharts-xaxis-title-inversed',
transform: 'translate(' + translateYAxisX + ', 0)',
})
const elXAxisTitleText = graphics.drawText({
x: w.config.yaxis[0].title.offsetX,
y: w.layout.gridHeight / 2 + w.config.yaxis[0].title.offsetY,
text: w.config.yaxis[0].title.text,
textAnchor: 'middle',
foreColor: w.config.yaxis[0].title.style.color,
fontSize: w.config.yaxis[0].title.style.fontSize,
fontWeight: w.config.yaxis[0].title.style.fontWeight,
fontFamily: w.config.yaxis[0].title.style.fontFamily,
cssClass:
'apexcharts-yaxis-title-text ' +
w.config.yaxis[0].title.style.cssClass,
})
elXaxisTitle.add(elXAxisTitleText)
elYaxis.add(elXaxisTitle)
}
let offX = 0
if (this.isCategoryBarHorizontal && w.config.yaxis[0].opposite) {
offX = w.layout.gridWidth
}
const axisBorder = w.config.xaxis.axisBorder
if (axisBorder.show) {
const elVerticalLine = graphics.drawLine(
w.globals.padHorizontal + axisBorder.offsetX + offX,
1 + axisBorder.offsetY,
w.globals.padHorizontal + axisBorder.offsetX + offX,
w.layout.gridHeight + axisBorder.offsetY,
axisBorder.color,
0,
)
if (this.elgrid && this.elgrid.elGridBorders && w.config.grid.show) {
this.elgrid.elGridBorders.add(elVerticalLine)
} else {
elYaxis.add(elVerticalLine)
}
}
if (w.config.yaxis[0].axisTicks.show) {
this.axesUtils.drawYAxisTicks(
offX,
labels.length,
w.config.yaxis[0].axisBorder,
w.config.yaxis[0].axisTicks,
0,
colHeight,
elYaxis,
)
}
return elYaxis
}
/**
* @param {number} x1
* @param {number} y2
* @param {any} appendToElement
*/
drawXaxisTicks(x1, y2, appendToElement) {
const w = this.w
const x2 = x1
if (x1 < 0 || x1 - 2 > w.layout.gridWidth) return
const y1 = this.offY + w.config.xaxis.axisTicks.offsetY
y2 = y2 + y1 + w.config.xaxis.axisTicks.height
if (w.config.xaxis.position === 'top') {
y2 = y1 - w.config.xaxis.axisTicks.height
}
if (w.config.xaxis.axisTicks.show) {
const graphics = new Graphics(this.w)
const line = graphics.drawLine(
x1 + w.config.xaxis.axisTicks.offsetX,
y1 + w.config.xaxis.offsetY,
x2 + w.config.xaxis.axisTicks.offsetX,
y2 + w.config.xaxis.offsetY,
w.config.xaxis.axisTicks.color,
)
// we are not returning anything, but appending directly to the element passed in param
appendToElement.add(line)
line.node.classList.add('apexcharts-xaxis-tick')
}
}
getXAxisTicksPositions() {
const w = this.w
const xAxisTicksPositions = []
const xCount = this.xaxisLabels.length
let x1 = w.globals.padHorizontal
if (w.labelData.timescaleLabels.length > 0) {
for (let i = 0; i < xCount; i++) {
x1 = this.xaxisLabels[i].position
xAxisTicksPositions.push(x1)
}
} else {
const xCountForCategoryCharts = xCount
for (let i = 0; i < xCountForCategoryCharts; i++) {
let x1Count = xCountForCategoryCharts
if (w.axisFlags.isXNumeric && w.config.chart.type !== 'bar') {
x1Count -= 1
}
x1 = x1 + w.layout.gridWidth / x1Count
xAxisTicksPositions.push(x1)
}
}
return xAxisTicksPositions
}
// to rotate x-axis labels or to put ... for longer text in xaxis
xAxisLabelCorrections() {
const w = this.w
const graphics = new Graphics(this.w)
const xAxis = w.dom.baseEl.querySelector('.apexcharts-xaxis-texts-g')
const xAxisTexts = w.dom.baseEl.querySelectorAll(
'.apexcharts-xaxis-texts-g text:not(.apexcharts-xaxis-group-label)',
)
const yAxisTextsInversed = w.dom.baseEl.querySelectorAll(
'.apexcharts-yaxis-inversed text',
)
const xAxisTextsInversed = w.dom.baseEl.querySelectorAll(
'.apexcharts-xaxis-inversed-texts-g text tspan',
)
if (w.layout.rotateXLabels || w.config.xaxis.labels.rotateAlways) {
for (let xat = 0; xat < xAxisTexts.length; xat++) {
const textRotatingCenter = graphics.rotateAroundCenter(xAxisTexts[xat])
textRotatingCenter.y = textRotatingCenter.y - 1 // + tickWidth/4;
textRotatingCenter.x = textRotatingCenter.x + 1
xAxisTexts[xat].setAttribute(
'transform',
`rotate(${w.config.xaxis.labels.rotate} ${textRotatingCenter.x} ${textRotatingCenter.y})`,
)
xAxisTexts[xat].setAttribute('text-anchor', `end`)
const offsetHeight = 10
xAxis?.setAttribute('transform', `translate(0, ${-offsetHeight})`)
const tSpan = xAxisTexts[xat].childNodes
if (w.config.xaxis.labels.trim) {
/**
* @param {any} ts
*/
Array.prototype.forEach.call(tSpan, (ts) => {
graphics.placeTextWithEllipsis(
ts,
ts.textContent,
w.layout.xAxisLabelsHeight -
(w.config.legend.position === 'bottom' ? 20 : 10),
)
})
}
}
} else {
const width = w.layout.gridWidth / (w.labelData.labels.length + 1)
for (let xat = 0; xat < xAxisTexts.length; xat++) {
const tSpan = xAxisTexts[xat].childNodes
if (w.config.xaxis.labels.trim && w.config.xaxis.type !== 'datetime') {
/**
* @param {any} ts
*/
Array.prototype.forEach.call(tSpan, (ts) => {
graphics.placeTextWithEllipsis(ts, ts.textContent, width)
})
}
}
}
if (yAxisTextsInversed.length > 0) {
// truncate rotated y axis in bar chart (x axis)
const firstLabelPosX = /** @type {SVGGraphicsElement} */ (
yAxisTextsInversed[yAxisTextsInversed.length - 1]
).getBBox()
const lastLabelPosX = /** @type {SVGGraphicsElement} */ (
yAxisTextsInversed[0]
).getBBox()
if (firstLabelPosX.x < -20) {
yAxisTextsInversed[
yAxisTextsInversed.length - 1
].parentNode?.removeChild(
yAxisTextsInversed[yAxisTextsInversed.length - 1],
)
}
if (
lastLabelPosX.x + lastLabelPosX.width > w.layout.gridWidth &&
!w.globals.isBarHorizontal
) {
yAxisTextsInversed[0].parentNode?.removeChild(yAxisTextsInversed[0])
}
// truncate rotated x axis in bar chart (y axis)
for (let xat = 0; xat < xAxisTextsInversed.length; xat++) {
graphics.placeTextWithEllipsis(
xAxisTextsInversed[xat],
xAxisTextsInversed[xat].textContent ?? '',
w.config.yaxis[0].labels.maxWidth -
(w.config.yaxis[0].title.text
? parseFloat(w.config.yaxis[0].title.style.fontSize) * 2
: 0) -
15,
)
}
}
}
// renderXAxisBands() {
// let w = this.w;
// let plotBand = document.createElementNS(SVGNS, 'rect')
// w.dom.elGraphical.add(plotBand)
// }
}