apexcharts
Version:
A JavaScript Chart Library
620 lines (534 loc) • 16.5 kB
JavaScript
// @ts-check
import Graphics from './Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts Series Class for interaction with the Series of the chart.
*
* @module Series
**/
export default class Series {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {{ toggleDataSeries?: Function, revertDefaultAxisMinMax?: Function, updateSeries?: Function }} [callbacks]
*/
constructor(
w,
{
toggleDataSeries = undefined,
revertDefaultAxisMinMax = undefined,
updateSeries = undefined,
} = {},
) {
this.w = w
// Injected callbacks for cross-module coordination (toggleSeries/showSeries/hideSeries/resetSeries)
/** @type {Function | null} */ this._toggleDataSeries =
toggleDataSeries || null
/** @type {Function | null} */ this._revertDefaultAxisMinMax =
revertDefaultAxisMinMax || null
/** @type {Function | null} */ this._updateSeries = updateSeries || null
this.legendInactiveClass = 'legend-mouseover-inactive'
}
clearSeriesCache() {
const w = this.w
if (w.globals.cachedSelectors) {
delete w.globals.cachedSelectors.allSeriesEls
delete w.globals.cachedSelectors.highlightSeriesEls
}
}
getAllSeriesEls() {
// cache the result to avoid repeated querySelectorAll
const w = this.w
const cacheKey = 'allSeriesEls'
if (!w.globals.cachedSelectors[cacheKey]) {
w.globals.cachedSelectors[cacheKey] = /** @type {any} */ (
w.dom.baseEl.getElementsByClassName(`apexcharts-series`)
)
}
return w.globals.cachedSelectors[cacheKey]
}
/**
* @param {string} seriesName
*/
getSeriesByName(seriesName) {
return this.w.dom.baseEl.querySelector(
`.apexcharts-inner .apexcharts-series[seriesName='${Utils.escapeString(
seriesName,
)}']`,
)
}
/**
* @param {string} seriesName
*/
isSeriesHidden(seriesName) {
const targetElement = this.getSeriesByName(seriesName)
const el = /** @type {Element} */ (targetElement)
const realIndex = parseInt(el.getAttribute('data:realIndex') ?? '0', 10)
const isHidden = el.classList.contains('apexcharts-series-collapsed')
return { isHidden, realIndex }
}
/**
* @param {any} elSeries
* @param {number} index
*/
addCollapsedClassToSeries(elSeries, index) {
Series.addCollapsedClassToSeries(this.w, elSeries, index)
}
/**
* @param {import('../types/internal').ChartStateW} w
* @param {any} elSeries
* @param {number} index
*/
static addCollapsedClassToSeries(w, elSeries, index) {
/**
* @param {any[]} series
*/
function iterateOnAllCollapsedSeries(series) {
for (let cs = 0; cs < series.length; cs++) {
if (series[cs].index === index) {
elSeries.node.classList.add('apexcharts-series-collapsed')
}
}
}
iterateOnAllCollapsedSeries(w.globals.collapsedSeries)
iterateOnAllCollapsedSeries(w.globals.ancillaryCollapsedSeries)
}
/**
* @param {string} seriesName
*/
toggleSeries(seriesName) {
const isSeriesHidden = this.isSeriesHidden(seriesName)
this._toggleDataSeries?.(isSeriesHidden.realIndex, isSeriesHidden.isHidden)
return isSeriesHidden.isHidden
}
/**
* @param {string} seriesName
*/
showSeries(seriesName) {
const isSeriesHidden = this.isSeriesHidden(seriesName)
if (isSeriesHidden.isHidden) {
this._toggleDataSeries?.(isSeriesHidden.realIndex, true)
}
}
/**
* @param {string} seriesName
*/
hideSeries(seriesName) {
const isSeriesHidden = this.isSeriesHidden(seriesName)
if (!isSeriesHidden.isHidden) {
this._toggleDataSeries?.(isSeriesHidden.realIndex, false)
}
}
resetSeries(
shouldUpdateChart = true,
shouldResetZoom = true,
shouldResetCollapsed = true,
) {
const w = this.w
this.clearSeriesCache()
let series = Utils.clone(w.globals.initialSeries)
w.globals.previousPaths = []
if (shouldResetCollapsed) {
w.globals.collapsedSeries = []
w.globals.ancillaryCollapsedSeries = []
w.globals.collapsedSeriesIndices = []
w.globals.ancillaryCollapsedSeriesIndices = []
} else {
series = this.emptyCollapsedSeries(series)
}
w.config.series = series
if (shouldUpdateChart) {
if (shouldResetZoom) {
w.interact.zoomed = false
this._revertDefaultAxisMinMax?.()
}
this._updateSeries?.(
series,
w.config.chart.animations.dynamicAnimation.enabled,
)
}
}
/**
* @param {any[]} series
*/
emptyCollapsedSeries(series) {
const w = this.w
for (let i = 0; i < series.length; i++) {
if (w.globals.collapsedSeriesIndices.indexOf(i) > -1) {
series[i].data = []
}
}
return series
}
/**
* @param {string} seriesName
*/
highlightSeries(seriesName) {
const w = this.w
const targetElement = this.getSeriesByName(seriesName)
const realIndex = parseInt(
targetElement?.getAttribute('data:realIndex') ?? '',
10,
)
const cacheKey = 'highlightSeriesEls'
let allSeriesEls = w.globals.cachedSelectors[cacheKey]
if (!allSeriesEls) {
allSeriesEls = w.dom.baseEl.querySelectorAll(
`.apexcharts-series, .apexcharts-datalabels, .apexcharts-yaxis`,
)
w.globals.cachedSelectors[cacheKey] = allSeriesEls
}
let seriesEl = null
let dataLabelEl = null
let yaxisEl = null
if (w.globals.axisCharts || w.config.chart.type === 'radialBar') {
if (w.globals.axisCharts) {
seriesEl = w.dom.baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${realIndex}']`,
)
dataLabelEl = w.dom.baseEl.querySelector(
`.apexcharts-datalabels[data\\:realIndex='${realIndex}']`,
)
const yaxisIndex = w.globals.seriesYAxisReverseMap[realIndex]
yaxisEl = w.dom.baseEl.querySelector(
`.apexcharts-yaxis[rel='${yaxisIndex}']`,
)
} else {
seriesEl = w.dom.baseEl.querySelector(
`.apexcharts-series[rel='${realIndex + 1}']`,
)
}
} else {
seriesEl = w.dom.baseEl.querySelector(
`.apexcharts-series[rel='${realIndex + 1}'] path`,
)
}
for (let se = 0; se < allSeriesEls.length; se++) {
const serEl = /** @type {Element} */ (allSeriesEls[se])
serEl.classList.add(this.legendInactiveClass)
}
if (seriesEl) {
if (!w.globals.axisCharts) {
const parentEl = /** @type {Element} */ (seriesEl.parentNode)
parentEl?.classList.remove(this.legendInactiveClass)
}
seriesEl.classList.remove(this.legendInactiveClass)
if (dataLabelEl !== null) {
dataLabelEl.classList.remove(this.legendInactiveClass)
}
if (yaxisEl !== null) {
yaxisEl.classList.remove(this.legendInactiveClass)
}
} else {
for (let se = 0; se < allSeriesEls.length; se++) {
const serEl = /** @type {Element} */ (allSeriesEls[se])
serEl.classList.remove(this.legendInactiveClass)
}
}
}
/**
* @param {Event} e
* @param {any} targetElement
*/
toggleSeriesOnHover(e, targetElement) {
const w = this.w
if (!targetElement) targetElement = e.target
const allSeriesEls = w.dom.baseEl.querySelectorAll(
`.apexcharts-series, .apexcharts-datalabels, .apexcharts-yaxis`,
)
if (e.type === 'mousemove') {
const realIndex = parseInt(targetElement.getAttribute('rel'), 10) - 1
this.highlightSeries(w.seriesData.seriesNames[realIndex])
} else if (e.type === 'mouseout') {
for (let se = 0; se < allSeriesEls.length; se++) {
allSeriesEls[se].classList.remove(this.legendInactiveClass)
}
}
}
/**
* @param {Event} e
* @param {any} targetElement
*/
highlightRangeInSeries(e, targetElement) {
const w = this.w
const allHeatMapElements = w.dom.baseEl.getElementsByClassName(
'apexcharts-heatmap-rect',
)
/**
* @param {string} action
*/
const activeInactive = (action) => {
for (let i = 0; i < allHeatMapElements.length; i++) {
const actionFn = /** @type {any} */ (allHeatMapElements[i]).classList[
action
]
if (typeof actionFn === 'function') {
actionFn.call(
/** @type {any} */ (allHeatMapElements[i]).classList,
this.legendInactiveClass,
)
}
}
}
/**
* @param {Record<string, any>} range
* @param {number} rangeMax
*/
const removeInactiveClassFromHoveredRange = (range, rangeMax) => {
for (let i = 0; i < allHeatMapElements.length; i++) {
const val = Number(allHeatMapElements[i].getAttribute('val'))
if (
val >= range.from &&
(val < range.to || (range.to === rangeMax && val === rangeMax))
) {
allHeatMapElements[i].classList.remove(this.legendInactiveClass)
}
}
}
if (e.type === 'mousemove') {
const seriesCnt = parseInt(targetElement.getAttribute('rel'), 10) - 1
activeInactive('add')
const ranges = w.config.plotOptions.heatmap.colorScale.ranges
const range = ranges[seriesCnt]
/**
* @param {number} acc
* @param {Record<string, any>} cur
*/
const rangeMax = ranges.reduce(
(/** @type {number} */ acc, /** @type {any} */ cur) =>
Math.max(acc, cur.to),
0,
)
removeInactiveClassFromHoveredRange(range, rangeMax)
} else if (e.type === 'mouseout') {
activeInactive('remove')
}
}
/**
* @param {string[]} chartTypes
*/
getActiveConfigSeriesIndex(order = 'asc', chartTypes = []) {
const w = this.w
let activeIndex = 0
if (w.config.series.length > 1) {
// active series flag is required to know if user has not deactivated via legend click
/**
* @param {Record<string, any>} s
* @param {number} index
*/
const activeSeriesIndex = w.config.series.map((s, index) => {
const checkChartType = () => {
if (w.globals.comboCharts) {
return (
chartTypes.length === 0 ||
(chartTypes.length &&
chartTypes.indexOf(
/** @type {Record<string,any>} */ (w.config.series[index])
.type,
) > -1)
)
}
return true
}
const hasData =
/** @type {any} */ (s).data &&
/** @type {any} */ (s).data.length > 0 &&
w.globals.collapsedSeriesIndices.indexOf(index) === -1
return hasData && checkChartType() ? index : -1
})
for (
let a = order === 'asc' ? 0 : activeSeriesIndex.length - 1;
order === 'asc' ? a < activeSeriesIndex.length : a >= 0;
order === 'asc' ? a++ : a--
) {
if (activeSeriesIndex[a] !== -1) {
activeIndex = activeSeriesIndex[a]
break
}
}
}
return activeIndex
}
getBarSeriesIndices() {
const w = this.w
if (w.globals.comboCharts) {
return this.w.config.series
.map((/** @type {any} */ s, /** @type {number} */ i) => {
return s.type === 'bar' || s.type === 'column' ? i : -1
})
.filter((/** @type {number} */ i) => {
return i !== -1
})
}
/**
* @param {Record<string, any>} s
* @param {number} i
*/
return this.w.config.series.map((s, i) => {
return i
})
}
getPreviousPaths() {
const w = this.w
w.globals.previousPaths = []
/**
* @param {any} seriesEls
* @param {number} i
* @param {string} type
*/
function pushPaths(seriesEls, i, type) {
const paths = seriesEls[i].childNodes
const dArr = {
type,
paths: /** @type {any[]} */ ([]),
realIndex: seriesEls[i].getAttribute('data:realIndex'),
}
for (let j = 0; j < paths.length; j++) {
if (paths[j].hasAttribute('pathTo')) {
const d = paths[j].getAttribute('pathTo')
dArr.paths.push({ d })
}
}
w.globals.previousPaths.push(dArr)
}
/**
* @param {string} chartType
*/
const getPaths = (chartType) => {
return w.dom.baseEl.querySelectorAll(
`.apexcharts-${chartType}-series .apexcharts-series`,
)
}
const chartTypes = [
'line',
'area',
'bar',
'rangebar',
'rangeArea',
'candlestick',
'radar',
]
chartTypes.forEach((type) => {
const paths = getPaths(type)
for (let p = 0; p < paths.length; p++) {
pushPaths(paths, p, type)
}
})
const heatTreeSeries = w.dom.baseEl.querySelectorAll(
`.apexcharts-${w.config.chart.type} .apexcharts-series`,
)
if (heatTreeSeries.length > 0) {
for (let h = 0; h < heatTreeSeries.length; h++) {
const seriesEls = w.dom.baseEl.querySelectorAll(
`.apexcharts-${w.config.chart.type} .apexcharts-series[data\\:realIndex='${h}'] rect`,
)
const dArr = []
for (let i = 0; i < seriesEls.length; i++) {
/**
* @param {number} x
*/
const getAttr = (/** @type {string} */ x) => {
return /** @type {Element} */ (seriesEls[i]).getAttribute(x)
}
const rect = {
x: parseFloat(getAttr('x') ?? '0'),
y: parseFloat(getAttr('y') ?? '0'),
width: parseFloat(getAttr('width') ?? '0'),
height: parseFloat(getAttr('height') ?? '0'),
}
dArr.push({
rect,
color: seriesEls[i].getAttribute('color'),
})
}
w.globals.previousPaths.push(dArr)
}
}
if (!w.globals.axisCharts) {
// for non-axis charts (i.e., circular charts, pathFrom is not usable. We need whole series)
w.globals.previousPaths = w.seriesData.series
}
}
clearPreviousPaths() {
const w = this.w
w.globals.previousPaths = []
w.globals.allSeriesCollapsed = false
}
handleNoData() {
const w = this.w
const me = this
const noDataOpts = w.config.noData
const graphics = new Graphics(me.w)
let x = w.globals.svgWidth / 2
let y = w.globals.svgHeight / 2
let textAnchor = 'middle'
w.globals.noData = true
w.globals.animationEnded = true
if (noDataOpts.align === 'left') {
x = 10
textAnchor = 'start'
} else if (noDataOpts.align === 'right') {
x = w.globals.svgWidth - 10
textAnchor = 'end'
}
if (noDataOpts.verticalAlign === 'top') {
y = 50
} else if (noDataOpts.verticalAlign === 'bottom') {
y = w.globals.svgHeight - 50
}
x = x + noDataOpts.offsetX
y = y + parseInt(noDataOpts.style.fontSize, 10) + 2 + noDataOpts.offsetY
if (noDataOpts.text !== undefined && noDataOpts.text !== '') {
const titleText = graphics.drawText({
x,
y,
text: noDataOpts.text,
textAnchor,
fontSize: noDataOpts.style.fontSize,
fontFamily: noDataOpts.style.fontFamily,
foreColor: noDataOpts.style.color,
opacity: 1,
cssClass: 'apexcharts-text-nodata',
})
w.dom.Paper.add(titleText)
}
}
// When user clicks on legends, the collapsed series is filled with [0,0,0,...,0]
// This is because we don't want to alter the series' length as it is used at many places
/**
* @param {any[]} series
*/
setNullSeriesToZeroValues(series) {
const w = this.w
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length === 0) {
for (let j = 0; j < series[w.globals.maxValsInArrayIndex].length; j++) {
series[sl].push(0)
}
}
}
return series
}
hasAllSeriesEqualX() {
let equalLen = true
const w = this.w
const filteredSerX = this.filteredSeriesX()
for (let i = 0; i < filteredSerX.length - 1; i++) {
if (filteredSerX[i][0] !== filteredSerX[i + 1][0]) {
equalLen = false
break
}
}
w.globals.allSeriesHasEqualX = equalLen
return equalLen
}
filteredSeriesX() {
const w = this.w
/**
* @param {any[]} ser
*/
const filteredSeriesX = w.seriesData.seriesX.map((ser) =>
ser.length > 0 ? ser : [],
)
return filteredSeriesX
}
}