UNPKG

apexcharts

Version:

A JavaScript Chart Library

752 lines (624 loc) 21.9 kB
import Labels from './Labels' import Position from './Position' import Marker from './Marker' import Intersect from './Intersect' import AxesTooltip from './AxesTooltip' 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 { constructor (ctx) { this.ctx = ctx this.w = ctx.w const w = this.w 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 = w.config.tooltip.intersect this.showTooltipTitle = w.config.tooltip.x.show this.fixedTooltip = w.config.tooltip.fixed.enabled this.xaxisTooltip = null this.yaxisTTEls = null this.isBarHorizontal = w.config.plotOptions.bar.horizontal this.isBarShared = (!w.config.plotOptions.bar.horizontal && w.config.tooltip.shared) } getElTooltip (ctx) { if (!ctx) ctx = this return ctx.w.globals.dom.baseEl.querySelector('.apexcharts-tooltip') } getElXCrosshairs () { return this.w.globals.dom.baseEl.querySelector( '.apexcharts-xcrosshairs' ) } getElGrid () { return this.w.globals.dom.baseEl.querySelector( '.apexcharts-grid' ) } drawTooltip (xyRatios) { let w = this.w this.xyRatios = xyRatios this.blxaxisTooltip = w.config.xaxis.tooltip.enabled && w.globals.axisCharts this.blyaxisTooltip = w.config.yaxis[0].tooltip.enabled && w.globals.axisCharts this.allTooltipSeriesGroups = [] if (!w.globals.axisCharts) { this.showTooltipTitle = false } const tooltipEl = document.createElement('div') tooltipEl.classList.add('apexcharts-tooltip') tooltipEl.classList.add(w.config.tooltip.theme) w.globals.dom.elWrap.appendChild(tooltipEl) if (w.globals.axisCharts) { this.axesTooltip.drawXaxisTooltip() this.axesTooltip.drawYaxisTooltip() this.axesTooltip.setXCrosshairWidth() this.axesTooltip.handleYCrosshair() let xAxis = new XAxis(this.ctx) this.xAxisTicksPositions = xAxis.getXAxisTicksPositions() } // we forcefully set intersect true for these conditions if ( (w.globals.comboCharts && !w.config.tooltip.shared) || (w.config.tooltip.intersect && !w.config.tooltip.shared) || (w.config.chart.type === 'bar' && !w.config.tooltip.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(this) } // no visible series, exit if (w.globals.collapsedSeries.length === w.globals.series.length) return this.dataPointsDividedHeight = w.globals.gridHeight / w.globals.dataPoints this.dataPointsDividedWidth = w.globals.gridWidth / w.globals.dataPoints if (this.showTooltipTitle) { this.tooltipTitle = document.createElement('div') this.tooltipTitle.classList.add('apexcharts-tooltip-title') this.tooltipTitle.style.fontFamily = w.config.tooltip.style.fontFamily || w.config.chart.fontFamily this.tooltipTitle.style.fontSize = w.config.tooltip.style.fontSize tooltipEl.appendChild(this.tooltipTitle) } let ttItemsCnt = w.globals.series.length // whether shared or not, default is shared if ( ((w.globals.xyCharts) || w.globals.comboCharts) && w.config.tooltip.shared ) { if (!this.showOnIntersect) { ttItemsCnt = w.globals.series.length } else { ttItemsCnt = 1 } } this.ttItems = this.createTTElements(ttItemsCnt) this.addSVGEvents() } createTTElements (ttItemsCnt) { const w = this.w let ttItems = [] const tooltipEl = this.getElTooltip() for (let i = 0; i < ttItemsCnt; i++) { let gTxt = document.createElement('div') gTxt.classList.add('apexcharts-tooltip-series-group') let point = document.createElement('span') point.classList.add('apexcharts-tooltip-marker') point.style.backgroundColor = w.globals.colors[i] gTxt.appendChild(point) const gYZ = document.createElement('div') gYZ.classList.add('apexcharts-tooltip-text') gYZ.style.fontFamily = w.config.tooltip.style.fontFamily || w.config.chart.fontFamily gYZ.style.fontSize = w.config.tooltip.style.fontSize // y values group const gYValText = document.createElement('div') gYValText.classList.add('apexcharts-tooltip-y-group') let txtLabel = document.createElement('span') txtLabel.classList.add('apexcharts-tooltip-text-label') gYValText.appendChild(txtLabel) let txtValue = document.createElement('span') txtValue.classList.add('apexcharts-tooltip-text-value') gYValText.appendChild(txtValue) // z values group const gZValText = document.createElement('div') gZValText.classList.add('apexcharts-tooltip-z-group') let txtZLabel = document.createElement('span') txtZLabel.classList.add('apexcharts-tooltip-text-z-label') gZValText.appendChild(txtZLabel) let txtZValue = document.createElement('span') txtZValue.classList.add('apexcharts-tooltip-text-z-value') gZValText.appendChild(txtZValue) gYZ.appendChild(gYValText) gYZ.appendChild(gZValText) gTxt.appendChild(gYZ) tooltipEl.appendChild(gTxt) ttItems.push(gTxt) } return ttItems } addSVGEvents () { const w = this.w let type = w.config.chart.type const tooltipEl = this.getElTooltip() const barOrCandlestick = !!(type === 'bar' || type === 'candlestick') let hoverArea = w.globals.dom.Paper.node const elGrid = this.getElGrid() if (elGrid) { this.seriesBound = elGrid.getBoundingClientRect() } let tooltipY = [] let tooltipX = [] let seriesHoverParams = { hoverArea, elGrid: elGrid, tooltipEl: tooltipEl, tooltipY, tooltipX, ttItems: this.ttItems } let points if (w.globals.axisCharts) { if ( type === 'area' || type === 'line' || type === 'scatter' || type === 'bubble' ) { points = w.globals.dom.baseEl.querySelectorAll( ".apexcharts-series[data\\:longestSeries='true'] .apexcharts-marker" ) } else if (barOrCandlestick) { points = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-series .apexcharts-bar-area', '.apexcharts-series .apexcharts-candlestick-area' ) } else if (type === 'heatmap') { points = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-series .apexcharts-heatmap' ) } else if (type === 'radar') { points = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-series .apexcharts-marker' ) } 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) || ((barOrCandlestick && this.hasBars()) && w.config.tooltip.shared) if (validSharedChartTypes) { this.addPathsEventListeners([hoverArea], seriesHoverParams) } else if ((barOrCandlestick) && !w.globals.comboCharts) { this.addBarsEventListeners(seriesHoverParams) } else if ((type === 'bubble' || type === 'scatter' || type === 'radar') || (this.showOnIntersect && (type === 'area' || type === 'line'))) { this.addPointsEventsListeners(seriesHoverParams) } else if (!w.globals.axisCharts || type === 'heatmap') { let seriesAll = w.globals.dom.baseEl.querySelectorAll('.apexcharts-series') this.addPathsEventListeners(seriesAll, seriesHoverParams) } if (this.showOnIntersect) { let linePoints = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-line-series .apexcharts-marker' ) if (linePoints.length > 0) { // if we find any lineSeries, addEventListeners for them this.addPathsEventListeners(linePoints, seriesHoverParams) } let areaPoints = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-area-series .apexcharts-marker' ) if (areaPoints.length > 0) { // if we find any areaSeries, addEventListeners for them this.addPathsEventListeners(areaPoints, seriesHoverParams) } // combo charts may have bars, so add event listeners here too if (this.hasBars() && !w.config.tooltip.shared) { this.addBarsEventListeners(seriesHoverParams) } } } drawFixedTooltipRect () { let w = this.w const tooltipEl = this.getElTooltip() let tooltipRect = tooltipEl.getBoundingClientRect() let ttWidth = tooltipRect.width + 10 let ttHeight = tooltipRect.height + 10 let x = w.config.tooltip.fixed.offsetX let y = w.config.tooltip.fixed.offsetY if (w.config.tooltip.fixed.position.toLowerCase().indexOf('right') > -1) { x = x + w.globals.svgWidth - ttWidth + 10 } if (w.config.tooltip.fixed.position.toLowerCase().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 } } addPointsEventsListeners (seriesHoverParams) { let w = this.w let points = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-series-markers .apexcharts-marker' ) this.addPathsEventListeners(points, seriesHoverParams) } addBarsEventListeners (seriesHoverParams) { let w = this.w let bars = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-bar-area, .apexcharts-candlestick-area' ) this.addPathsEventListeners(bars, seriesHoverParams) } addPathsEventListeners (paths, opts) { let self = this for (let p = 0; p < paths.length; p++) { let extendedOpts = { paths: paths[p], tooltipEl: opts.tooltipEl, tooltipY: opts.tooltipY, tooltipX: opts.tooltipX, elGrid: opts.elGrid, hoverArea: opts.hoverArea, ttItems: opts.ttItems } this.w.globals.tooltipOpts = extendedOpts let events = ['mousemove', 'touchmove', 'mouseout', 'touchend'] events.map((ev) => { return paths[p].addEventListener( ev, self.seriesHover.bind(self, extendedOpts), { capture: false, passive: true } ) }) } } /* ** The actual series hover function */ seriesHover (opt, e) { let chartGroups = [] // if user has more than one charts in group, we need to sync if (this.w.config.chart.group) { chartGroups = this.ctx.getGroupedCharts() } if (chartGroups.length) { chartGroups.forEach((ch) => { const tooltipEl = this.getElTooltip(ch) const newOpts = { paths: opt.paths, tooltipEl: 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 }) } } seriesHoverByContext ({ chartCtx, ttCtx, opt, e }) { let w = chartCtx.w const tooltipEl = this.getElTooltip() // tooltipRect is calculated on every mousemove, because the text is dynamic ttCtx.tooltipRect = { x: 0, y: 0, ttWidth: tooltipEl.getBoundingClientRect().width, ttHeight: tooltipEl.getBoundingClientRect().height } ttCtx.e = e // highlight the current hovered bars if (ttCtx.hasBars() && !w.globals.comboCharts && !ttCtx.isBarShared) { if (w.config.tooltip.onDatasetHover.highlightDataSeries) { let series = new Series(chartCtx) series.toggleSeriesOnHover(e, e.target.parentNode) } } if (ttCtx.fixedTooltip) { ttCtx.drawFixedTooltipRect() } 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 }) } } // tooltip handling for line/area/bar/columns/scatter axisChartsTooltips ({ e, opt }) { let w = this.w let j, x, y let self = this let capj = null const tooltipEl = this.getElTooltip() const xcrosshairs = this.getElXCrosshairs() 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 let isStickyTooltip = w.globals.xyCharts || (w.config.chart.type === 'bar' && (!this.isBarHorizontal && this.hasBars()) && w.config.tooltip.shared) || (w.globals.comboCharts && this.hasBars) if (w.config.chart.type === 'bar' && (this.isBarHorizontal && this.hasBars())) { isStickyTooltip = false } if (e.type === 'mousemove' || e.type === 'touchmove') { if (xcrosshairs !== null) { xcrosshairs.classList.add('active') } if (self.ycrosshairs !== null && self.blyaxisTooltip) { self.ycrosshairs.classList.add('active') } if (isStickyTooltip && !self.showOnIntersect) { capj = self.tooltipUtil.getNearestValues({ context: self, hoverArea: opt.hoverArea, elGrid: opt.elGrid, clientX, clientY, hasBars: self.hasBars }) j = capj.j let capturedSeries = capj.capturedSeries if (capj.hoverX < 0 || capj.hoverX > w.globals.gridWidth) { // capj.hoverY causing issues in grouped charts, so commented out that condition for now // if (capj.hoverX < 0 || capj.hoverX > w.globals.gridWidth || capj.hoverY < 0 || capj.hoverY > w.globals.gridHeight) { self.handleMouseOut(opt) return } if (capturedSeries !== null) { let ignoreNull = w.globals.series[capturedSeries][j] === null if (ignoreNull) { opt.tooltipEl.classList.remove('active') return } if (typeof w.globals.series[capturedSeries][j] !== 'undefined') { if ( w.config.tooltip.shared && this.tooltipUtil.isXoverlap(j) && this.tooltipUtil.isinitialSeriesSameLen() ) { this.create(self, capturedSeries, j, opt.ttItems) } else { this.create(self, capturedSeries, j, opt.ttItems, false) } } else { if (this.tooltipUtil.isXoverlap(j)) { self.create(self, 0, j, opt.ttItems) } } } else { // couldn't capture any series. check if shared X is same, // if yes, draw a grouped tooltip if (this.tooltipUtil.isXoverlap(j)) { self.create(self, 0, j, opt.ttItems) } } } else { if (w.config.chart.type === 'heatmap') { let markerXY = this.intersect.handleHeatTooltip({ e, opt, x, y }) x = markerXY.x y = markerXY.y tooltipEl.style.left = x + 'px' tooltipEl.style.top = y + 'px' } else { if (this.hasBars) { this.intersect.handleBarTooltip({ e, opt }) } if (this.hasMarkers) { // intersect - line/area/scatter/bubble this.intersect.handleMarkerTooltip({ e, opt, x, y }) } } } if (this.blyaxisTooltip) { for (let yt = 0; yt < w.config.yaxis.length; yt++) { self.axesTooltip.drawYaxisTooltipText(yt, clientY, self.xyRatios) } } opt.tooltipEl.classList.add('active') } else if (e.type === 'mouseout' || e.type === 'touchend') { this.handleMouseOut(opt) } } // tooltip handling for pie/donuts nonAxisChartsTooltips ({ e, opt, tooltipRect }) { let w = this.w let rel = opt.paths.getAttribute('rel') const tooltipEl = this.getElTooltip() let trX = 0 let trY = 0 let elPie = null const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX if (w.config.chart.type === 'radialBar') { elPie = w.globals.dom.baseEl.querySelector( '.apexcharts-radialbar' ) } else { elPie = w.globals.dom.baseEl.querySelector( '.apexcharts-pie' ) trX = parseInt(elPie.getAttribute('data:innerTranslateX')) trY = parseInt(elPie.getAttribute('data:innerTranslateY')) } let seriesBound = elPie.getBoundingClientRect() if (e.type === 'mousemove' || e.type === 'touchmove') { tooltipEl.classList.add('active') this.tooltipLabels.drawSeriesTexts({ ttItems: opt.ttItems, i: parseInt(rel) - 1, shared: false }) let x = clientX - seriesBound.left - tooltipRect.ttWidth / 2.2 + trX let y = e.clientY - seriesBound.top - tooltipRect.ttHeight / 2 - 15 + trY if (x < 0) { x = 0 } else if (x + tooltipRect.ttWidth > w.globals.gridWidth) { x = clientX - seriesBound.left - tooltipRect.ttWidth + trX } if (y < 0) y = tooltipRect.ttHeight + 20 tooltipEl.style.left = x + w.globals.translateX + 'px' tooltipEl.style.top = y + 'px' } else if (e.type === 'mouseout' || e.type === 'touchend') { tooltipEl.classList.remove('active') } } deactivateHoverFilter () { let w = this.w let graphics = new Graphics(this.ctx) let allPaths = w.globals.dom.Paper.select(`.apexcharts-bar-area`) for (let b = 0; b < allPaths.length; b++) { graphics.pathMouseLeave(allPaths[b]) } } handleMouseOut (opt) { const w = this.w const xcrosshairs = this.getElXCrosshairs() opt.tooltipEl.classList.remove('active') this.deactivateHoverFilter() if (w.config.chart.type !== 'bubble') { this.marker.resetPointsSize() } if (xcrosshairs !== null) { xcrosshairs.classList.remove('active') } if (this.ycrosshairs !== null) { this.ycrosshairs.classList.remove('active') } if (this.blxaxisTooltip) { this.xaxisTooltip.classList.remove('active') } if (this.blyaxisTooltip) { if (this.yaxisTTEls === null) { this.yaxisTTEls = w.globals.dom.baseEl.querySelectorAll('.apexcharts-yaxistooltip') } for (let i = 0; i < this.yaxisTTEls.length; i++) { this.yaxisTTEls[i].classList.remove('active') } } } getElMarkers () { return this.w.globals.dom.baseEl.querySelectorAll(' .apexcharts-series-markers') } getAllMarkers () { return this.w.globals.dom.baseEl.querySelectorAll( '.apexcharts-series-markers .apexcharts-marker' ) } hasMarkers () { const markers = this.getElMarkers() return markers.length > 0 } getElBars () { return this.w.globals.dom.baseEl.querySelectorAll('.apexcharts-bar-series, .apexcharts-candlestick-series') } hasBars () { const bars = this.getElBars() return bars.length > 0 } create (context, capturedSeries, j, ttItems, shared = null) { let w = this.w let self = context if (shared === null) shared = w.config.tooltip.shared const hasMarkers = this.hasMarkers() const bars = this.getElBars() if (shared) { self.tooltipLabels.drawSeriesTexts({ ttItems, i: capturedSeries, j, shared: this.showOnIntersect ? false : w.config.tooltip.shared }) if (hasMarkers) { if (w.globals.markers.largestSize > 0) { self.marker.enlargePoints(j) } else { self.tooltipPosition.moveDynamicPointsOnHover(j) } } if (this.hasBars()) { this.barSeriesHeight = this.tooltipUtil.getBarsHeight(bars) if (this.barSeriesHeight > 0) { // hover state, activate snap filter let graphics = new Graphics(this.ctx) let paths = w.globals.dom.Paper.select(`.apexcharts-bar-area[j='${j}']`) // de-activate first this.deactivateHoverFilter() this.tooltipPosition.moveStickyTooltipOverBars(j) for (let b = 0; b < paths.length; b++) { graphics.pathMouseEnter(paths[b]) } } } } else { self.tooltipLabels.drawSeriesTexts({ shared: false, ttItems, i: capturedSeries, j }) if (this.hasBars()) { self.tooltipPosition.moveStickyTooltipOverBars(j) } if (hasMarkers) { self.tooltipPosition.moveMarkers(capturedSeries, j) } } } }