UNPKG

apexcharts

Version:

A JavaScript Chart Library

459 lines (394 loc) 12.5 kB
// @ts-check import Utilities from '../../utils/Utils' import Graphics from '../Graphics' /** * ApexCharts Tooltip.Utils Class to support Tooltip functionality. * * @module Tooltip.Utils **/ export default class Utils { /** * @param {import('./Tooltip').default} tooltipContext */ constructor(tooltipContext) { this.w = tooltipContext.w this.ttCtx = tooltipContext } /** ** When hovering over series, you need to capture which series is being hovered on. ** This function will return both capturedseries index as well as inner index of that series * @memberof Utils * @param {{ hoverArea: any, elGrid: any, clientX: any, clientY: any, context?: any }} opts */ getNearestValues({ hoverArea, elGrid, clientX, clientY }) { const w = this.w const seriesBound = elGrid.getBoundingClientRect() const hoverWidth = seriesBound.width const hoverHeight = seriesBound.height let xDivisor = hoverWidth / (w.globals.dataPoints - 1) const yDivisor = hoverHeight / w.globals.dataPoints const hasBars = this.hasBars() if ( (w.globals.comboCharts || hasBars) && !w.config.xaxis.convertedCatToNumeric ) { xDivisor = hoverWidth / w.globals.dataPoints } const hoverX = clientX - seriesBound.left - w.globals.barPadForNumericAxis const hoverY = clientY - seriesBound.top const notInRect = hoverX < 0 || hoverY < 0 || hoverX > hoverWidth || hoverY > hoverHeight if (notInRect) { hoverArea.classList.remove('hovering-zoom') hoverArea.classList.remove('hovering-pan') } else { if (w.interact.zoomEnabled) { hoverArea.classList.remove('hovering-pan') hoverArea.classList.add('hovering-zoom') } else if (w.interact.panEnabled) { hoverArea.classList.remove('hovering-zoom') hoverArea.classList.add('hovering-pan') } } let j = Math.round(hoverX / xDivisor) const jHorz = Math.floor(hoverY / yDivisor) if (hasBars && !w.config.xaxis.convertedCatToNumeric) { j = Math.ceil(hoverX / xDivisor) j = j - 1 } let capturedSeries = null let closest = null /** * @param {number[]} seriesXVal */ let seriesXValArr = w.globals.seriesXvalues.map( (/** @type {any} */ seriesXVal) => { /** * @param {number} s */ return seriesXVal.filter((/** @type {any} */ s) => Utilities.isNumber(s), ) }, ) /** * @param {number[]} seriesYVal */ const seriesYValArr = w.globals.seriesYvalues.map( (/** @type {any} */ seriesYVal) => { /** * @param {number} s */ return seriesYVal.filter((/** @type {any} */ s) => Utilities.isNumber(s), ) }, ) // if X axis type is not category and tooltip is not shared, then we need to find the cursor position and get the nearest value if (w.axisFlags.isXNumeric) { // Change origin of cursor position so that we can compute the relative nearest point to the cursor on our chart // we only need to scale because all points are relative to the bounds.left and bounds.top => origin is virtually (0, 0) const chartGridEl = this.ttCtx.getElGrid() if (!chartGridEl) return { hoverX, hoverY } const chartGridElBoundingRect = chartGridEl.getBoundingClientRect() const transformedHoverX = hoverX * (chartGridElBoundingRect.width / hoverWidth) const transformedHoverY = hoverY * (chartGridElBoundingRect.height / hoverHeight) closest = this.closestInMultiArray( transformedHoverX, transformedHoverY, seriesXValArr, seriesYValArr, ) capturedSeries = closest.index j = closest.j ?? 0 if (capturedSeries !== null && w.globals.hasNullValues) { // initial push, it should be a little smaller than the 1st val seriesXValArr = w.globals.seriesXvalues[capturedSeries] closest = this.closestInArray(transformedHoverX, seriesXValArr) j = closest.j ?? 0 } } w.interact.capturedSeriesIndex = capturedSeries === null ? -1 : capturedSeries if (!j || j < 1) j = 0 if (w.globals.isBarHorizontal) { w.interact.capturedDataPointIndex = jHorz } else { w.interact.capturedDataPointIndex = j } return { capturedSeries, j: w.globals.isBarHorizontal ? jHorz : j, hoverX, hoverY, } } /** * @param {any[]} Xarrays */ getFirstActiveXArray(Xarrays) { const w = this.w let activeIndex = 0 /** * @param {number[]} xarr * @param {number} index */ const firstActiveSeriesIndex = Xarrays.map( (/** @type {any} */ xarr, /** @type {any} */ index) => { return xarr.length > 0 ? index : -1 }, ) for (let a = 0; a < firstActiveSeriesIndex.length; a++) { if ( firstActiveSeriesIndex[a] !== -1 && w.globals.collapsedSeriesIndices.indexOf(a) === -1 && w.globals.ancillaryCollapsedSeriesIndices.indexOf(a) === -1 ) { activeIndex = firstActiveSeriesIndex[a] break } } return activeIndex } /** * @param {number} hoverX * @param {number} hoverY * @param {any[]} Xarrays * @param {any[]} Yarrays */ closestInMultiArray(hoverX, hoverY, Xarrays, Yarrays) { const w = this.w // Determine which series are active (not collapsed) /** * @param {number} seriesIndex */ const isActiveSeries = (seriesIndex) => { return ( w.globals.collapsedSeriesIndices.indexOf(seriesIndex) === -1 && w.globals.ancillaryCollapsedSeriesIndices.indexOf(seriesIndex) === -1 ) } let closestDist = Infinity let closestSeriesIndex = null let closestPointIndex = null // Iterate through all series and points to find the closest (x,y) to (hoverX, hoverY) for (let i = 0; i < Xarrays.length; i++) { if (!isActiveSeries(i)) { continue } const xArr = Xarrays[i] const yArr = Yarrays[i] const len = Math.min(xArr.length, yArr.length) for (let j = 0; j < len; j++) { const xVal = xArr[j] const distX = hoverX - xVal let dist = Math.sqrt(distX * distX) if (!w.globals.allSeriesHasEqualX) { const yVal = yArr[j] const distY = hoverY - yVal dist = Math.sqrt(distX * distX + distY * distY) } if (dist < closestDist) { closestDist = dist closestSeriesIndex = i closestPointIndex = j } } } return { index: closestSeriesIndex, j: closestPointIndex, } } /** * @param {number} val * @param {any[]} arr */ closestInArray(val, arr) { const curr = arr[0] let currIndex = null let diff = Math.abs(val - curr) for (let i = 0; i < arr.length; i++) { const newdiff = Math.abs(val - arr[i]) if (newdiff < diff) { diff = newdiff currIndex = i } } return { j: currIndex, } } /** * When there are multiple series, it is possible to have different x values for each series. * But it may be possible in those multiple series, that there is same x value for 2 or more * series. * @memberof Utils * @param {number} j - the inner index of series (series[i][j]) * @return {boolean} */ isXoverlap(j) { const w = this.w const xSameForAllSeriesJArr = [] /** * @param {number[]} s */ const seriesX = w.seriesData.seriesX.filter( (/** @type {any} */ s) => typeof s[0] !== 'undefined', ) if (seriesX.length > 0) { for (let i = 0; i < seriesX.length - 1; i++) { if ( typeof seriesX[i][j] !== 'undefined' && typeof seriesX[i + 1][j] !== 'undefined' ) { if (seriesX[i][j] !== seriesX[i + 1][j]) { xSameForAllSeriesJArr.push('unEqual') } } } } if (xSameForAllSeriesJArr.length === 0) { return true } return false } isInitialSeriesSameLen() { let sameLen = true const initialSeries = /** @type {any[]} */ (this.w.globals.initialSeries)?.filter( /** * @param {Record<string, any>} s * @param {number} i */ (s, i) => !this.w.globals.collapsedSeriesIndices?.includes(i), ) || [] for (let i = 0; i < initialSeries.length - 1; i++) { if (!initialSeries[i]?.data || !initialSeries[i + 1]?.data) return true if (initialSeries[i].data.length !== initialSeries[i + 1].data.length) { sameLen = false break } } return sameLen } /** * @param {any[]} allbars */ getBarsHeight(allbars) { const bars = [...allbars] const totalHeight = bars.reduce((acc, bar) => acc + bar.getBBox().height, 0) return totalHeight } /** * @param {number} capturedSeries */ getElMarkers(capturedSeries) { // The selector .apexcharts-series-markers-wrap > * includes marker groups for which the // .apexcharts-series-markers class is not added due to null values or discrete markers if (typeof capturedSeries == 'number') { return this.w.dom.baseEl.querySelectorAll( `.apexcharts-series[data\\:realIndex='${capturedSeries}'] .apexcharts-series-markers-wrap > *`, ) } return this.w.dom.baseEl.querySelectorAll( '.apexcharts-series-markers-wrap > *', ) } getAllMarkers(filterCollapsed = false) { // first get all marker parents. This parent class contains series-index // which helps to sort the markers as they are dynamic let markersWraps = /** @type {any[]} */ ([ ...this.w.dom.baseEl.querySelectorAll('.apexcharts-series-markers-wrap'), ]) if (filterCollapsed) { /** * @param {any} m */ markersWraps = markersWraps.filter((/** @type {any} */ m) => { const realIndex = Number(m.getAttribute('data:realIndex')) return this.w.globals.collapsedSeriesIndices.indexOf(realIndex) === -1 }) } /** * @param {any} a * @param {any} b */ markersWraps.sort((/** @type {any} */ a, /** @type {any} */ b) => { var indexA = Number(a.getAttribute('data:realIndex')) var indexB = Number(b.getAttribute('data:realIndex')) return indexB < indexA ? 1 : indexB > indexA ? -1 : 0 }) /** @type {any[]} */ const markers = [] /** * @param {any} m */ markersWraps.forEach((/** @type {any} */ m) => { markers.push(m.querySelector('.apexcharts-marker')) }) return markers } /** * @param {number} capturedSeries */ hasMarkers(capturedSeries) { const markers = this.getElMarkers(capturedSeries) return markers.length > 0 } /** * @param {any} point * @param {number} size */ getPathFromPoint(point, size) { const cx = Number(point.getAttribute('cx')) const cy = Number(point.getAttribute('cy')) const shape = point.getAttribute('shape') return new Graphics(this.w).getMarkerPath(cx, cy, shape, size) } getElBars() { return this.w.dom.baseEl.querySelectorAll( '.apexcharts-bar-series, .apexcharts-candlestick-series, .apexcharts-boxPlot-series, .apexcharts-rangebar-series', ) } hasBars() { const bars = this.getElBars() return bars.length > 0 } /** * @param {number} index */ getHoverMarkerSize(index) { const w = this.w let hoverSize = w.config.markers.hover.size if (hoverSize === undefined) { hoverSize = w.globals.markers.size[index] + w.config.markers.hover.sizeOffset } return hoverSize } /** * @param {string} state */ toggleAllTooltipSeriesGroups(state) { const w = this.w const ttCtx = this.ttCtx if (ttCtx.allTooltipSeriesGroups.length === 0) { ttCtx.allTooltipSeriesGroups = w.dom.baseEl.querySelectorAll( '.apexcharts-tooltip-series-group', ) } const allTooltipSeriesGroups = ttCtx.allTooltipSeriesGroups for (let i = 0; i < allTooltipSeriesGroups.length; i++) { if (state === 'enable') { allTooltipSeriesGroups[i].classList.add('apexcharts-active') allTooltipSeriesGroups[i].style.display = w.config.tooltip.items.display } else { allTooltipSeriesGroups[i].classList.remove('apexcharts-active') allTooltipSeriesGroups[i].style.display = 'none' } } } }