apexcharts
Version:
A JavaScript Chart Library
1,272 lines (1,110 loc) • 36.7 kB
JavaScript
// @ts-check
import Labels from './Labels'
import Position from './Position'
import Marker from './Marker'
import Intersect from './Intersect'
import AxesTooltip from './AxesTooltip'
import { BrowserAPIs } from '../../ssr/BrowserAPIs.js'
import Graphics from '../Graphics'
import Series from '../Series'
import XAxis from './../axes/XAxis'
import Utils from './Utils'
/**
* ApexCharts Core Tooltip Class to handle the tooltip generation.
*
* @module Tooltip
**/
export default class Tooltip {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w, ctx) {
this.w = w
this.ctx = ctx // needed: getGroupedCharts, getSyncedCharts, fireEvent, XAxis instantiation
this.tConfig = w.config.tooltip
this.tooltipUtil = new Utils(this)
this.tooltipLabels = new Labels(this)
this.tooltipPosition = new Position(this)
this.marker = new Marker(this)
this.intersect = new Intersect(this)
this.axesTooltip = new AxesTooltip(this)
this.showOnIntersect = this.tConfig.intersect
this.showTooltipTitle = this.tConfig.x.show
this.fixedTooltip = this.tConfig.fixed.enabled
/** @type {HTMLElement | null} */
this.xaxisTooltip = null
/** @type {HTMLElement | null} */
this.xaxisTooltipText = null
/** @type {HTMLElement | null} */
this.yaxisTooltip = null
/** @type {HTMLElement[] | null} */
this.yaxisTooltipText = null
/** @type {HTMLElement[] | null} */
this.yaxisTTEls = null
/** @type {number} */
this.xaxisOffY = 0
/** @type {number} */
this.yaxisOffX = 0
/** @type {number} */
this.xcrosshairsWidth = 0
/** @type {Element | null} */
this.ycrosshairs = null
/** @type {Element | null} */
this.ycrosshairsHidden = null
/** @type {Element | null} */
this.tooltip = null
/** @type {any} */
this.e = null
this.isBarShared = !w.globals.isBarHorizontal && this.tConfig.shared
this.lastHoverTime = Date.now()
this.dimensionUpdateScheduled = false
// Properties set in drawTooltip() / create() / event handlers
/** @type {import('../../types/internal').XYRatios | null} */
this.xyRatios = null
/** @type {boolean} */
this.isXAxisTooltipEnabled = false
/** @type {boolean[]} */
this.yaxisTooltips = []
/** @type {any} */
this.allTooltipSeriesGroups = []
/** @type {any} */
this.xAxisTicksPositions = null
/** @type {number} */
this.dataPointsDividedHeight = 0
/** @type {number} */
this.dataPointsDividedWidth = 0
/** @type {HTMLElement | null} */
this.tooltipTitle = null
/** @type {NodeListOf<Element> | null} */
this.legendLabels = null
/** @type {any} */
this.ttItems = null
/** @type {DOMRect | null} */
this.seriesBound = null
/** @type {ReturnType<typeof setTimeout> | undefined} */
this.seriesHoverTimeout = undefined
/** @type {number} */
this.clientX = 0
/** @type {number} */
this.clientY = 0
/** @type {number} */
this.barSeriesHeight = 0
/** @type {{ x: number, y: number, ttWidth: number, ttHeight: number }} */
this.tooltipRect = { x: 0, y: 0, ttWidth: 0, ttHeight: 0 }
}
setupDimensionCache() {
const w = this.w
const tooltipEl = this.getElTooltip()
if (!tooltipEl) return
// Initial dimension cache
this.updateDimensionCache()
// Setup ResizeObserver for automatic dimension updates
if (typeof ResizeObserver !== 'undefined' && !w.globals.resizeObserver) {
w.globals.resizeObserver = new ResizeObserver(() => {
if (!this.dimensionUpdateScheduled) {
this.dimensionUpdateScheduled = true
requestAnimationFrame(() => {
this.updateDimensionCache()
this.dimensionUpdateScheduled = false
})
}
})
w.globals.resizeObserver.observe(tooltipEl)
}
}
updateDimensionCache() {
const w = this.w
const tooltipEl = this.getElTooltip()
if (!tooltipEl) return
const rect = tooltipEl.getBoundingClientRect()
w.globals.dimensionCache.tooltip = /** @type {any} */ ({
width: rect.width,
height: rect.height,
lastUpdate: Date.now(),
})
}
getCachedDimensions() {
const w = this.w
// Return cached dimensions if available and fresh (< 1 second old)
if (w.globals.dimensionCache.tooltip) {
const cache = /** @type {Record<string,any>} */ (
w.globals.dimensionCache.tooltip
)
const age = Date.now() - cache.lastUpdate
if (age < 1000) {
return {
ttWidth: cache.width,
ttHeight: cache.height,
}
}
}
// Fallback to live measurement and update cache
this.updateDimensionCache()
const cache = /** @type {Record<string,any>} */ (
w.globals.dimensionCache.tooltip
)
return cache
? {
ttWidth: cache.width,
ttHeight: cache.height,
}
: { ttWidth: 0, ttHeight: 0 }
}
/**
* @param {{ w: import('../../types/internal').ChartStateW }} [ctx]
* @returns {HTMLElement | null}
*/
getElTooltip(ctx) {
if (!ctx) ctx = this
if (!ctx.w.dom.baseEl) return null
return /** @type {HTMLElement | null} */ (
ctx.w.dom.baseEl.querySelector('.apexcharts-tooltip')
)
}
getElXCrosshairs() {
return this.w.dom.baseEl.querySelector('.apexcharts-xcrosshairs')
}
getElGrid() {
return this.w.dom.baseEl.querySelector('.apexcharts-grid')
}
/**
* @param {import('../../types/internal').XYRatios} xyRatios
*/
drawTooltip(xyRatios) {
const w = this.w
this.xyRatios = xyRatios
this.isXAxisTooltipEnabled =
w.config.xaxis.tooltip.enabled && w.globals.axisCharts
/**
* @param {number} y
*/
this.yaxisTooltips = w.config.yaxis.map((y) => {
return y.show && y.tooltip.enabled && w.globals.axisCharts ? true : false
})
/** @type {any} */
this.allTooltipSeriesGroups = []
if (!w.globals.axisCharts) {
this.showTooltipTitle = false
}
// Remove any existing tooltip element to avoid duplicates on re-draws
// (e.g. fastUpdate path calls drawTooltip without full DOM teardown)
const existingTooltip = this.getElTooltip()
if (existingTooltip?.parentNode) {
existingTooltip.parentNode.removeChild(existingTooltip)
}
this.tooltipTitle = null
const tooltipEl = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'div',
)
tooltipEl.classList.add('apexcharts-tooltip')
if (w.config.tooltip.cssClass) {
tooltipEl.classList.add(w.config.tooltip.cssClass)
}
tooltipEl.classList.add(`apexcharts-theme-${this.tConfig.theme || 'light'}`)
// accessibility attributes
if (
w.config.chart.accessibility.enabled &&
w.config.chart.accessibility.announcements.enabled
) {
tooltipEl.setAttribute('role', 'tooltip')
tooltipEl.setAttribute('aria-live', 'polite')
tooltipEl.setAttribute('aria-atomic', 'true')
tooltipEl.setAttribute('aria-hidden', 'true')
}
w.dom.elWrap.appendChild(tooltipEl)
if (w.globals.axisCharts) {
this.axesTooltip.drawXaxisTooltip()
this.axesTooltip.drawYaxisTooltip()
this.axesTooltip.setXCrosshairWidth()
this.axesTooltip.handleYCrosshair()
const xAxis = new XAxis(this.w, this.ctx, undefined)
this.xAxisTicksPositions = xAxis.getXAxisTicksPositions()
}
// we forcefully set intersect true for these conditions
if (
(w.globals.comboCharts ||
this.tConfig.intersect ||
w.config.chart.type === 'rangeBar') &&
!this.tConfig.shared
) {
this.showOnIntersect = true
}
if (w.config.markers.size === 0 || w.globals.markers.largestSize === 0) {
// when user don't want to show points all the time, but only on when hovering on series
this.marker.drawDynamicPoints()
}
// no visible series, exit
if (w.globals.collapsedSeries.length === w.seriesData.series.length) return
this.dataPointsDividedHeight = w.layout.gridHeight / w.globals.dataPoints
this.dataPointsDividedWidth = w.layout.gridWidth / w.globals.dataPoints
if (this.showTooltipTitle) {
this.tooltipTitle = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'div',
)
this.tooltipTitle.classList.add('apexcharts-tooltip-title')
this.tooltipTitle.style.fontFamily =
this.tConfig.style.fontFamily || w.config.chart.fontFamily
this.tooltipTitle.style.fontSize = this.tConfig.style.fontSize
tooltipEl.appendChild(this.tooltipTitle)
}
let ttItemsCnt = w.seriesData.series.length // whether shared or not, default is shared
if ((w.globals.xyCharts || w.globals.comboCharts) && this.tConfig.shared) {
if (!this.showOnIntersect) {
ttItemsCnt = w.seriesData.series.length
} else {
ttItemsCnt = 1
}
}
this.legendLabels = w.dom.baseEl.querySelectorAll('.apexcharts-legend-text')
this.ttItems = this.createTTElements(ttItemsCnt)
this.addSVGEvents()
this.setupDimensionCache()
}
/**
* @param {number} ttItemsCnt
*/
createTTElements(ttItemsCnt) {
const w = this.w
/** @type {any[]} */
const ttItems = []
const tooltipEl = this.getElTooltip()
if (!tooltipEl) return ttItems
for (let i = 0; i < ttItemsCnt; i++) {
const gTxt = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'div',
)
gTxt.classList.add(
'apexcharts-tooltip-series-group',
`apexcharts-tooltip-series-group-${i}`,
)
gTxt.style.order = String(
w.config.tooltip.inverseOrder ? ttItemsCnt - i : i + 1,
)
const point = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'span',
)
point.classList.add('apexcharts-tooltip-marker')
if (w.config.tooltip.fillSeriesColor) {
point.style.backgroundColor = w.globals.colors[i]
} else {
point.style.color = w.globals.colors[i]
}
const mShape = w.config.markers.shape
let shape = mShape
if (Array.isArray(mShape)) {
shape = mShape[i]
}
point.setAttribute('shape', shape)
gTxt.appendChild(point)
const gYZ = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'div',
)
gYZ.classList.add('apexcharts-tooltip-text')
gYZ.style.fontFamily =
this.tConfig.style.fontFamily || w.config.chart.fontFamily
gYZ.style.fontSize = this.tConfig.style.fontSize
;['y', 'goals', 'z'].forEach((g) => {
const gValText = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'div',
)
gValText.classList.add(`apexcharts-tooltip-${g}-group`)
const txtLabel = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'span',
)
txtLabel.classList.add(`apexcharts-tooltip-text-${g}-label`)
gValText.appendChild(txtLabel)
const txtValue = BrowserAPIs.createElementNS(
'http://www.w3.org/1999/xhtml',
'span',
)
txtValue.classList.add(`apexcharts-tooltip-text-${g}-value`)
gValText.appendChild(txtValue)
gYZ.appendChild(gValText)
})
gTxt.appendChild(gYZ)
tooltipEl.appendChild(gTxt)
ttItems.push(gTxt)
}
return ttItems
}
addSVGEvents() {
const w = this.w
const type = w.config.chart.type
const tooltipEl = this.getElTooltip()
if (!tooltipEl) return
const commonBar = !!(
type === 'bar' ||
type === 'candlestick' ||
type === 'boxPlot' ||
type === 'rangeBar'
)
const chartWithmarkers =
type === 'area' ||
type === 'line' ||
type === 'scatter' ||
type === 'bubble' ||
type === 'radar'
const hoverArea = w.dom.Paper.node
const elGrid = this.getElGrid()
if (elGrid) {
this.seriesBound = elGrid.getBoundingClientRect()
}
/** @type {any[]} */
const tooltipY = []
/** @type {any[]} */
const tooltipX = []
const seriesHoverParams = {
hoverArea,
elGrid,
tooltipEl,
tooltipY,
tooltipX,
ttItems: this.ttItems,
}
let points
if (w.globals.axisCharts) {
if (chartWithmarkers) {
points = w.dom.baseEl.querySelectorAll(
".apexcharts-series[data\\:longestSeries='true'] .apexcharts-marker",
)
} else if (commonBar) {
points = w.dom.baseEl.querySelectorAll(
'.apexcharts-series .apexcharts-bar-area, .apexcharts-series .apexcharts-candlestick-area, .apexcharts-series .apexcharts-boxPlot-area, .apexcharts-series .apexcharts-rangebar-area',
)
} else if (type === 'heatmap' || type === 'treemap') {
points = w.dom.baseEl.querySelectorAll(
'.apexcharts-series .apexcharts-heatmap, .apexcharts-series .apexcharts-treemap',
)
}
if (points && points.length) {
for (let p = 0; p < points.length; p++) {
tooltipY.push(points[p].getAttribute('cy'))
tooltipX.push(points[p].getAttribute('cx'))
}
}
}
const validSharedChartTypes =
(w.globals.xyCharts && !this.showOnIntersect) ||
(w.globals.comboCharts && !this.showOnIntersect) ||
(commonBar && this.tooltipUtil.hasBars() && this.tConfig.shared)
if (validSharedChartTypes) {
this.addPathsEventListeners([hoverArea], seriesHoverParams)
} else if (
(commonBar && !w.globals.comboCharts) ||
(chartWithmarkers && this.showOnIntersect)
) {
this.addDatapointEventsListeners(seriesHoverParams)
} else if (
!w.globals.axisCharts ||
type === 'heatmap' ||
type === 'treemap'
) {
const seriesAll = w.dom.baseEl.querySelectorAll('.apexcharts-series')
this.addPathsEventListeners(seriesAll, seriesHoverParams)
}
if (this.showOnIntersect) {
const lineAreaPoints = w.dom.baseEl.querySelectorAll(
'.apexcharts-line-series .apexcharts-marker, .apexcharts-area-series .apexcharts-marker',
)
if (lineAreaPoints.length > 0) {
// if we find any lineSeries, addEventListeners for them
this.addPathsEventListeners(lineAreaPoints, seriesHoverParams)
}
// combo charts may have bars, so add event listeners here too
if (this.tooltipUtil.hasBars() && !this.tConfig.shared) {
this.addDatapointEventsListeners(seriesHoverParams)
}
}
}
drawFixedTooltipRect() {
const w = this.w
const tooltipEl = this.getElTooltip()
if (!tooltipEl) return { x: 0, y: 0, ttWidth: 0, ttHeight: 0 }
const tooltipRect = tooltipEl.getBoundingClientRect()
const ttWidth = tooltipRect.width + 10
const ttHeight = tooltipRect.height + 10
let x = this.tConfig.fixed.offsetX
let y = this.tConfig.fixed.offsetY
const fixed = this.tConfig.fixed.position.toLowerCase()
if (fixed.indexOf('right') > -1) {
x = x + w.globals.svgWidth - ttWidth + 10
}
if (fixed.indexOf('bottom') > -1) {
y = y + w.globals.svgHeight - ttHeight - 10
}
tooltipEl.style.left = x + 'px'
tooltipEl.style.top = y + 'px'
return {
x,
y,
ttWidth,
ttHeight,
}
}
/**
* @param {Record<string, any>} seriesHoverParams
*/
addDatapointEventsListeners(seriesHoverParams) {
const w = this.w
const points = w.dom.baseEl.querySelectorAll(
'.apexcharts-series-markers .apexcharts-marker, .apexcharts-bar-area, .apexcharts-candlestick-area, .apexcharts-boxPlot-area, .apexcharts-rangebar-area',
)
this.addPathsEventListeners(points, seriesHoverParams)
}
/**
* @param {any} paths
* @param {Record<string, any>} opts
*/
addPathsEventListeners(paths, opts) {
const self = this
for (let p = 0; p < paths.length; p++) {
const extendedOpts = {
paths: paths[p],
tooltipEl: opts.tooltipEl,
tooltipY: opts.tooltipY,
tooltipX: opts.tooltipX,
elGrid: opts.elGrid,
hoverArea: opts.hoverArea,
ttItems: opts.ttItems,
}
const events = [
'mousemove',
'mouseup',
'touchmove',
'mouseout',
'touchend',
]
events.map((ev) => {
return paths[p].addEventListener(
ev,
self.onSeriesHover.bind(self, extendedOpts),
{ capture: false, passive: true },
)
})
}
}
/*
** Check to see if the tooltips should be updated based on a mouse / touch event
* @param {Record<string, any>} opt
* @param {Event} e
*/
/** @param {Record<string, any>} opt @param {any} e */
onSeriesHover(opt, e) {
// If a user is moving their mouse quickly, don't bother updating the tooltip every single frame
const targetDelay = 20
const timeSinceLastUpdate = Date.now() - this.lastHoverTime
if (timeSinceLastUpdate >= targetDelay) {
// The tooltip was last updated over 100ms ago - redraw it even if the user is still moving their
// mouse so they get some feedback that their moves are being registered
this.seriesHover(opt, e)
} else {
// The tooltip was last updated less than 100ms ago
// Cancel any other delayed draw, so we don't show stale data
clearTimeout(this.seriesHoverTimeout)
// Schedule the next draw so that it happens about 100ms after the last update
this.seriesHoverTimeout = setTimeout(() => {
this.seriesHover(opt, e)
}, targetDelay - timeSinceLastUpdate)
}
}
/*
** The actual series hover function
* @param {Record<string, any>} opt
* @param {Event} e
*/
/** @param {Record<string, any>} opt @param {any} e */
seriesHover(opt, e) {
this.lastHoverTime = Date.now()
let chartGroups = []
const w = this.w
// if user has more than one charts in group, we need to sync
if (w.config.chart.group) {
chartGroups = this.ctx.getGroupedCharts()
}
if (
w.globals.axisCharts &&
((w.globals.minX === -Infinity && w.globals.maxX === Infinity) ||
w.globals.dataPoints === 0)
) {
return
}
if (chartGroups.length) {
/**
* @param {Record<string, any>} ch
*/
chartGroups.forEach((ch) => {
const tooltipEl = this.getElTooltip(ch)
const newOpts = {
paths: opt.paths,
tooltipEl,
tooltipY: opt.tooltipY,
tooltipX: opt.tooltipX,
elGrid: opt.elGrid,
hoverArea: opt.hoverArea,
ttItems: ch.w.globals.tooltip.ttItems,
}
// all the charts should have the same minX and maxX (same xaxis) for multiple tooltips to work correctly
if (
ch.w.globals.minX === this.w.globals.minX &&
ch.w.globals.maxX === this.w.globals.maxX
) {
ch.w.globals.tooltip.seriesHoverByContext({
chartCtx: ch,
ttCtx: ch.w.globals.tooltip,
opt: newOpts,
e,
})
}
})
} else {
this.seriesHoverByContext({
chartCtx: this.ctx,
ttCtx: this.w.globals.tooltip,
opt,
e,
})
}
}
/** @param {{chartCtx: any, ttCtx: any, opt: any, e: any}} opts */
seriesHoverByContext({ chartCtx, ttCtx, opt, e }) {
const w = chartCtx.w
const tooltipEl = this.getElTooltip(chartCtx)
if (!tooltipEl) return
// use cached dimensions instead of live getBoundingClientRect
const cachedDims = ttCtx.getCachedDimensions()
ttCtx.tooltipRect = {
x: 0,
y: 0,
ttWidth: cachedDims.ttWidth,
ttHeight: cachedDims.ttHeight,
}
ttCtx.e = e
// highlight the current hovered bars
if (
ttCtx.tooltipUtil.hasBars() &&
!w.globals.comboCharts &&
!ttCtx.isBarShared
) {
if (this.tConfig.onDatasetHover.highlightDataSeries) {
const series = new Series(chartCtx.w)
series.toggleSeriesOnHover(e, e.target.parentNode)
}
}
if (w.globals.axisCharts) {
ttCtx.axisChartsTooltips({
e,
opt,
tooltipRect: ttCtx.tooltipRect,
})
} else {
// non-plot charts i.e pie/donut/circle
ttCtx.nonAxisChartsTooltips({
e,
opt,
tooltipRect: ttCtx.tooltipRect,
})
}
if (ttCtx.fixedTooltip) {
ttCtx.drawFixedTooltipRect()
}
}
// tooltip handling for line/area/bar/columns/scatter
/** @param {{e: any, opt: any}} opts */
axisChartsTooltips({ e, opt }) {
const w = this.w
let x, y
const seriesBound = opt.elGrid.getBoundingClientRect()
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX
const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY
this.clientY = clientY
this.clientX = clientX
w.interact.capturedSeriesIndex = -1
w.interact.capturedDataPointIndex = -1
if (
clientY < seriesBound.top ||
clientY > seriesBound.top + seriesBound.height
) {
this.handleMouseOut(opt)
return
}
if (
Array.isArray(this.tConfig.enabledOnSeries) &&
!w.config.tooltip.shared
) {
const index = parseInt(opt.paths.getAttribute('index'), 10)
if (this.tConfig.enabledOnSeries.indexOf(index) < 0) {
this.handleMouseOut(opt)
return
}
}
const tooltipEl = this.getElTooltip()
if (!tooltipEl) return
const xcrosshairs = this.getElXCrosshairs()
let syncedCharts = []
if (w.config.chart.group) {
// we need to fallback to sticky tooltip in case charts are synced
syncedCharts = this.ctx.getSyncedCharts()
}
const isStickyTooltip =
w.globals.xyCharts ||
(w.config.chart.type === 'bar' &&
!w.globals.isBarHorizontal &&
this.tooltipUtil.hasBars() &&
this.tConfig.shared) ||
(w.globals.comboCharts && this.tooltipUtil.hasBars())
if (
e.type === 'mousemove' ||
e.type === 'touchmove' ||
e.type === 'mouseup'
) {
// there is no series to hover over
if (
w.globals.collapsedSeries.length +
w.globals.ancillaryCollapsedSeries.length ===
w.seriesData.series.length
) {
return
}
if (xcrosshairs !== null) {
xcrosshairs.classList.add('apexcharts-active')
}
const hasYAxisTooltip = this.yaxisTooltips?.filter(
(/** @type {any} */ b) => {
return b === true
},
)
// ycrosshairs is set dynamically during drawTooltip()
const _yc = /** @type {any} */ (this).ycrosshairs
if (_yc !== null && hasYAxisTooltip?.length) {
_yc.classList.add('apexcharts-active')
}
if (
(isStickyTooltip && !this.showOnIntersect) ||
syncedCharts.length > 1
) {
this.handleStickyTooltip(e, clientX, clientY, opt)
} else {
if (
w.config.chart.type === 'heatmap' ||
w.config.chart.type === 'treemap'
) {
const markerXY = this.intersect.handleHeatTreeTooltip({
e,
opt,
x,
y,
type: w.config.chart.type,
})
x = markerXY.x
y = markerXY.y
tooltipEl.style.left = x + 'px'
tooltipEl.style.top = y + 'px'
} else {
if (this.tooltipUtil.hasBars()) {
this.intersect.handleBarTooltip({
e,
opt,
})
}
if (this.tooltipUtil.hasMarkers(0)) {
// intersect - line/area/scatter/bubble
this.intersect.handleMarkerTooltip({
e,
opt,
x,
y,
})
}
}
}
if (this.yaxisTooltips && this.yaxisTooltips.length) {
for (let yt = 0; yt < w.config.yaxis.length; yt++) {
this.axesTooltip.drawYaxisTooltipText(
yt,
clientY,
/** @type {import('../../types/internal').XYRatios} */ (
this.xyRatios
),
)
}
}
w.dom.baseEl.classList.add('apexcharts-tooltip-active')
opt.tooltipEl.classList.add('apexcharts-active')
if (
w.config.chart.accessibility.enabled &&
w.config.chart.accessibility.announcements.enabled
) {
opt.tooltipEl.removeAttribute('aria-hidden')
}
} else if (e.type === 'mouseout' || e.type === 'touchend') {
this.handleMouseOut(opt)
}
}
// tooltip handling for pie/donuts
/** @param {{e: any, opt: any, tooltipRect: any}} opts */
nonAxisChartsTooltips({ e, opt, tooltipRect }) {
const w = this.w
const rel = opt.paths.getAttribute('rel')
const tooltipEl = this.getElTooltip()
if (!tooltipEl) return
const seriesBound = w.dom.elWrap.getBoundingClientRect()
if (e.type === 'mousemove' || e.type === 'touchmove') {
w.dom.baseEl.classList.add('apexcharts-tooltip-active')
tooltipEl.classList.add('apexcharts-active')
if (
w.config.chart.accessibility.enabled &&
w.config.chart.accessibility.announcements.enabled
) {
tooltipEl.removeAttribute('aria-hidden')
}
this.tooltipLabels.drawSeriesTexts({
ttItems: opt.ttItems,
i: parseInt(rel, 10) - 1,
shared: false,
})
let x, y
// opt.paths is the <g class="apexcharts-series"> group element;
// data:cx / data:cy are set on the child <path> arc element inside it
const arcPath = opt.paths.querySelector('path[data\\:cx]') || opt.paths
if (
w.config.tooltip.intersect &&
arcPath.hasAttribute('data:cx') &&
arcPath.hasAttribute('data:cy')
) {
const svgBound = w.dom.Paper.node.getBoundingClientRect()
x =
svgBound.left -
seriesBound.left +
parseFloat(arcPath.getAttribute('data:cx')) -
tooltipRect.ttWidth / 2
y =
svgBound.top -
seriesBound.top +
parseFloat(arcPath.getAttribute('data:cy')) -
tooltipRect.ttHeight -
10
} else {
x =
(w.interact.clientX ?? 0) - seriesBound.left - tooltipRect.ttWidth / 2
y =
(w.interact.clientY ?? 0) -
seriesBound.top -
tooltipRect.ttHeight -
10
}
tooltipEl.style.left = x + 'px'
tooltipEl.style.top = y + 'px'
if (w.config.legend.tooltipHoverFormatter) {
const legendFormatter = w.config.legend.tooltipHoverFormatter
const i = rel - 1
const legendEl = /** @type {HTMLElement | undefined} */ (
this.legendLabels?.[i]
)
if (!legendEl) return
const legendName = legendEl.getAttribute('data:default-text')
const text = legendFormatter(legendName, {
seriesIndex: i,
dataPointIndex: i,
w,
})
legendEl.innerHTML = text
}
} else if (e.type === 'mouseout' || e.type === 'touchend') {
tooltipEl.classList.remove('apexcharts-active')
w.dom.baseEl.classList.remove('apexcharts-tooltip-active')
if (w.config.legend.tooltipHoverFormatter) {
this.legendLabels?.forEach((l) => {
const defaultText = l.getAttribute('data:default-text')
/** @type {HTMLElement} */ l.innerHTML = decodeURIComponent(
defaultText ?? '',
)
})
}
}
}
/**
* @param {Event} e
* @param {number} clientX
* @param {number} clientY
* @param {Record<string, any>} opt
*/
handleStickyTooltip(e, clientX, clientY, opt) {
const w = this.w
const capj = this.tooltipUtil.getNearestValues({
context: this,
hoverArea: opt.hoverArea,
elGrid: opt.elGrid,
clientX,
clientY,
})
const j = capj.j
let capturedSeries = capj.capturedSeries
if (
capturedSeries !== null &&
w.globals.collapsedSeriesIndices.includes(capturedSeries ?? -1)
)
capturedSeries = null
const bounds = opt.elGrid.getBoundingClientRect()
if (capj.hoverX < 0 || capj.hoverX > bounds.width) {
this.handleMouseOut(opt)
return
}
if (capturedSeries !== null) {
this.handleStickyCapturedSeries(e, capturedSeries ?? -1, opt, j ?? 0)
} else {
// couldn't capture any series. check if shared X is same,
// if yes, draw a grouped tooltip
if (this.tooltipUtil.isXoverlap(j ?? 0) || w.globals.isBarHorizontal) {
const firstVisibleSeries = w.seriesData.series.findIndex(
/**
* @param {any} s
* @param {number} i
*/
(s, i) => !w.globals.collapsedSeriesIndices.includes(i),
)
this.create(e, this, firstVisibleSeries, j ?? 0, opt.ttItems)
}
}
}
/**
* @param {Event} e
* @param {number} capturedSeries
* @param {Record<string, any>} opt
* @param {number} j
*/
handleStickyCapturedSeries(e, capturedSeries, opt, j) {
const w = this.w
if (!this.tConfig.shared) {
const ignoreNull = w.seriesData.series[capturedSeries][j] === null
if (ignoreNull) {
this.handleMouseOut(opt)
return
}
}
if (typeof w.seriesData.series[capturedSeries][j] !== 'undefined') {
if (
this.tConfig.shared &&
this.tooltipUtil.isXoverlap(j) &&
this.tooltipUtil.isInitialSeriesSameLen()
) {
this.create(e, this, capturedSeries, j, opt.ttItems)
} else {
this.create(e, this, capturedSeries, j, opt.ttItems, false)
}
} else {
if (this.tooltipUtil.isXoverlap(j)) {
const firstVisibleSeries = w.seriesData.series.findIndex(
/**
* @param {any} s
* @param {number} i
*/
(s, i) => !w.globals.collapsedSeriesIndices.includes(i),
)
this.create(e, this, firstVisibleSeries, j, opt.ttItems)
}
}
}
deactivateHoverFilter() {
const w = this.w
const graphics = new Graphics(this.w, this.ctx)
const allPaths = w.dom.Paper.find(`.apexcharts-bar-area`)
for (let b = 0; b < allPaths.length; b++) {
graphics.pathMouseLeave(
/** @type {any} */ (allPaths[b]),
/** @type {any} */ (undefined),
)
}
}
/**
* @param {Record<string, any>} opt
*/
handleMouseOut(opt) {
const w = this.w
const xcrosshairs = this.getElXCrosshairs()
w.dom.baseEl.classList.remove('apexcharts-tooltip-active')
opt.tooltipEl.classList.remove('apexcharts-active')
if (
w.config.chart.accessibility.enabled &&
w.config.chart.accessibility.announcements.enabled
) {
opt.tooltipEl.setAttribute('aria-hidden', 'true')
}
this.deactivateHoverFilter()
if (w.config.chart.type !== 'bubble') {
this.marker.resetPointsSize()
}
if (xcrosshairs !== null) {
xcrosshairs.classList.remove('apexcharts-active')
}
// ycrosshairs is set dynamically during drawTooltip()
const _yc2 = /** @type {any} */ (this).ycrosshairs
if (_yc2 !== null) {
_yc2.classList.remove('apexcharts-active')
}
if (this.isXAxisTooltipEnabled) {
this.xaxisTooltip?.classList.remove('apexcharts-active')
}
if (this.yaxisTooltips && this.yaxisTooltips.length) {
if (this.yaxisTTEls === null) {
this.yaxisTTEls = /** @type {HTMLElement[]} */ ([
...w.dom.baseEl.querySelectorAll('.apexcharts-yaxistooltip'),
])
}
for (let i = 0; i < this.yaxisTTEls.length; i++) {
this.yaxisTTEls[i].classList.remove('apexcharts-active')
}
}
if (w.config.legend.tooltipHoverFormatter) {
this.legendLabels?.forEach((l) => {
const defaultText = l.getAttribute('data:default-text')
/** @type {HTMLElement} */ l.innerHTML = decodeURIComponent(
defaultText ?? '',
)
})
}
}
/**
* @param {Event} e
* @param {number} seriesIndex
* @param {number} dataPointIndex
*/
markerClick(e, seriesIndex, dataPointIndex) {
const w = this.w
if (typeof w.config.chart.events.markerClick === 'function') {
w.config.chart.events.markerClick(e, this.ctx, {
seriesIndex,
dataPointIndex,
w,
})
}
this.ctx.events.fireEvent('markerClick', [
e,
this.ctx,
{ seriesIndex, dataPointIndex, w },
])
}
/**
* @param {Event} e
* @param {any} context
* @param {number} capturedSeries
* @param {number} j
* @param {any} ttItems
* @param {boolean | null} shared
*/
create(e, context, capturedSeries, j, ttItems, shared = null) {
const w = this.w
const ttCtx = context
if (e.type === 'mouseup') {
this.markerClick(e, capturedSeries, j)
}
if (shared === null) shared = this.tConfig.shared
const hasMarkers = this.tooltipUtil.hasMarkers(capturedSeries)
const bars = this.tooltipUtil.getElBars()
const handlePoints = () => {
if (w.globals.markers.largestSize > 0) {
ttCtx.marker.enlargePoints(j)
} else {
ttCtx.tooltipPosition.moveDynamicPointsOnHover(j)
}
}
if (w.config.legend.tooltipHoverFormatter) {
const legendFormatter = w.config.legend.tooltipHoverFormatter
const els = /** @type {HTMLElement[]} */ (
Array.from(this.legendLabels ?? [])
)
// reset all legend values first
els.forEach((l) => {
const legendName = l.getAttribute('data:default-text')
l.innerHTML = decodeURIComponent(legendName ?? '')
})
// for irregular time series
for (let i = 0; i < els.length; i++) {
const l = els[i]
const lsIndex = parseInt(l.getAttribute('i') ?? '', 10)
const legendName = decodeURIComponent(
l.getAttribute('data:default-text') ?? '',
)
const text = legendFormatter(legendName, {
seriesIndex: shared ? lsIndex : capturedSeries,
dataPointIndex: j,
w,
})
if (!shared) {
l.innerHTML = lsIndex === capturedSeries ? text : legendName
if (capturedSeries === lsIndex) {
break
}
} else {
l.innerHTML =
w.globals.collapsedSeriesIndices.indexOf(lsIndex) < 0
? text
: legendName
}
}
}
const _rangeData = /** @type {any} */ (w.rangeData)
const commonSeriesTextsParams = {
ttItems,
i: capturedSeries,
j,
...(_rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y1 !==
undefined && {
y1: _rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y1,
}),
...(_rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y2 !==
undefined && {
y2: _rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y2,
}),
}
if (shared) {
ttCtx.tooltipLabels.drawSeriesTexts({
...commonSeriesTextsParams,
shared: this.showOnIntersect ? false : this.tConfig.shared,
})
if (hasMarkers) {
handlePoints()
} else if (this.tooltipUtil.hasBars()) {
this.barSeriesHeight = this.tooltipUtil.getBarsHeight(
/** @type {any[]} */ ([...bars]),
)
if (this.barSeriesHeight > 0) {
// hover state, activate snap filter
const graphics = new Graphics(this.w, this.ctx)
const paths = w.dom.Paper.find(`.apexcharts-bar-area[j='${j}']`)
// de-activate first
this.deactivateHoverFilter()
const points = ttCtx.tooltipUtil.getAllMarkers(true)
if (points.length && !this.barSeriesHeight) {
handlePoints()
}
ttCtx.tooltipPosition.moveStickyTooltipOverBars(j, capturedSeries)
for (let b = 0; b < paths.length; b++) {
graphics.pathMouseEnter(
/** @type {any} */ (paths[b]),
/** @type {any} */ (undefined),
)
}
}
}
} else {
ttCtx.tooltipLabels.drawSeriesTexts({
shared: false,
...commonSeriesTextsParams,
})
if (this.tooltipUtil.hasBars()) {
ttCtx.tooltipPosition.moveStickyTooltipOverBars(j, capturedSeries)
}
if (hasMarkers) {
ttCtx.tooltipPosition.moveMarkers(capturedSeries, j)
}
}
}
}