Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
import Utils from '../utils/Utils'
/**
* ApexCharts Animation Class.
*
* @module Animations
**/
export default class Animations {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
animateLine(el, from, to, speed) {
el.attr(from).animate(speed).attr(to)
}
/*
** Animate radius of a circle element
*/
animateMarker(el, speed, easing, cb) {
el.attr({
opacity: 0,
})
.animate(speed)
.attr({
opacity: 1,
})
.after(() => {
cb()
})
}
/*
** Animate rect properties
*/
animateRect(el, from, to, speed, fn) {
el.attr(from)
.animate(speed)
.attr(to)
.after(() => fn())
}
animatePathsGradually(params) {
let { el, realIndex, j, fill, pathFrom, pathTo, speed, delay } = params
let me = this
let w = this.w
let delayFactor = 0
if (w.config.chart.animations.animateGradually.enabled) {
delayFactor = w.config.chart.animations.animateGradually.delay
}
if (
w.config.chart.animations.dynamicAnimation.enabled &&
w.globals.dataChanged &&
w.config.chart.type !== 'bar'
) {
// disabled due to this bug - https://github.com/apexcharts/vue-apexcharts/issues/75
delayFactor = 0
}
me.morphSVG(
el,
realIndex,
j,
w.config.chart.type === 'line' && !w.globals.comboCharts
? 'stroke'
: fill,
pathFrom,
pathTo,
speed,
delay * delayFactor
)
}
showDelayedElements() {
this.w.globals.delayedElements.forEach((d) => {
const ele = d.el
ele.classList.remove('apexcharts-element-hidden')
ele.classList.add('apexcharts-hidden-element-shown')
})
}
animationCompleted(el) {
const w = this.w
if (w.globals.animationEnded) return
w.globals.animationEnded = true
this.showDelayedElements()
if (typeof w.config.chart.events.animationEnd === 'function') {
w.config.chart.events.animationEnd(this.ctx, { el, w })
}
}
// SVG.js animation for morphing one path to another
morphSVG(el, realIndex, j, fill, pathFrom, pathTo, speed, delay) {
let w = this.w
if (!pathFrom) {
pathFrom = el.attr('pathFrom')
}
if (!pathTo) {
pathTo = el.attr('pathTo')
}
const disableAnimationForCorrupPath = (path) => {
if (w.config.chart.type === 'radar') {
// radar chart drops the path to bottom and hence a corrup path looks ugly
// therefore, disable animation for such a case
speed = 1
}
return `M 0 ${w.globals.gridHeight}`
}
if (
!pathFrom ||
pathFrom.indexOf('undefined') > -1 ||
pathFrom.indexOf('NaN') > -1
) {
pathFrom = disableAnimationForCorrupPath()
}
if (
!pathTo.trim() ||
pathTo.indexOf('undefined') > -1 ||
pathTo.indexOf('NaN') > -1
) {
pathTo = disableAnimationForCorrupPath()
}
if (!w.globals.shouldAnimate) {
speed = 1
}
el.plot(pathFrom)
.animate(1, delay)
.plot(pathFrom)
.animate(speed, delay)
.plot(pathTo)
.after(() => {
// a flag to indicate that the original mount function can return true now as animation finished here
if (Utils.isNumber(j)) {
if (
j === w.globals.series[w.globals.maxValsInArrayIndex].length - 2 &&
w.globals.shouldAnimate
) {
this.animationCompleted(el)
}
} else if (fill !== 'none' && w.globals.shouldAnimate) {
if (
(!w.globals.comboCharts &&
realIndex === w.globals.series.length - 1) ||
w.globals.comboCharts
) {
this.animationCompleted(el)
}
}
this.showDelayedElements()
})
}
}

25
frontend/node_modules/apexcharts/src/modules/Base.js generated vendored Normal file
View File

@@ -0,0 +1,25 @@
import Config from './settings/Config'
import Globals from './settings/Globals'
/**
* ApexCharts Base Class for extending user options with pre-defined ApexCharts config.
*
* @module Base
**/
export default class Base {
constructor(opts) {
this.opts = opts
}
init() {
const config = new Config(this.opts).init({ responsiveOverride: false })
const globals = new Globals().init(config)
const w = {
config,
globals
}
return w
}
}

605
frontend/node_modules/apexcharts/src/modules/Core.js generated vendored Normal file
View File

@@ -0,0 +1,605 @@
import Bar from '../charts/Bar'
import BarStacked from '../charts/BarStacked'
import BoxCandleStick from '../charts/BoxCandleStick'
import CoreUtils from './CoreUtils'
import Crosshairs from './Crosshairs'
import HeatMap from '../charts/HeatMap'
import Globals from '../modules/settings/Globals'
import Pie from '../charts/Pie'
import Radar from '../charts/Radar'
import Radial from '../charts/Radial'
import RangeBar from '../charts/RangeBar'
import Legend from './legend/Legend'
import Line from '../charts/Line'
import Treemap from '../charts/Treemap'
import Graphics from './Graphics'
import Range from './Range'
import Utils from '../utils/Utils'
import TimeScale from './TimeScale'
/**
* ApexCharts Core Class responsible for major calculations and creating elements.
*
* @module Core
**/
export default class Core {
constructor(el, ctx) {
this.ctx = ctx
this.w = ctx.w
this.el = el
}
setupElements() {
const { globals: gl, config: cnf } = this.w
const ct = cnf.chart.type
const axisChartsArrTypes = [
'line',
'area',
'bar',
'rangeBar',
'rangeArea',
'candlestick',
'boxPlot',
'scatter',
'bubble',
'radar',
'heatmap',
'treemap',
]
const xyChartsArrTypes = [
'line',
'area',
'bar',
'rangeBar',
'rangeArea',
'candlestick',
'boxPlot',
'scatter',
'bubble',
]
gl.axisCharts = axisChartsArrTypes.includes(ct)
gl.xyCharts = xyChartsArrTypes.includes(ct)
gl.isBarHorizontal =
['bar', 'rangeBar', 'boxPlot'].includes(ct) &&
cnf.plotOptions.bar.horizontal
gl.chartClass = `.apexcharts${gl.chartID}`
gl.dom.baseEl = this.el
gl.dom.elWrap = document.createElement('div')
Graphics.setAttrs(gl.dom.elWrap, {
id: gl.chartClass.substring(1),
class: `apexcharts-canvas ${gl.chartClass.substring(1)}`,
})
this.el.appendChild(gl.dom.elWrap)
// gl.dom.Paper = new window.SVG.Doc(gl.dom.elWrap)
gl.dom.Paper = window.SVG().addTo(gl.dom.elWrap)
gl.dom.Paper.attr({
class: 'apexcharts-svg',
'xmlns:data': 'ApexChartsNS',
transform: `translate(${cnf.chart.offsetX}, ${cnf.chart.offsetY})`,
})
gl.dom.Paper.node.style.background =
cnf.theme.mode === 'dark' && !cnf.chart.background
? '#424242'
: cnf.theme.mode === 'light' && !cnf.chart.background
? '#fff'
: cnf.chart.background
this.setSVGDimensions()
gl.dom.elLegendForeign = document.createElementNS(gl.SVGNS, 'foreignObject')
Graphics.setAttrs(gl.dom.elLegendForeign, {
x: 0,
y: 0,
width: gl.svgWidth,
height: gl.svgHeight,
})
gl.dom.elLegendWrap = document.createElement('div')
gl.dom.elLegendWrap.classList.add('apexcharts-legend')
gl.dom.elLegendWrap.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
gl.dom.elLegendForeign.appendChild(gl.dom.elLegendWrap)
gl.dom.Paper.node.appendChild(gl.dom.elLegendForeign)
gl.dom.elGraphical = gl.dom.Paper.group().attr({
class: 'apexcharts-inner apexcharts-graphical',
})
gl.dom.elDefs = gl.dom.Paper.defs()
gl.dom.Paper.add(gl.dom.elGraphical)
gl.dom.elGraphical.add(gl.dom.elDefs)
}
plotChartType(ser, xyRatios) {
const { w, ctx } = this
const { config: cnf, globals: gl } = w
const seriesTypes = {
line: { series: [], i: [] },
area: { series: [], i: [] },
scatter: { series: [], i: [] },
bubble: { series: [], i: [] },
column: { series: [], i: [] },
candlestick: { series: [], i: [] },
boxPlot: { series: [], i: [] },
rangeBar: { series: [], i: [] },
rangeArea: { series: [], seriesRangeEnd: [], i: [] },
}
const chartType = cnf.chart.type || 'line'
let nonComboType = null
let comboCount = 0
gl.series.forEach((serie, st) => {
const seriesType = ser[st].type || chartType
if (seriesTypes[seriesType]) {
if (seriesType === 'rangeArea') {
seriesTypes[seriesType].series.push(gl.seriesRangeStart[st])
seriesTypes[seriesType].seriesRangeEnd.push(gl.seriesRangeEnd[st])
} else {
seriesTypes[seriesType].series.push(serie)
}
seriesTypes[seriesType].i.push(st)
if (seriesType === 'column' || seriesType === 'bar')
w.globals.columnSeries = seriesTypes.column
} else if (
[
'heatmap',
'treemap',
'pie',
'donut',
'polarArea',
'radialBar',
'radar',
].includes(seriesType)
) {
nonComboType = seriesType
} else if (seriesType === 'bar') {
seriesTypes['column'].series.push(serie)
seriesTypes['column'].i.push(st)
} else {
console.warn(
`You have specified an unrecognized series type (${seriesType}).`
)
}
if (chartType !== seriesType && seriesType !== 'scatter') comboCount++
})
if (comboCount > 0) {
if (nonComboType) {
console.warn(
`Chart or series type ${nonComboType} cannot appear with other chart or series types.`
)
}
if (
seriesTypes.column.series.length > 0 &&
cnf.plotOptions.bar.horizontal
) {
comboCount -= seriesTypes.column.series.length
seriesTypes.column = { series: [], i: [] }
w.globals.columnSeries = { series: [], i: [] }
console.warn(
'Horizontal bars are not supported in a mixed/combo chart. Please turn off `plotOptions.bar.horizontal`'
)
}
}
gl.comboCharts ||= comboCount > 0
const line = new Line(ctx, xyRatios)
const boxCandlestick = new BoxCandleStick(ctx, xyRatios)
ctx.pie = new Pie(ctx)
const radialBar = new Radial(ctx)
ctx.rangeBar = new RangeBar(ctx, xyRatios)
const radar = new Radar(ctx)
let elGraph = []
if (gl.comboCharts) {
const coreUtils = new CoreUtils(ctx)
if (seriesTypes.area.series.length > 0) {
elGraph.push(
...coreUtils.drawSeriesByGroup(
seriesTypes.area,
gl.areaGroups,
'area',
line
)
)
}
if (seriesTypes.column.series.length > 0) {
if (cnf.chart.stacked) {
const barStacked = new BarStacked(ctx, xyRatios)
elGraph.push(
barStacked.draw(seriesTypes.column.series, seriesTypes.column.i)
)
} else {
ctx.bar = new Bar(ctx, xyRatios)
elGraph.push(
ctx.bar.draw(seriesTypes.column.series, seriesTypes.column.i)
)
}
}
if (seriesTypes.rangeArea.series.length > 0) {
elGraph.push(
line.draw(
seriesTypes.rangeArea.series,
'rangeArea',
seriesTypes.rangeArea.i,
seriesTypes.rangeArea.seriesRangeEnd
)
)
}
if (seriesTypes.line.series.length > 0) {
elGraph.push(
...coreUtils.drawSeriesByGroup(
seriesTypes.line,
gl.lineGroups,
'line',
line
)
)
}
if (seriesTypes.candlestick.series.length > 0) {
elGraph.push(
boxCandlestick.draw(
seriesTypes.candlestick.series,
'candlestick',
seriesTypes.candlestick.i
)
)
}
if (seriesTypes.boxPlot.series.length > 0) {
elGraph.push(
boxCandlestick.draw(
seriesTypes.boxPlot.series,
'boxPlot',
seriesTypes.boxPlot.i
)
)
}
if (seriesTypes.rangeBar.series.length > 0) {
elGraph.push(
ctx.rangeBar.draw(seriesTypes.rangeBar.series, seriesTypes.rangeBar.i)
)
}
if (seriesTypes.scatter.series.length > 0) {
const scatterLine = new Line(ctx, xyRatios, true)
elGraph.push(
scatterLine.draw(
seriesTypes.scatter.series,
'scatter',
seriesTypes.scatter.i
)
)
}
if (seriesTypes.bubble.series.length > 0) {
const bubbleLine = new Line(ctx, xyRatios, true)
elGraph.push(
bubbleLine.draw(
seriesTypes.bubble.series,
'bubble',
seriesTypes.bubble.i
)
)
}
} else {
switch (cnf.chart.type) {
case 'line':
elGraph = line.draw(gl.series, 'line')
break
case 'area':
elGraph = line.draw(gl.series, 'area')
break
case 'bar':
if (cnf.chart.stacked) {
const barStacked = new BarStacked(ctx, xyRatios)
elGraph = barStacked.draw(gl.series)
} else {
ctx.bar = new Bar(ctx, xyRatios)
elGraph = ctx.bar.draw(gl.series)
}
break
case 'candlestick':
const candleStick = new BoxCandleStick(ctx, xyRatios)
elGraph = candleStick.draw(gl.series, 'candlestick')
break
case 'boxPlot':
const boxPlot = new BoxCandleStick(ctx, xyRatios)
elGraph = boxPlot.draw(gl.series, cnf.chart.type)
break
case 'rangeBar':
elGraph = ctx.rangeBar.draw(gl.series)
break
case 'rangeArea':
elGraph = line.draw(
gl.seriesRangeStart,
'rangeArea',
undefined,
gl.seriesRangeEnd
)
break
case 'heatmap':
const heatmap = new HeatMap(ctx, xyRatios)
elGraph = heatmap.draw(gl.series)
break
case 'treemap':
const treemap = new Treemap(ctx, xyRatios)
elGraph = treemap.draw(gl.series)
break
case 'pie':
case 'donut':
case 'polarArea':
elGraph = ctx.pie.draw(gl.series)
break
case 'radialBar':
elGraph = radialBar.draw(gl.series)
break
case 'radar':
elGraph = radar.draw(gl.series)
break
default:
elGraph = line.draw(gl.series)
}
}
return elGraph
}
setSVGDimensions() {
const { globals: gl, config: cnf } = this.w
cnf.chart.width = cnf.chart.width || '100%'
cnf.chart.height = cnf.chart.height || 'auto'
gl.svgWidth = cnf.chart.width
gl.svgHeight = cnf.chart.height
let elDim = Utils.getDimensions(this.el)
const widthUnit = cnf.chart.width
.toString()
.split(/[0-9]+/g)
.pop()
if (widthUnit === '%') {
if (Utils.isNumber(elDim[0])) {
if (elDim[0].width === 0) {
elDim = Utils.getDimensions(this.el.parentNode)
}
gl.svgWidth = (elDim[0] * parseInt(cnf.chart.width, 10)) / 100
}
} else if (widthUnit === 'px' || widthUnit === '') {
gl.svgWidth = parseInt(cnf.chart.width, 10)
}
const heightUnit = String(cnf.chart.height)
.toString()
.split(/[0-9]+/g)
.pop()
if (gl.svgHeight !== 'auto' && gl.svgHeight !== '') {
if (heightUnit === '%') {
const elParentDim = Utils.getDimensions(this.el.parentNode)
gl.svgHeight = (elParentDim[1] * parseInt(cnf.chart.height, 10)) / 100
} else {
gl.svgHeight = parseInt(cnf.chart.height, 10)
}
} else {
gl.svgHeight = gl.axisCharts ? gl.svgWidth / 1.61 : gl.svgWidth / 1.2
}
gl.svgWidth = Math.max(gl.svgWidth, 0)
gl.svgHeight = Math.max(gl.svgHeight, 0)
Graphics.setAttrs(gl.dom.Paper.node, {
width: gl.svgWidth,
height: gl.svgHeight,
})
if (heightUnit !== '%') {
const offsetY = cnf.chart.sparkline.enabled
? 0
: gl.axisCharts
? cnf.chart.parentHeightOffset
: 0
gl.dom.Paper.node.parentNode.parentNode.style.minHeight = `${
gl.svgHeight + offsetY
}px`
}
gl.dom.elWrap.style.width = `${gl.svgWidth}px`
gl.dom.elWrap.style.height = `${gl.svgHeight}px`
}
shiftGraphPosition() {
const { globals: gl } = this.w
const { translateY: tY, translateX: tX } = gl
Graphics.setAttrs(gl.dom.elGraphical.node, {
transform: `translate(${tX}, ${tY})`,
})
}
resizeNonAxisCharts() {
const { w } = this
const { globals: gl } = w
let legendHeight = 0
let offY = w.config.chart.sparkline.enabled ? 1 : 15
offY += w.config.grid.padding.bottom
if (
['top', 'bottom'].includes(w.config.legend.position) &&
w.config.legend.show &&
!w.config.legend.floating
) {
legendHeight =
new Legend(this.ctx).legendHelpers.getLegendDimensions().clwh + 7
}
const el = w.globals.dom.baseEl.querySelector(
'.apexcharts-radialbar, .apexcharts-pie'
)
let chartInnerDimensions = w.globals.radialSize * 2.05
if (
el &&
!w.config.chart.sparkline.enabled &&
w.config.plotOptions.radialBar.startAngle !== 0
) {
const elRadialRect = Utils.getBoundingClientRect(el)
chartInnerDimensions = elRadialRect.bottom
const maxHeight = elRadialRect.bottom - elRadialRect.top
chartInnerDimensions = Math.max(w.globals.radialSize * 2.05, maxHeight)
}
const newHeight = Math.ceil(
chartInnerDimensions + gl.translateY + legendHeight + offY
)
if (gl.dom.elLegendForeign) {
gl.dom.elLegendForeign.setAttribute('height', newHeight)
}
if (w.config.chart.height && String(w.config.chart.height).includes('%'))
return
gl.dom.elWrap.style.height = `${newHeight}px`
Graphics.setAttrs(gl.dom.Paper.node, { height: newHeight })
gl.dom.Paper.node.parentNode.parentNode.style.minHeight = `${newHeight}px`
}
coreCalculations() {
new Range(this.ctx).init()
}
resetGlobals() {
const resetxyValues = () => this.w.config.series.map(() => [])
const globalObj = new Globals()
const { globals: gl } = this.w
globalObj.initGlobalVars(gl)
gl.seriesXvalues = resetxyValues()
gl.seriesYvalues = resetxyValues()
}
isMultipleY() {
if (Array.isArray(this.w.config.yaxis) && this.w.config.yaxis.length > 1) {
this.w.globals.isMultipleYAxis = true
return true
}
return false
}
xySettings() {
const { w } = this
let xyRatios = null
if (w.globals.axisCharts) {
if (w.config.xaxis.crosshairs.position === 'back') {
new Crosshairs(this.ctx).drawXCrosshairs()
}
if (w.config.yaxis[0].crosshairs.position === 'back') {
new Crosshairs(this.ctx).drawYCrosshairs()
}
if (
w.config.xaxis.type === 'datetime' &&
w.config.xaxis.labels.formatter === undefined
) {
this.ctx.timeScale = new TimeScale(this.ctx)
let formattedTimeScale = []
if (
isFinite(w.globals.minX) &&
isFinite(w.globals.maxX) &&
!w.globals.isBarHorizontal
) {
formattedTimeScale = this.ctx.timeScale.calculateTimeScaleTicks(
w.globals.minX,
w.globals.maxX
)
} else if (w.globals.isBarHorizontal) {
formattedTimeScale = this.ctx.timeScale.calculateTimeScaleTicks(
w.globals.minY,
w.globals.maxY
)
}
this.ctx.timeScale.recalcDimensionsBasedOnFormat(formattedTimeScale)
}
const coreUtils = new CoreUtils(this.ctx)
xyRatios = coreUtils.getCalculatedRatios()
}
return xyRatios
}
updateSourceChart(targetChart) {
this.ctx.w.globals.selection = undefined
this.ctx.updateHelpers._updateOptions(
{
chart: {
selection: {
xaxis: {
min: targetChart.w.globals.minX,
max: targetChart.w.globals.maxX,
},
},
},
},
false,
false
)
}
setupBrushHandler() {
const { ctx, w } = this
if (!w.config.chart.brush.enabled) return
if (typeof w.config.chart.events.selection !== 'function') {
const targets = Array.isArray(w.config.chart.brush.targets)
? w.config.chart.brush.targets
: [w.config.chart.brush.target]
targets.forEach((target) => {
const targetChart = ctx.constructor.getChartByID(target)
targetChart.w.globals.brushSource = this.ctx
if (typeof targetChart.w.config.chart.events.zoomed !== 'function') {
targetChart.w.config.chart.events.zoomed = () =>
this.updateSourceChart(targetChart)
}
if (typeof targetChart.w.config.chart.events.scrolled !== 'function') {
targetChart.w.config.chart.events.scrolled = () =>
this.updateSourceChart(targetChart)
}
})
w.config.chart.events.selection = (chart, e) => {
targets.forEach((target) => {
const targetChart = ctx.constructor.getChartByID(target)
targetChart.ctx.updateHelpers._updateOptions(
{
xaxis: {
min: e.xaxis.min,
max: e.xaxis.max,
},
},
false,
false,
false,
false
)
})
}
}
}
}

View File

@@ -0,0 +1,657 @@
/*
** Util functions which are dependent on ApexCharts instance
*/
class CoreUtils {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
static checkComboSeries(series, chartType) {
let comboCharts = false
let comboBarCount = 0
let comboCount = 0
if (chartType === undefined) {
chartType = 'line'
}
// Check if user specified a type in series that may make us a combo chart.
// The default type for chart is "line" and the default for series is the
// chart type, therefore, if the types of all series match the chart type,
// this should not be considered a combo chart.
if (series.length && typeof series[0].type !== 'undefined') {
series.forEach((s) => {
if (
s.type === 'bar' ||
s.type === 'column' ||
s.type === 'candlestick' ||
s.type === 'boxPlot'
) {
comboBarCount++
}
if (typeof s.type !== 'undefined' && s.type !== chartType) {
comboCount++
}
})
}
if (comboCount > 0) {
comboCharts = true
}
return {
comboBarCount,
comboCharts,
}
}
/**
* @memberof CoreUtils
* returns the sum of all individual values in a multiple stacked series
* Eg. w.globals.series = [[32,33,43,12], [2,3,5,1]]
* @return [34,36,48,13]
**/
getStackedSeriesTotals(excludedSeriesIndices = []) {
const w = this.w
let total = []
if (w.globals.series.length === 0) return total
for (
let i = 0;
i < w.globals.series[w.globals.maxValsInArrayIndex].length;
i++
) {
let t = 0
for (let j = 0; j < w.globals.series.length; j++) {
if (
typeof w.globals.series[j][i] !== 'undefined' &&
excludedSeriesIndices.indexOf(j) === -1
) {
t += w.globals.series[j][i]
}
}
total.push(t)
}
return total
}
// get total of the all values inside all series
getSeriesTotalByIndex(index = null) {
if (index === null) {
// non-plot chart types - pie / donut / circle
return this.w.config.series.reduce((acc, cur) => acc + cur, 0)
} else {
// axis charts - supporting multiple series
return this.w.globals.series[index].reduce((acc, cur) => acc + cur, 0)
}
}
/**
* @memberof CoreUtils
* returns the sum of values in a multiple stacked grouped charts
* Eg. w.globals.series = [[32,33,43,12], [2,3,5,1], [43, 23, 34, 22]]
* series 1 and 2 are in a group, while series 3 is in another group
* @return [[34, 36, 48, 12], [43, 23, 34, 22]]
**/
getStackedSeriesTotalsByGroups() {
const w = this.w
let total = []
w.globals.seriesGroups.forEach((sg) => {
let includedIndexes = []
w.config.series.forEach((s, si) => {
if (sg.indexOf(w.globals.seriesNames[si]) > -1) {
includedIndexes.push(si)
}
})
const excludedIndices = w.globals.series
.map((_, fi) => (includedIndexes.indexOf(fi) === -1 ? fi : -1))
.filter((f) => f !== -1)
total.push(this.getStackedSeriesTotals(excludedIndices))
})
return total
}
setSeriesYAxisMappings() {
const gl = this.w.globals
const cnf = this.w.config
// The old config method to map multiple series to a y axis is to
// include one yaxis config per series but set each yaxis seriesName to the
// same series name. This relies on indexing equivalence to map series to
// an axis: series[n] => yaxis[n]. This needs to be retained for compatibility.
// But we introduce an alternative that explicitly configures yaxis elements
// with the series that will be referenced to them (seriesName: []). This
// only requires including the yaxis elements that will be seen on the chart.
// Old way:
// ya: s
// 0: 0
// 1: 1
// 2: 1
// 3: 1
// 4: 1
// Axes 0..4 are all scaled and all will be rendered unless the axes are
// show: false. If the chart is stacked, it's assumed that series 1..4 are
// the contributing series. This is not particularly intuitive.
// New way:
// ya: s
// 0: [0]
// 1: [1,2,3,4]
// If the chart is stacked, it can be assumed that any axis with multiple
// series is stacked.
//
// If this is an old chart and we are being backward compatible, it will be
// expected that each series is associated with it's corresponding yaxis
// through their indices, one-to-one.
// If yaxis.seriesName matches series.name, we have indices yi and si.
// A name match where yi != si is interpretted as yaxis[yi] and yaxis[si]
// will both be scaled to fit the combined series[si] and series[yi].
// Consider series named: S0,S1,S2 and yaxes A0,A1,A2.
//
// Example 1: A0 and A1 scaled the same.
// A0.seriesName: S0
// A1.seriesName: S0
// A2.seriesName: S2
// Then A1 <-> A0
//
// Example 2: A0, A1 and A2 all scaled the same.
// A0.seriesName: S2
// A1.seriesName: S0
// A2.seriesName: S1
// A0 <-> A2, A1 <-> A0, A2 <-> A1 --->>> A0 <-> A1 <-> A2
let axisSeriesMap = []
let seriesYAxisReverseMap = []
let unassignedSeriesIndices = []
let seriesNameArrayStyle =
gl.series.length > cnf.yaxis.length ||
cnf.yaxis.some((a) => Array.isArray(a.seriesName))
cnf.series.forEach((s, i) => {
unassignedSeriesIndices.push(i)
seriesYAxisReverseMap.push(null)
})
cnf.yaxis.forEach((yaxe, yi) => {
axisSeriesMap[yi] = []
})
let unassignedYAxisIndices = []
// here, we loop through the yaxis array and find the item which has "seriesName" property
cnf.yaxis.forEach((yaxe, yi) => {
let assigned = false
// Allow seriesName to be either a string (for backward compatibility),
// in which case, handle multiple yaxes referencing the same series.
// or an array of strings so that a yaxis can reference multiple series.
// Feature request #4237
if (yaxe.seriesName) {
let seriesNames = []
if (Array.isArray(yaxe.seriesName)) {
seriesNames = yaxe.seriesName
} else {
seriesNames.push(yaxe.seriesName)
}
seriesNames.forEach((name) => {
cnf.series.forEach((s, si) => {
if (s.name === name) {
let remove = si
if (yi === si || seriesNameArrayStyle) {
// New style, don't allow series to be double referenced
if (
!seriesNameArrayStyle ||
unassignedSeriesIndices.indexOf(si) > -1
) {
axisSeriesMap[yi].push([yi, si])
} else {
console.warn(
"Series '" +
s.name +
"' referenced more than once in what looks like the new style." +
' That is, when using either seriesName: [],' +
' or when there are more series than yaxes.'
)
}
} else {
// The series index refers to the target yaxis and the current
// yaxis index refers to the actual referenced series.
axisSeriesMap[si].push([si, yi])
remove = yi
}
assigned = true
remove = unassignedSeriesIndices.indexOf(remove)
if (remove !== -1) {
unassignedSeriesIndices.splice(remove, 1)
}
}
})
})
}
if (!assigned) {
unassignedYAxisIndices.push(yi)
}
})
axisSeriesMap = axisSeriesMap.map((yaxe, yi) => {
let ra = []
yaxe.forEach((sa) => {
seriesYAxisReverseMap[sa[1]] = sa[0]
ra.push(sa[1])
})
return ra
})
// All series referenced directly by yaxes have been assigned to those axes.
// Any series so far unassigned will be assigned to any yaxes that have yet
// to reference series directly, one-for-one in order of appearance, with
// all left-over series assigned to either the last unassigned yaxis, or the
// last yaxis if all have assigned series. This captures the
// default single and multiaxis config options which simply includes zero,
// one or as many yaxes as there are series but do not reference them by name.
let lastUnassignedYAxis = cnf.yaxis.length - 1
for (let i = 0; i < unassignedYAxisIndices.length; i++) {
lastUnassignedYAxis = unassignedYAxisIndices[i]
axisSeriesMap[lastUnassignedYAxis] = []
if (unassignedSeriesIndices) {
let si = unassignedSeriesIndices[0]
unassignedSeriesIndices.shift()
axisSeriesMap[lastUnassignedYAxis].push(si)
seriesYAxisReverseMap[si] = lastUnassignedYAxis
} else {
break
}
}
unassignedSeriesIndices.forEach((i) => {
axisSeriesMap[lastUnassignedYAxis].push(i)
seriesYAxisReverseMap[i] = lastUnassignedYAxis
})
// For the old-style seriesName-as-string-only, leave the zero-length yaxis
// array elements in for compatibility so that series.length == yaxes.length
// for multi axis charts.
gl.seriesYAxisMap = axisSeriesMap.map((x) => x)
gl.seriesYAxisReverseMap = seriesYAxisReverseMap.map((x) => x)
// Set default series group names
gl.seriesYAxisMap.forEach((axisSeries, ai) => {
axisSeries.forEach((si) => {
// series may be bare until loaded in realtime
if (cnf.series[si] && cnf.series[si].group === undefined) {
// A series with no group defined will be named after the axis that
// referenced it and thus form a group automatically.
cnf.series[si].group = 'apexcharts-axis-'.concat(ai.toString())
}
})
})
}
isSeriesNull(index = null) {
let r = []
if (index === null) {
// non-plot chart types - pie / donut / circle
r = this.w.config.series.filter((d) => d !== null)
} else {
// axis charts - supporting multiple series
r = this.w.config.series[index].data.filter((d) => d !== null)
}
return r.length === 0
}
seriesHaveSameValues(index) {
return this.w.globals.series[index].every((val, i, arr) => val === arr[0])
}
getCategoryLabels(labels) {
const w = this.w
let catLabels = labels.slice()
if (w.config.xaxis.convertedCatToNumeric) {
catLabels = labels.map((i, li) => {
return w.config.xaxis.labels.formatter(i - w.globals.minX + 1)
})
}
return catLabels
}
// maxValsInArrayIndex is the index of series[] which has the largest number of items
getLargestSeries() {
const w = this.w
w.globals.maxValsInArrayIndex = w.globals.series
.map((a) => a.length)
.indexOf(
Math.max.apply(
Math,
w.globals.series.map((a) => a.length)
)
)
}
getLargestMarkerSize() {
const w = this.w
let size = 0
w.globals.markers.size.forEach((m) => {
size = Math.max(size, m)
})
if (w.config.markers.discrete && w.config.markers.discrete.length) {
w.config.markers.discrete.forEach((m) => {
size = Math.max(size, m.size)
})
}
if (size > 0) {
if (w.config.markers.hover.size > 0) {
size = w.config.markers.hover.size
} else {
size += w.config.markers.hover.sizeOffset
}
}
w.globals.markers.largestSize = size
return size
}
/**
* @memberof Core
* returns the sum of all values in a series
* Eg. w.globals.series = [[32,33,43,12], [2,3,5,1]]
* @return [120, 11]
**/
getSeriesTotals() {
const w = this.w
w.globals.seriesTotals = w.globals.series.map((ser, index) => {
let total = 0
if (Array.isArray(ser)) {
for (let j = 0; j < ser.length; j++) {
total += ser[j]
}
} else {
// for pie/donuts/gauges
total += ser
}
return total
})
}
getSeriesTotalsXRange(minX, maxX) {
const w = this.w
const seriesTotalsXRange = w.globals.series.map((ser, index) => {
let total = 0
for (let j = 0; j < ser.length; j++) {
if (
w.globals.seriesX[index][j] > minX &&
w.globals.seriesX[index][j] < maxX
) {
total += ser[j]
}
}
return total
})
return seriesTotalsXRange
}
/**
* @memberof CoreUtils
* returns the percentage value of all individual values which can be used in a 100% stacked series
* Eg. w.globals.series = [[32, 33, 43, 12], [2, 3, 5, 1]]
* @return [[94.11, 91.66, 89.58, 92.30], [5.88, 8.33, 10.41, 7.7]]
**/
getPercentSeries() {
const w = this.w
w.globals.seriesPercent = w.globals.series.map((ser, index) => {
let seriesPercent = []
if (Array.isArray(ser)) {
for (let j = 0; j < ser.length; j++) {
let total = w.globals.stackedSeriesTotals[j]
let percent = 0
if (total) {
percent = (100 * ser[j]) / total
}
seriesPercent.push(percent)
}
} else {
const total = w.globals.seriesTotals.reduce((acc, val) => acc + val, 0)
let percent = (100 * ser) / total
seriesPercent.push(percent)
}
return seriesPercent
})
}
getCalculatedRatios() {
let w = this.w
let gl = w.globals
let yRatio = []
let invertedYRatio = 0
let xRatio = 0
let invertedXRatio = 0
let zRatio = 0
let baseLineY = []
let baseLineInvertedY = 0.1
let baseLineX = 0
gl.yRange = []
if (gl.isMultipleYAxis) {
for (let i = 0; i < gl.minYArr.length; i++) {
gl.yRange.push(Math.abs(gl.minYArr[i] - gl.maxYArr[i]))
baseLineY.push(0)
}
} else {
gl.yRange.push(Math.abs(gl.minY - gl.maxY))
}
gl.xRange = Math.abs(gl.maxX - gl.minX)
gl.zRange = Math.abs(gl.maxZ - gl.minZ)
// multiple y axis
for (let i = 0; i < gl.yRange.length; i++) {
yRatio.push(gl.yRange[i] / gl.gridHeight)
}
xRatio = gl.xRange / gl.gridWidth
invertedYRatio = gl.yRange / gl.gridWidth
invertedXRatio = gl.xRange / gl.gridHeight
zRatio = (gl.zRange / gl.gridHeight) * 16
if (!zRatio) {
zRatio = 1
}
if (gl.minY !== Number.MIN_VALUE && Math.abs(gl.minY) !== 0) {
// Negative numbers present in series
gl.hasNegs = true
}
// Check we have a map as series may still to be added/updated.
if (w.globals.seriesYAxisReverseMap.length > 0) {
let scaleBaseLineYScale = (y, i) => {
let yAxis = w.config.yaxis[w.globals.seriesYAxisReverseMap[i]]
let sign = y < 0 ? -1 : 1
y = Math.abs(y)
if (yAxis.logarithmic) {
y = this.getBaseLog(yAxis.logBase, y)
}
return (-sign * y) / yRatio[i]
}
if (gl.isMultipleYAxis) {
baseLineY = []
// baseline variables is the 0 of the yaxis which will be needed when there are negatives
for (let i = 0; i < yRatio.length; i++) {
baseLineY.push(scaleBaseLineYScale(gl.minYArr[i], i))
}
} else {
baseLineY = []
baseLineY.push(scaleBaseLineYScale(gl.minY, 0))
if (gl.minY !== Number.MIN_VALUE && Math.abs(gl.minY) !== 0) {
baseLineInvertedY = -gl.minY / invertedYRatio // this is for bar chart
baseLineX = gl.minX / xRatio
}
}
} else {
baseLineY = []
baseLineY.push(0)
baseLineInvertedY = 0
baseLineX = 0
}
return {
yRatio,
invertedYRatio,
zRatio,
xRatio,
invertedXRatio,
baseLineInvertedY,
baseLineY,
baseLineX,
}
}
getLogSeries(series) {
const w = this.w
w.globals.seriesLog = series.map((s, i) => {
let yAxisIndex = w.globals.seriesYAxisReverseMap[i]
if (
w.config.yaxis[yAxisIndex] &&
w.config.yaxis[yAxisIndex].logarithmic
) {
return s.map((d) => {
if (d === null) return null
return this.getLogVal(w.config.yaxis[yAxisIndex].logBase, d, i)
})
} else {
return s
}
})
return w.globals.invalidLogScale ? series : w.globals.seriesLog
}
getLogValAtSeriesIndex(val, seriesIndex) {
if (val === null) return null
const w = this.w
let yAxisIndex = w.globals.seriesYAxisReverseMap[seriesIndex]
if (w.config.yaxis[yAxisIndex] && w.config.yaxis[yAxisIndex].logarithmic) {
return this.getLogVal(
w.config.yaxis[yAxisIndex].logBase,
val,
seriesIndex
)
}
return val
}
getBaseLog(base, value) {
return Math.log(value) / Math.log(base)
}
getLogVal(b, d, seriesIndex) {
if (d <= 0) {
return 0 // Should be Number.NEGATIVE_INFINITY
}
const w = this.w
const min_log_val =
w.globals.minYArr[seriesIndex] === 0
? -1 // make sure we dont calculate log of 0
: this.getBaseLog(b, w.globals.minYArr[seriesIndex])
const max_log_val =
w.globals.maxYArr[seriesIndex] === 0
? 0 // make sure we dont calculate log of 0
: this.getBaseLog(b, w.globals.maxYArr[seriesIndex])
const number_of_height_levels = max_log_val - min_log_val
if (d < 1) return d / number_of_height_levels
const log_height_value = this.getBaseLog(b, d) - min_log_val
return log_height_value / number_of_height_levels
}
getLogYRatios(yRatio) {
const w = this.w
const gl = this.w.globals
gl.yLogRatio = yRatio.slice()
gl.logYRange = gl.yRange.map((_, i) => {
let yAxisIndex = w.globals.seriesYAxisReverseMap[i]
if (
w.config.yaxis[yAxisIndex] &&
this.w.config.yaxis[yAxisIndex].logarithmic
) {
let maxY = -Number.MAX_VALUE
let minY = Number.MIN_VALUE
let range = 1
gl.seriesLog.forEach((s, si) => {
s.forEach((v) => {
if (w.config.yaxis[si] && w.config.yaxis[si].logarithmic) {
maxY = Math.max(v, maxY)
minY = Math.min(v, minY)
}
})
})
range = Math.pow(gl.yRange[i], Math.abs(minY - maxY) / gl.yRange[i])
gl.yLogRatio[i] = range / gl.gridHeight
return range
}
})
return gl.invalidLogScale ? yRatio.slice() : gl.yLogRatio
}
// Some config objects can be array - and we need to extend them correctly
static extendArrayProps(configInstance, options, w) {
if (options?.yaxis) {
options = configInstance.extendYAxis(options, w)
}
if (options?.annotations) {
if (options.annotations.yaxis) {
options = configInstance.extendYAxisAnnotations(options)
}
if (options?.annotations?.xaxis) {
options = configInstance.extendXAxisAnnotations(options)
}
if (options?.annotations?.points) {
options = configInstance.extendPointAnnotations(options)
}
}
return options
}
// Series of the same group and type can be stacked together distinct from
// other series of the same type on the same axis.
drawSeriesByGroup(typeSeries, typeGroups, type, chartClass) {
let w = this.w
let graph = []
if (typeSeries.series.length > 0) {
// draw each group separately
typeGroups.forEach((gn) => {
let gs = []
let gi = []
typeSeries.i.forEach((i, ii) => {
if (w.config.series[i].group === gn) {
gs.push(typeSeries.series[ii])
gi.push(i)
}
})
gs.length > 0 && graph.push(chartClass.draw(gs, type, gi))
})
}
return graph
}
}
export default CoreUtils

View File

@@ -0,0 +1,138 @@
import Graphics from './Graphics'
import Filters from './Filters'
import Utils from '../utils/Utils'
class Crosshairs {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
drawXCrosshairs() {
const w = this.w
let graphics = new Graphics(this.ctx)
let filters = new Filters(this.ctx)
let crosshairGradient = w.config.xaxis.crosshairs.fill.gradient
let crosshairShadow = w.config.xaxis.crosshairs.dropShadow
let fillType = w.config.xaxis.crosshairs.fill.type
let gradientFrom = crosshairGradient.colorFrom
let gradientTo = crosshairGradient.colorTo
let opacityFrom = crosshairGradient.opacityFrom
let opacityTo = crosshairGradient.opacityTo
let stops = crosshairGradient.stops
let shadow = 'none'
let dropShadow = crosshairShadow.enabled
let shadowLeft = crosshairShadow.left
let shadowTop = crosshairShadow.top
let shadowBlur = crosshairShadow.blur
let shadowColor = crosshairShadow.color
let shadowOpacity = crosshairShadow.opacity
let xcrosshairsFill = w.config.xaxis.crosshairs.fill.color
if (w.config.xaxis.crosshairs.show) {
if (fillType === 'gradient') {
xcrosshairsFill = graphics.drawGradient(
'vertical',
gradientFrom,
gradientTo,
opacityFrom,
opacityTo,
null,
stops,
null
)
}
let xcrosshairs = graphics.drawRect()
if (w.config.xaxis.crosshairs.width === 1) {
// to prevent drawing 2 lines, convert rect to line
xcrosshairs = graphics.drawLine()
}
let gridHeight = w.globals.gridHeight
if (!Utils.isNumber(gridHeight) || gridHeight < 0) {
gridHeight = 0
}
let crosshairsWidth = w.config.xaxis.crosshairs.width
if (!Utils.isNumber(crosshairsWidth) || crosshairsWidth < 0) {
crosshairsWidth = 0
}
xcrosshairs.attr({
class: 'apexcharts-xcrosshairs',
x: 0,
y: 0,
y2: gridHeight,
width: crosshairsWidth,
height: gridHeight,
fill: xcrosshairsFill,
filter: shadow,
'fill-opacity': w.config.xaxis.crosshairs.opacity,
stroke: w.config.xaxis.crosshairs.stroke.color,
'stroke-width': w.config.xaxis.crosshairs.stroke.width,
'stroke-dasharray': w.config.xaxis.crosshairs.stroke.dashArray
})
if (dropShadow) {
xcrosshairs = filters.dropShadow(xcrosshairs, {
left: shadowLeft,
top: shadowTop,
blur: shadowBlur,
color: shadowColor,
opacity: shadowOpacity
})
}
w.globals.dom.elGraphical.add(xcrosshairs)
}
}
drawYCrosshairs() {
const w = this.w
let graphics = new Graphics(this.ctx)
let crosshair = w.config.yaxis[0].crosshairs
const offX = w.globals.barPadForNumericAxis
if (w.config.yaxis[0].crosshairs.show) {
let ycrosshairs = graphics.drawLine(
-offX,
0,
w.globals.gridWidth + offX,
0,
crosshair.stroke.color,
crosshair.stroke.dashArray,
crosshair.stroke.width
)
ycrosshairs.attr({
class: 'apexcharts-ycrosshairs'
})
w.globals.dom.elGraphical.add(ycrosshairs)
}
// draw an invisible crosshair to help in positioning the yaxis tooltip
let ycrosshairsHidden = graphics.drawLine(
-offX,
0,
w.globals.gridWidth + offX,
0,
crosshair.stroke.color,
0,
0
)
ycrosshairsHidden.attr({
class: 'apexcharts-ycrosshairs-hidden'
})
w.globals.dom.elGraphical.add(ycrosshairsHidden)
}
}
export default Crosshairs

743
frontend/node_modules/apexcharts/src/modules/Data.js generated vendored Normal file
View File

@@ -0,0 +1,743 @@
import CoreUtils from './CoreUtils'
import DateTime from './../utils/DateTime'
import Series from './Series'
import Utils from '../utils/Utils'
import Defaults from './settings/Defaults'
export default class Data {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.twoDSeries = []
this.threeDSeries = []
this.twoDSeriesX = []
this.seriesGoals = []
this.coreUtils = new CoreUtils(this.ctx)
}
isMultiFormat() {
return this.isFormatXY() || this.isFormat2DArray()
}
// given format is [{x, y}, {x, y}]
isFormatXY() {
const series = this.w.config.series.slice()
const sr = new Series(this.ctx)
this.activeSeriesIndex = sr.getActiveConfigSeriesIndex()
if (
typeof series[this.activeSeriesIndex].data !== 'undefined' &&
series[this.activeSeriesIndex].data.length > 0 &&
series[this.activeSeriesIndex].data[0] !== null &&
typeof series[this.activeSeriesIndex].data[0].x !== 'undefined' &&
series[this.activeSeriesIndex].data[0] !== null
) {
return true
}
}
// given format is [[x, y], [x, y]]
isFormat2DArray() {
const series = this.w.config.series.slice()
const sr = new Series(this.ctx)
this.activeSeriesIndex = sr.getActiveConfigSeriesIndex()
if (
typeof series[this.activeSeriesIndex].data !== 'undefined' &&
series[this.activeSeriesIndex].data.length > 0 &&
typeof series[this.activeSeriesIndex].data[0] !== 'undefined' &&
series[this.activeSeriesIndex].data[0] !== null &&
series[this.activeSeriesIndex].data[0].constructor === Array
) {
return true
}
}
handleFormat2DArray(ser, i) {
const cnf = this.w.config
const gl = this.w.globals
const isBoxPlot =
cnf.chart.type === 'boxPlot' || cnf.series[i].type === 'boxPlot'
for (let j = 0; j < ser[i].data.length; j++) {
if (typeof ser[i].data[j][1] !== 'undefined') {
if (
Array.isArray(ser[i].data[j][1]) &&
ser[i].data[j][1].length === 4 &&
!isBoxPlot
) {
// candlestick nested ohlc format
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j][1][3]))
} else if (ser[i].data[j].length >= 5) {
// candlestick non-nested ohlc format
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j][4]))
} else {
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j][1]))
}
gl.dataFormatXNumeric = true
}
if (cnf.xaxis.type === 'datetime') {
// if timestamps are provided and xaxis type is datetime,
let ts = new Date(ser[i].data[j][0])
ts = new Date(ts).getTime()
this.twoDSeriesX.push(ts)
} else {
this.twoDSeriesX.push(ser[i].data[j][0])
}
}
for (let j = 0; j < ser[i].data.length; j++) {
if (typeof ser[i].data[j][2] !== 'undefined') {
this.threeDSeries.push(ser[i].data[j][2])
gl.isDataXYZ = true
}
}
}
handleFormatXY(ser, i) {
const cnf = this.w.config
const gl = this.w.globals
const dt = new DateTime(this.ctx)
let activeI = i
if (gl.collapsedSeriesIndices.indexOf(i) > -1) {
// fix #368
activeI = this.activeSeriesIndex
}
// get series
for (let j = 0; j < ser[i].data.length; j++) {
if (typeof ser[i].data[j].y !== 'undefined') {
if (Array.isArray(ser[i].data[j].y)) {
this.twoDSeries.push(
Utils.parseNumber(ser[i].data[j].y[ser[i].data[j].y.length - 1])
)
} else {
this.twoDSeries.push(Utils.parseNumber(ser[i].data[j].y))
}
}
if (
typeof ser[i].data[j].goals !== 'undefined' &&
Array.isArray(ser[i].data[j].goals)
) {
if (typeof this.seriesGoals[i] === 'undefined') {
this.seriesGoals[i] = []
}
this.seriesGoals[i].push(ser[i].data[j].goals)
} else {
if (typeof this.seriesGoals[i] === 'undefined') {
this.seriesGoals[i] = []
}
this.seriesGoals[i].push(null)
}
}
// get seriesX
for (let j = 0; j < ser[activeI].data.length; j++) {
const isXString = typeof ser[activeI].data[j].x === 'string'
const isXArr = Array.isArray(ser[activeI].data[j].x)
const isXDate = !isXArr && !!dt.isValidDate(ser[activeI].data[j].x)
if (isXString || isXDate) {
// user supplied '01/01/2017' or a date string (a JS date object is not supported)
if (isXString || cnf.xaxis.convertedCatToNumeric) {
const isRangeColumn = gl.isBarHorizontal && gl.isRangeData
if (cnf.xaxis.type === 'datetime' && !isRangeColumn) {
this.twoDSeriesX.push(dt.parseDate(ser[activeI].data[j].x))
} else {
// a category and not a numeric x value
this.fallbackToCategory = true
this.twoDSeriesX.push(ser[activeI].data[j].x)
if (
!isNaN(ser[activeI].data[j].x) &&
this.w.config.xaxis.type !== 'category' &&
typeof ser[activeI].data[j].x !== 'string'
) {
gl.isXNumeric = true
}
}
} else {
if (cnf.xaxis.type === 'datetime') {
this.twoDSeriesX.push(
dt.parseDate(ser[activeI].data[j].x.toString())
)
} else {
gl.dataFormatXNumeric = true
gl.isXNumeric = true
this.twoDSeriesX.push(parseFloat(ser[activeI].data[j].x))
}
}
} else if (isXArr) {
// a multiline label described in array format
this.fallbackToCategory = true
this.twoDSeriesX.push(ser[activeI].data[j].x)
} else {
// a numeric value in x property
gl.isXNumeric = true
gl.dataFormatXNumeric = true
this.twoDSeriesX.push(ser[activeI].data[j].x)
}
}
if (ser[i].data[0] && typeof ser[i].data[0].z !== 'undefined') {
for (let t = 0; t < ser[i].data.length; t++) {
this.threeDSeries.push(ser[i].data[t].z)
}
gl.isDataXYZ = true
}
}
handleRangeData(ser, i) {
const gl = this.w.globals
let range = {}
if (this.isFormat2DArray()) {
range = this.handleRangeDataFormat('array', ser, i)
} else if (this.isFormatXY()) {
range = this.handleRangeDataFormat('xy', ser, i)
}
// Fix: RangeArea Chart: hide all series results in a crash #3984
gl.seriesRangeStart.push(range.start === undefined ? [] : range.start)
gl.seriesRangeEnd.push(range.end === undefined ? [] : range.end)
gl.seriesRange.push(range.rangeUniques)
// check for overlaps to avoid clashes in a timeline chart
gl.seriesRange.forEach((sr, si) => {
if (sr) {
sr.forEach((sarr, sarri) => {
sarr.y.forEach((arr, arri) => {
for (let sri = 0; sri < sarr.y.length; sri++) {
if (arri !== sri) {
const range1y1 = arr.y1
const range1y2 = arr.y2
const range2y1 = sarr.y[sri].y1
const range2y2 = sarr.y[sri].y2
if (range1y1 <= range2y2 && range2y1 <= range1y2) {
if (sarr.overlaps.indexOf(arr.rangeName) < 0) {
sarr.overlaps.push(arr.rangeName)
}
if (sarr.overlaps.indexOf(sarr.y[sri].rangeName) < 0) {
sarr.overlaps.push(sarr.y[sri].rangeName)
}
}
}
}
})
})
}
})
return range
}
handleCandleStickBoxData(ser, i) {
const gl = this.w.globals
let ohlc = {}
if (this.isFormat2DArray()) {
ohlc = this.handleCandleStickBoxDataFormat('array', ser, i)
} else if (this.isFormatXY()) {
ohlc = this.handleCandleStickBoxDataFormat('xy', ser, i)
}
gl.seriesCandleO[i] = ohlc.o
gl.seriesCandleH[i] = ohlc.h
gl.seriesCandleM[i] = ohlc.m
gl.seriesCandleL[i] = ohlc.l
gl.seriesCandleC[i] = ohlc.c
return ohlc
}
handleRangeDataFormat(format, ser, i) {
const rangeStart = []
const rangeEnd = []
const uniqueKeys = ser[i].data
.filter(
(thing, index, self) => index === self.findIndex((t) => t.x === thing.x)
)
.map((r, index) => {
return {
x: r.x,
overlaps: [],
y: [],
}
})
if (format === 'array') {
for (let j = 0; j < ser[i].data.length; j++) {
if (Array.isArray(ser[i].data[j])) {
rangeStart.push(ser[i].data[j][1][0])
rangeEnd.push(ser[i].data[j][1][1])
} else {
rangeStart.push(ser[i].data[j])
rangeEnd.push(ser[i].data[j])
}
}
} else if (format === 'xy') {
for (let j = 0; j < ser[i].data.length; j++) {
let isDataPoint2D = Array.isArray(ser[i].data[j].y)
const id = Utils.randomId()
const x = ser[i].data[j].x
const y = {
y1: isDataPoint2D ? ser[i].data[j].y[0] : ser[i].data[j].y,
y2: isDataPoint2D ? ser[i].data[j].y[1] : ser[i].data[j].y,
rangeName: id,
}
// CAUTION: mutating config object by adding a new property
// TODO: As this is specifically for timeline rangebar charts, update the docs mentioning the series only supports xy format
ser[i].data[j].rangeName = id
const uI = uniqueKeys.findIndex((t) => t.x === x)
uniqueKeys[uI].y.push(y)
rangeStart.push(y.y1)
rangeEnd.push(y.y2)
}
}
return {
start: rangeStart,
end: rangeEnd,
rangeUniques: uniqueKeys,
}
}
handleCandleStickBoxDataFormat(format, ser, i) {
const w = this.w
const isBoxPlot =
w.config.chart.type === 'boxPlot' || w.config.series[i].type === 'boxPlot'
const serO = []
const serH = []
const serM = []
const serL = []
const serC = []
if (format === 'array') {
if (
(isBoxPlot && ser[i].data[0].length === 6) ||
(!isBoxPlot && ser[i].data[0].length === 5)
) {
for (let j = 0; j < ser[i].data.length; j++) {
serO.push(ser[i].data[j][1])
serH.push(ser[i].data[j][2])
if (isBoxPlot) {
serM.push(ser[i].data[j][3])
serL.push(ser[i].data[j][4])
serC.push(ser[i].data[j][5])
} else {
serL.push(ser[i].data[j][3])
serC.push(ser[i].data[j][4])
}
}
} else {
for (let j = 0; j < ser[i].data.length; j++) {
if (Array.isArray(ser[i].data[j][1])) {
serO.push(ser[i].data[j][1][0])
serH.push(ser[i].data[j][1][1])
if (isBoxPlot) {
serM.push(ser[i].data[j][1][2])
serL.push(ser[i].data[j][1][3])
serC.push(ser[i].data[j][1][4])
} else {
serL.push(ser[i].data[j][1][2])
serC.push(ser[i].data[j][1][3])
}
}
}
}
} else if (format === 'xy') {
for (let j = 0; j < ser[i].data.length; j++) {
if (Array.isArray(ser[i].data[j].y)) {
serO.push(ser[i].data[j].y[0])
serH.push(ser[i].data[j].y[1])
if (isBoxPlot) {
serM.push(ser[i].data[j].y[2])
serL.push(ser[i].data[j].y[3])
serC.push(ser[i].data[j].y[4])
} else {
serL.push(ser[i].data[j].y[2])
serC.push(ser[i].data[j].y[3])
}
}
}
}
return {
o: serO,
h: serH,
m: serM,
l: serL,
c: serC,
}
}
parseDataAxisCharts(ser, ctx = this.ctx) {
const cnf = this.w.config
const gl = this.w.globals
const dt = new DateTime(ctx)
const xlabels =
cnf.labels.length > 0 ? cnf.labels.slice() : cnf.xaxis.categories.slice()
gl.isRangeBar = cnf.chart.type === 'rangeBar' && gl.isBarHorizontal
gl.hasXaxisGroups =
cnf.xaxis.type === 'category' && cnf.xaxis.group.groups.length > 0
if (gl.hasXaxisGroups) {
gl.groups = cnf.xaxis.group.groups
}
ser.forEach((s, i) => {
if (s.name !== undefined) {
gl.seriesNames.push(s.name)
} else {
gl.seriesNames.push('series-' + parseInt(i + 1, 10))
}
})
this.coreUtils.setSeriesYAxisMappings()
// At this point, every series that didn't have a user defined group name
// has been given a name according to the yaxis the series is referenced by.
// This fits the existing behaviour where all series associated with an axis
// are defacto presented as a single group. It is now formalised.
let buckets = []
let groups = [...new Set(cnf.series.map((s) => s.group))]
cnf.series.forEach((s, i) => {
let index = groups.indexOf(s.group)
if (!buckets[index]) buckets[index] = []
buckets[index].push(gl.seriesNames[i])
})
gl.seriesGroups = buckets
const handleDates = () => {
for (let j = 0; j < xlabels.length; j++) {
if (typeof xlabels[j] === 'string') {
// user provided date strings
let isDate = dt.isValidDate(xlabels[j])
if (isDate) {
this.twoDSeriesX.push(dt.parseDate(xlabels[j]))
} else {
throw new Error(
'You have provided invalid Date format. Please provide a valid JavaScript Date'
)
}
} else {
// user provided timestamps
this.twoDSeriesX.push(xlabels[j])
}
}
}
for (let i = 0; i < ser.length; i++) {
this.twoDSeries = []
this.twoDSeriesX = []
this.threeDSeries = []
if (typeof ser[i].data === 'undefined') {
console.error(
"It is a possibility that you may have not included 'data' property in series."
)
return
}
if (
cnf.chart.type === 'rangeBar' ||
cnf.chart.type === 'rangeArea' ||
ser[i].type === 'rangeBar' ||
ser[i].type === 'rangeArea'
) {
gl.isRangeData = true
if (cnf.chart.type === 'rangeBar' || cnf.chart.type === 'rangeArea') {
this.handleRangeData(ser, i)
}
}
if (this.isMultiFormat()) {
if (this.isFormat2DArray()) {
this.handleFormat2DArray(ser, i)
} else if (this.isFormatXY()) {
this.handleFormatXY(ser, i)
}
if (
cnf.chart.type === 'candlestick' ||
ser[i].type === 'candlestick' ||
cnf.chart.type === 'boxPlot' ||
ser[i].type === 'boxPlot'
) {
this.handleCandleStickBoxData(ser, i)
}
gl.series.push(this.twoDSeries)
gl.labels.push(this.twoDSeriesX)
gl.seriesX.push(this.twoDSeriesX)
gl.seriesGoals = this.seriesGoals
if (i === this.activeSeriesIndex && !this.fallbackToCategory) {
gl.isXNumeric = true
}
} else {
if (cnf.xaxis.type === 'datetime') {
// user didn't supplied [{x,y}] or [[x,y]], but single array in data.
// Also labels/categories were supplied differently
gl.isXNumeric = true
handleDates()
gl.seriesX.push(this.twoDSeriesX)
} else if (cnf.xaxis.type === 'numeric') {
gl.isXNumeric = true
if (xlabels.length > 0) {
this.twoDSeriesX = xlabels
gl.seriesX.push(this.twoDSeriesX)
}
}
gl.labels.push(this.twoDSeriesX)
const singleArray = ser[i].data.map((d) => Utils.parseNumber(d))
gl.series.push(singleArray)
}
gl.seriesZ.push(this.threeDSeries)
// overrided default color if user inputs color with series data
if (ser[i].color !== undefined) {
gl.seriesColors.push(ser[i].color)
} else {
gl.seriesColors.push(undefined)
}
}
return this.w
}
parseDataNonAxisCharts(ser) {
const gl = this.w.globals
const cnf = this.w.config
gl.series = ser.slice()
gl.seriesNames = cnf.labels.slice()
for (let i = 0; i < gl.series.length; i++) {
if (gl.seriesNames[i] === undefined) {
gl.seriesNames.push('series-' + (i + 1))
}
}
return this.w
}
/** User possibly set string categories in xaxis.categories or labels prop
* Or didn't set xaxis labels at all - in which case we manually do it.
* If user passed series data as [[3, 2], [4, 5]] or [{ x: 3, y: 55 }],
* this shouldn't be called
* @param {array} ser - the series which user passed to the config
*/
handleExternalLabelsData(ser) {
const cnf = this.w.config
const gl = this.w.globals
if (cnf.xaxis.categories.length > 0) {
// user provided labels in xaxis.category prop
gl.labels = cnf.xaxis.categories
} else if (cnf.labels.length > 0) {
// user provided labels in labels props
gl.labels = cnf.labels.slice()
} else if (this.fallbackToCategory) {
// user provided labels in x prop in [{ x: 3, y: 55 }] data, and those labels are already stored in gl.labels[0], so just re-arrange the gl.labels array
gl.labels = gl.labels[0]
if (gl.seriesRange.length) {
gl.seriesRange.map((srt) => {
srt.forEach((sr) => {
if (gl.labels.indexOf(sr.x) < 0 && sr.x) {
gl.labels.push(sr.x)
}
})
})
// remove duplicate x-axis labels
gl.labels = Array.from(
new Set(gl.labels.map(JSON.stringify)),
JSON.parse
)
}
if (cnf.xaxis.convertedCatToNumeric) {
const defaults = new Defaults(cnf)
defaults.convertCatToNumericXaxis(cnf, this.ctx, gl.seriesX[0])
this._generateExternalLabels(ser)
}
} else {
this._generateExternalLabels(ser)
}
}
_generateExternalLabels(ser) {
const gl = this.w.globals
const cnf = this.w.config
// user didn't provided any labels, fallback to 1-2-3-4-5
let labelArr = []
if (gl.axisCharts) {
if (gl.series.length > 0) {
if (this.isFormatXY()) {
// in case there is a combo chart (boxplot/scatter)
// and there are duplicated x values, we need to eliminate duplicates
const seriesDataFiltered = cnf.series.map((serie, s) => {
return serie.data.filter(
(v, i, a) => a.findIndex((t) => t.x === v.x) === i
)
})
const len = seriesDataFiltered.reduce(
(p, c, i, a) => (a[p].length > c.length ? p : i),
0
)
for (let i = 0; i < seriesDataFiltered[len].length; i++) {
labelArr.push(i + 1)
}
} else {
for (let i = 0; i < gl.series[gl.maxValsInArrayIndex].length; i++) {
labelArr.push(i + 1)
}
}
}
gl.seriesX = []
// create gl.seriesX as it will be used in calculations of x positions
for (let i = 0; i < ser.length; i++) {
gl.seriesX.push(labelArr)
}
// turn on the isXNumeric flag to allow minX and maxX to function properly
if (!this.w.globals.isBarHorizontal) {
gl.isXNumeric = true
}
}
// no series to pull labels from, put a 0-10 series
// possibly, user collapsed all series. Hence we can't work with above calc
if (labelArr.length === 0) {
labelArr = gl.axisCharts
? []
: gl.series.map((gls, glsi) => {
return glsi + 1
})
for (let i = 0; i < ser.length; i++) {
gl.seriesX.push(labelArr)
}
}
// Finally, pass the labelArr in gl.labels which will be printed on x-axis
gl.labels = labelArr
if (cnf.xaxis.convertedCatToNumeric) {
gl.categoryLabels = labelArr.map((l) => {
return cnf.xaxis.labels.formatter(l)
})
}
// Turn on this global flag to indicate no labels were provided by user
gl.noLabelsProvided = true
}
// Segregate user provided data into appropriate vars
parseData(ser) {
let w = this.w
let cnf = w.config
let gl = w.globals
this.excludeCollapsedSeriesInYAxis()
// If we detected string in X prop of series, we fallback to category x-axis
this.fallbackToCategory = false
this.ctx.core.resetGlobals()
this.ctx.core.isMultipleY()
if (gl.axisCharts) {
// axisCharts includes line / area / column / scatter
this.parseDataAxisCharts(ser)
this.coreUtils.getLargestSeries()
} else {
// non-axis charts are pie / donut
this.parseDataNonAxisCharts(ser)
}
// set Null values to 0 in all series when user hides/shows some series
if (cnf.chart.stacked) {
const series = new Series(this.ctx)
gl.series = series.setNullSeriesToZeroValues(gl.series)
}
this.coreUtils.getSeriesTotals()
if (gl.axisCharts) {
gl.stackedSeriesTotals = this.coreUtils.getStackedSeriesTotals()
gl.stackedSeriesTotalsByGroups =
this.coreUtils.getStackedSeriesTotalsByGroups()
}
this.coreUtils.getPercentSeries()
if (
!gl.dataFormatXNumeric &&
(!gl.isXNumeric ||
(cnf.xaxis.type === 'numeric' &&
cnf.labels.length === 0 &&
cnf.xaxis.categories.length === 0))
) {
// x-axis labels couldn't be detected; hence try searching every option in config
this.handleExternalLabelsData(ser)
}
// check for multiline xaxis
const catLabels = this.coreUtils.getCategoryLabels(gl.labels)
for (let l = 0; l < catLabels.length; l++) {
if (Array.isArray(catLabels[l])) {
gl.isMultiLineX = true
break
}
}
}
excludeCollapsedSeriesInYAxis() {
const w = this.w
// Post revision 3.46.0 there is no longer a strict one-to-one
// correspondence between series and Y axes.
// An axis can be ignored only while all series referenced by it
// are collapsed.
let yAxisIndexes = []
w.globals.seriesYAxisMap.forEach((yAxisArr, yi) => {
let collapsedCount = 0
yAxisArr.forEach((seriesIndex) => {
if (w.globals.collapsedSeriesIndices.indexOf(seriesIndex) !== -1) {
collapsedCount++
}
})
// It's possible to have a yaxis that doesn't reference any series yet,
// eg, because there are no series' yet, so don't list it as ignored
// prematurely.
if (collapsedCount > 0 && collapsedCount == yAxisArr.length) {
yAxisIndexes.push(yi)
}
})
w.globals.ignoreYAxisIndexes = yAxisIndexes.map((x) => x)
}
}

View File

@@ -0,0 +1,411 @@
import Scatter from './../charts/Scatter'
import Graphics from './Graphics'
import Filters from './Filters'
/**
* ApexCharts DataLabels Class for drawing dataLabels on Axes based Charts.
*
* @module DataLabels
**/
class DataLabels {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// When there are many datalabels to be printed, and some of them overlaps each other in the same series, this method will take care of that
// Also, when datalabels exceeds the drawable area and get clipped off, we need to adjust and move some pixels to make them visible again
dataLabelsCorrection(
x,
y,
val,
i,
dataPointIndex,
alwaysDrawDataLabel,
fontSize
) {
let w = this.w
let graphics = new Graphics(this.ctx)
let drawnextLabel = false //
let textRects = graphics.getTextRects(val, fontSize)
let width = textRects.width
let height = textRects.height
if (y < 0) y = 0
if (y > w.globals.gridHeight + height) y = w.globals.gridHeight + height / 2
// first value in series, so push an empty array
if (typeof w.globals.dataLabelsRects[i] === 'undefined')
w.globals.dataLabelsRects[i] = []
// then start pushing actual rects in that sub-array
w.globals.dataLabelsRects[i].push({ x, y, width, height })
let len = w.globals.dataLabelsRects[i].length - 2
let lastDrawnIndex =
typeof w.globals.lastDrawnDataLabelsIndexes[i] !== 'undefined'
? w.globals.lastDrawnDataLabelsIndexes[i][
w.globals.lastDrawnDataLabelsIndexes[i].length - 1
]
: 0
if (typeof w.globals.dataLabelsRects[i][len] !== 'undefined') {
let lastDataLabelRect = w.globals.dataLabelsRects[i][lastDrawnIndex]
if (
// next label forward and x not intersecting
x > lastDataLabelRect.x + lastDataLabelRect.width ||
y > lastDataLabelRect.y + lastDataLabelRect.height ||
y + height < lastDataLabelRect.y ||
x + width < lastDataLabelRect.x // next label is going to be drawn backwards
) {
// the 2 indexes don't override, so OK to draw next label
drawnextLabel = true
}
}
if (dataPointIndex === 0 || alwaysDrawDataLabel) {
drawnextLabel = true
}
return {
x,
y,
textRects,
drawnextLabel,
}
}
drawDataLabel({ type, pos, i, j, isRangeStart, strokeWidth = 2 }) {
// this method handles line, area, bubble, scatter charts as those charts contains markers/points which have pre-defined x/y positions
// all other charts like radar / bars / heatmaps will define their own drawDataLabel routine
let w = this.w
const graphics = new Graphics(this.ctx)
let dataLabelsConfig = w.config.dataLabels
let x = 0
let y = 0
let dataPointIndex = j
let elDataLabelsWrap = null
const seriesCollapsed = w.globals.collapsedSeriesIndices.indexOf(i) !== -1
if (seriesCollapsed || !dataLabelsConfig.enabled || !Array.isArray(pos.x)) {
return elDataLabelsWrap
}
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels',
})
for (let q = 0; q < pos.x.length; q++) {
x = pos.x[q] + dataLabelsConfig.offsetX
y = pos.y[q] + dataLabelsConfig.offsetY + strokeWidth
if (!isNaN(x)) {
// a small hack as we have 2 points for the first val to connect it
if (j === 1 && q === 0) dataPointIndex = 0
if (j === 1 && q === 1) dataPointIndex = 1
let val = w.globals.series[i][dataPointIndex]
if (type === 'rangeArea') {
if (isRangeStart) {
val = w.globals.seriesRangeStart[i][dataPointIndex]
} else {
val = w.globals.seriesRangeEnd[i][dataPointIndex]
}
}
let text = ''
const getText = (v) => {
return w.config.dataLabels.formatter(v, {
ctx: this.ctx,
seriesIndex: i,
dataPointIndex,
w,
})
}
if (w.config.chart.type === 'bubble') {
val = w.globals.seriesZ[i][dataPointIndex]
text = getText(val)
y = pos.y[q]
const scatter = new Scatter(this.ctx)
let centerTextInBubbleCoords = scatter.centerTextInBubble(
y,
i,
dataPointIndex
)
y = centerTextInBubbleCoords.y
} else {
if (typeof val !== 'undefined') {
text = getText(val)
}
}
let textAnchor = w.config.dataLabels.textAnchor
if (w.globals.isSlopeChart) {
if (dataPointIndex === 0) {
textAnchor = 'end'
} else if (dataPointIndex === w.config.series[i].data.length - 1) {
textAnchor = 'start'
} else {
textAnchor = 'middle'
}
}
this.plotDataLabelsText({
x,
y,
text,
i,
j: dataPointIndex,
parent: elDataLabelsWrap,
offsetCorrection: true,
dataLabelsConfig: w.config.dataLabels,
textAnchor,
})
}
}
return elDataLabelsWrap
}
plotDataLabelsText(opts) {
let w = this.w
let graphics = new Graphics(this.ctx)
let {
x,
y,
i,
j,
text,
textAnchor,
fontSize,
parent,
dataLabelsConfig,
color,
alwaysDrawDataLabel,
offsetCorrection,
className,
} = opts
let dataLabelText = null
if (Array.isArray(w.config.dataLabels.enabledOnSeries)) {
if (w.config.dataLabels.enabledOnSeries.indexOf(i) < 0) {
return dataLabelText
}
}
let correctedLabels = {
x,
y,
drawnextLabel: true,
textRects: null,
}
if (offsetCorrection) {
correctedLabels = this.dataLabelsCorrection(
x,
y,
text,
i,
j,
alwaysDrawDataLabel,
parseInt(dataLabelsConfig.style.fontSize, 10)
)
}
// when zoomed, we don't need to correct labels offsets,
// but if normally, labels get cropped, correct them
if (!w.globals.zoomed) {
x = correctedLabels.x
y = correctedLabels.y
}
if (correctedLabels.textRects) {
// fixes #2264
if (
x < -20 - correctedLabels.textRects.width ||
x > w.globals.gridWidth + correctedLabels.textRects.width + 30
) {
// datalabels fall outside drawing area, so draw a blank label
text = ''
}
}
let dataLabelColor = w.globals.dataLabels.style.colors[i]
if (
((w.config.chart.type === 'bar' || w.config.chart.type === 'rangeBar') &&
w.config.plotOptions.bar.distributed) ||
w.config.dataLabels.distributed
) {
dataLabelColor = w.globals.dataLabels.style.colors[j]
}
if (typeof dataLabelColor === 'function') {
dataLabelColor = dataLabelColor({
series: w.globals.series,
seriesIndex: i,
dataPointIndex: j,
w,
})
}
if (color) {
dataLabelColor = color
}
let offX = dataLabelsConfig.offsetX
let offY = dataLabelsConfig.offsetY
if (w.config.chart.type === 'bar' || w.config.chart.type === 'rangeBar') {
// for certain chart types, we handle offsets while calculating datalabels pos
// why? because bars/column may have negative values and based on that
// offsets becomes reversed
offX = 0
offY = 0
}
if (w.globals.isSlopeChart) {
if (j !== 0) {
offX = dataLabelsConfig.offsetX * -2 + 5
}
if (j !== 0 && j !== w.config.series[i].data.length - 1) {
offX = 0
}
}
if (correctedLabels.drawnextLabel) {
dataLabelText = graphics.drawText({
width: 100,
height: parseInt(dataLabelsConfig.style.fontSize, 10),
x: x + offX,
y: y + offY,
foreColor: dataLabelColor,
textAnchor: textAnchor || dataLabelsConfig.textAnchor,
text,
fontSize: fontSize || dataLabelsConfig.style.fontSize,
fontFamily: dataLabelsConfig.style.fontFamily,
fontWeight: dataLabelsConfig.style.fontWeight || 'normal',
})
dataLabelText.attr({
class: className || 'apexcharts-datalabel',
cx: x,
cy: y,
})
if (dataLabelsConfig.dropShadow.enabled) {
const textShadow = dataLabelsConfig.dropShadow
const filters = new Filters(this.ctx)
filters.dropShadow(dataLabelText, textShadow)
}
parent.add(dataLabelText)
if (typeof w.globals.lastDrawnDataLabelsIndexes[i] === 'undefined') {
w.globals.lastDrawnDataLabelsIndexes[i] = []
}
w.globals.lastDrawnDataLabelsIndexes[i].push(j)
}
return dataLabelText
}
addBackgroundToDataLabel(el, coords) {
const w = this.w
const bCnf = w.config.dataLabels.background
const paddingH = bCnf.padding
const paddingV = bCnf.padding / 2
const width = coords.width
const height = coords.height
const graphics = new Graphics(this.ctx)
const elRect = graphics.drawRect(
coords.x - paddingH,
coords.y - paddingV / 2,
width + paddingH * 2,
height + paddingV,
bCnf.borderRadius,
w.config.chart.background === 'transparent' || !w.config.chart.background
? '#fff'
: w.config.chart.background,
bCnf.opacity,
bCnf.borderWidth,
bCnf.borderColor
)
if (bCnf.dropShadow.enabled) {
const filters = new Filters(this.ctx)
filters.dropShadow(elRect, bCnf.dropShadow)
}
return elRect
}
dataLabelsBackground() {
const w = this.w
if (w.config.chart.type === 'bubble') return
const elDataLabels = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-datalabels text'
)
for (let i = 0; i < elDataLabels.length; i++) {
const el = elDataLabels[i]
const coords = el.getBBox()
let elRect = null
if (coords.width && coords.height) {
elRect = this.addBackgroundToDataLabel(el, coords)
}
if (elRect) {
el.parentNode.insertBefore(elRect.node, el)
const background = el.getAttribute('fill')
const shouldAnim =
w.config.chart.animations.enabled &&
!w.globals.resized &&
!w.globals.dataChanged
if (shouldAnim) {
elRect.animate().attr({ fill: background })
} else {
elRect.attr({ fill: background })
}
el.setAttribute('fill', w.config.dataLabels.background.foreColor)
}
}
}
bringForward() {
const w = this.w
const elDataLabelsNodes = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-datalabels'
)
const elSeries = w.globals.dom.baseEl.querySelector(
'.apexcharts-plot-series:last-child'
)
for (let i = 0; i < elDataLabelsNodes.length; i++) {
if (elSeries) {
elSeries.insertBefore(elDataLabelsNodes[i], elSeries.nextSibling)
}
}
}
}
export default DataLabels

120
frontend/node_modules/apexcharts/src/modules/Events.js generated vendored Normal file
View File

@@ -0,0 +1,120 @@
import Utils from '../utils/Utils'
export default class Events {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.documentEvent = Utils.bind(this.documentEvent, this)
}
addEventListener(name, handler) {
const w = this.w
if (w.globals.events.hasOwnProperty(name)) {
w.globals.events[name].push(handler)
} else {
w.globals.events[name] = [handler]
}
}
removeEventListener(name, handler) {
const w = this.w
if (!w.globals.events.hasOwnProperty(name)) {
return
}
let index = w.globals.events[name].indexOf(handler)
if (index !== -1) {
w.globals.events[name].splice(index, 1)
}
}
fireEvent(name, args) {
const w = this.w
if (!w.globals.events.hasOwnProperty(name)) {
return
}
if (!args || !args.length) {
args = []
}
let evs = w.globals.events[name]
let l = evs.length
for (let i = 0; i < l; i++) {
evs[i].apply(null, args)
}
}
setupEventHandlers() {
const w = this.w
const me = this.ctx
let clickableArea = w.globals.dom.baseEl.querySelector(w.globals.chartClass)
this.ctx.eventList.forEach((event) => {
clickableArea.addEventListener(
event,
(e) => {
const opts = Object.assign({}, w, {
seriesIndex: w.globals.axisCharts
? w.globals.capturedSeriesIndex
: 0,
dataPointIndex: w.globals.capturedDataPointIndex,
})
if (e.type === 'mousemove' || e.type === 'touchmove') {
if (typeof w.config.chart.events.mouseMove === 'function') {
w.config.chart.events.mouseMove(e, me, opts)
}
} else if (e.type === 'mouseleave' || e.type === 'touchleave') {
if (typeof w.config.chart.events.mouseLeave === 'function') {
w.config.chart.events.mouseLeave(e, me, opts)
}
} else if (
(e.type === 'mouseup' && e.which === 1) ||
e.type === 'touchend'
) {
if (typeof w.config.chart.events.click === 'function') {
w.config.chart.events.click(e, me, opts)
}
me.ctx.events.fireEvent('click', [e, me, opts])
}
},
{ capture: false, passive: true }
)
})
this.ctx.eventList.forEach((event) => {
w.globals.dom.baseEl.addEventListener(event, this.documentEvent, {
passive: true,
})
})
this.ctx.core.setupBrushHandler()
}
documentEvent(e) {
const w = this.w
const target = e.target.className
if (e.type === 'click') {
let elMenu = w.globals.dom.baseEl.querySelector('.apexcharts-menu')
if (
elMenu &&
elMenu.classList.contains('apexcharts-menu-open') &&
target !== 'apexcharts-menu-icon'
) {
elMenu.classList.remove('apexcharts-menu-open')
}
}
w.globals.clientX =
e.type === 'touchmove' ? e.touches[0].clientX : e.clientX
w.globals.clientY =
e.type === 'touchmove' ? e.touches[0].clientY : e.clientY
}
}

498
frontend/node_modules/apexcharts/src/modules/Exports.js generated vendored Normal file
View File

@@ -0,0 +1,498 @@
import Data from '../modules/Data'
import AxesUtils from '../modules/axes/AxesUtils'
import Series from '../modules/Series'
import Utils from '../utils/Utils'
class Exports {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
scaleSvgNode(svg, scale) {
// get current both width and height of the svg
let svgWidth = parseFloat(svg.getAttributeNS(null, 'width'))
let svgHeight = parseFloat(svg.getAttributeNS(null, 'height'))
// set new width and height based on the scale
svg.setAttributeNS(null, 'width', svgWidth * scale)
svg.setAttributeNS(null, 'height', svgHeight * scale)
svg.setAttributeNS(null, 'viewBox', '0 0 ' + svgWidth + ' ' + svgHeight)
}
getSvgString(_scale) {
return new Promise((resolve) => {
const w = this.w
let scale =
_scale ||
w.config.chart.toolbar.export.scale ||
w.config.chart.toolbar.export.width / w.globals.svgWidth
if (!scale) {
scale = 1 // if no scale is specified, don't scale...
}
let svgString = this.w.globals.dom.Paper.svg()
// clone the svg node so it remains intact in the UI
const svgNode = this.w.globals.dom.Paper.node.cloneNode(true)
// in case the scale is different than 1, the svg needs to be rescaled
if (scale !== 1) {
// scale the image
this.scaleSvgNode(svgNode, scale)
}
// Convert image URLs to base64
this.convertImagesToBase64(svgNode).then(() => {
svgString = new XMLSerializer().serializeToString(svgNode)
resolve(svgString.replace(/&nbsp;/g, '&#160;'))
})
})
}
convertImagesToBase64(svgNode) {
const images = svgNode.getElementsByTagName('image')
const promises = Array.from(images).map((img) => {
const href = img.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
if (href && !href.startsWith('data:')) {
return this.getBase64FromUrl(href)
.then((base64) => {
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', base64)
})
.catch((error) => {
console.error('Error converting image to base64:', error)
})
}
return Promise.resolve()
})
return Promise.all(promises)
}
getBase64FromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
resolve(canvas.toDataURL())
}
img.onerror = reject
img.src = url
})
}
cleanup() {
const w = this.w
// hide some elements to avoid printing them on exported svg
const xcrosshairs = w.globals.dom.baseEl.getElementsByClassName(
'apexcharts-xcrosshairs'
)
const ycrosshairs = w.globals.dom.baseEl.getElementsByClassName(
'apexcharts-ycrosshairs'
)
const zoomSelectionRects = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-zoom-rect, .apexcharts-selection-rect'
)
Array.prototype.forEach.call(zoomSelectionRects, (z) => {
z.setAttribute('width', 0)
})
if (xcrosshairs && xcrosshairs[0]) {
xcrosshairs[0].setAttribute('x', -500)
xcrosshairs[0].setAttribute('x1', -500)
xcrosshairs[0].setAttribute('x2', -500)
}
if (ycrosshairs && ycrosshairs[0]) {
ycrosshairs[0].setAttribute('y', -100)
ycrosshairs[0].setAttribute('y1', -100)
ycrosshairs[0].setAttribute('y2', -100)
}
}
svgUrl() {
return new Promise((resolve) => {
this.cleanup()
this.getSvgString().then((svgData) => {
const svgBlob = new Blob([svgData], {
type: 'image/svg+xml;charset=utf-8',
})
resolve(URL.createObjectURL(svgBlob))
})
})
}
dataURI(options) {
return new Promise((resolve) => {
const w = this.w
const scale = options
? options.scale || options.width / w.globals.svgWidth
: 1
this.cleanup()
const canvas = document.createElement('canvas')
canvas.width = w.globals.svgWidth * scale
canvas.height = parseInt(w.globals.dom.elWrap.style.height, 10) * scale // because of resizeNonAxisCharts
const canvasBg =
w.config.chart.background === 'transparent' ||
!w.config.chart.background
? '#fff'
: w.config.chart.background
let ctx = canvas.getContext('2d')
ctx.fillStyle = canvasBg
ctx.fillRect(0, 0, canvas.width * scale, canvas.height * scale)
this.getSvgString(scale).then((svgData) => {
const svgUrl = 'data:image/svg+xml,' + encodeURIComponent(svgData)
let img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
ctx.drawImage(img, 0, 0)
if (canvas.msToBlob) {
// Microsoft Edge can't navigate to data urls, so we return the blob instead
let blob = canvas.msToBlob()
resolve({ blob })
} else {
let imgURI = canvas.toDataURL('image/png')
resolve({ imgURI })
}
}
img.src = svgUrl
})
})
}
exportToSVG() {
this.svgUrl().then((url) => {
this.triggerDownload(
url,
this.w.config.chart.toolbar.export.svg.filename,
'.svg'
)
})
}
exportToPng() {
const scale = this.w.config.chart.toolbar.export.scale
const width = this.w.config.chart.toolbar.export.width
const option = scale
? { scale: scale }
: width
? { width: width }
: undefined
this.dataURI(option).then(({ imgURI, blob }) => {
if (blob) {
navigator.msSaveOrOpenBlob(blob, this.w.globals.chartID + '.png')
} else {
this.triggerDownload(
imgURI,
this.w.config.chart.toolbar.export.png.filename,
'.png'
)
}
})
}
exportToCSV({
series,
fileName,
columnDelimiter = ',',
lineDelimiter = '\n',
}) {
const w = this.w
if (!series) series = w.config.series
let columns = []
let rows = []
let result = ''
let universalBOM = '\uFEFF'
let gSeries = w.globals.series.map((s, i) => {
return w.globals.collapsedSeriesIndices.indexOf(i) === -1 ? s : []
})
const getFormattedCategory = (cat) => {
if (
typeof w.config.chart.toolbar.export.csv.categoryFormatter ===
'function'
) {
return w.config.chart.toolbar.export.csv.categoryFormatter(cat)
}
if (w.config.xaxis.type === 'datetime' && String(cat).length >= 10) {
return new Date(cat).toDateString()
}
return Utils.isNumber(cat) ? cat : cat.split(columnDelimiter).join('')
}
const getFormattedValue = (value) => {
return typeof w.config.chart.toolbar.export.csv.valueFormatter ===
'function'
? w.config.chart.toolbar.export.csv.valueFormatter(value)
: value
}
const seriesMaxDataLength = Math.max(
...series.map((s) => {
return s.data ? s.data.length : 0
})
)
const dataFormat = new Data(this.ctx)
const axesUtils = new AxesUtils(this.ctx)
const getCat = (i) => {
let cat = ''
// pie / donut/ radial
if (!w.globals.axisCharts) {
cat = w.config.labels[i]
} else {
// xy charts
// non datetime
if (
w.config.xaxis.type === 'category' ||
w.config.xaxis.convertedCatToNumeric
) {
if (w.globals.isBarHorizontal) {
let lbFormatter = w.globals.yLabelFormatters[0]
let sr = new Series(this.ctx)
let activeSeries = sr.getActiveConfigSeriesIndex()
cat = lbFormatter(w.globals.labels[i], {
seriesIndex: activeSeries,
dataPointIndex: i,
w,
})
} else {
cat = axesUtils.getLabel(
w.globals.labels,
w.globals.timescaleLabels,
0,
i
).text
}
}
// datetime, but labels specified in categories or labels
if (w.config.xaxis.type === 'datetime') {
if (w.config.xaxis.categories.length) {
cat = w.config.xaxis.categories[i]
} else if (w.config.labels.length) {
cat = w.config.labels[i]
}
}
}
// let the caller know the current category is null. this can happen for example
// when dealing with line charts having inconsistent time series data
if (cat === null) return 'nullvalue'
if (Array.isArray(cat)) {
cat = cat.join(' ')
}
return Utils.isNumber(cat) ? cat : cat.split(columnDelimiter).join('')
}
// Fix https://github.com/apexcharts/apexcharts.js/issues/3365
const getEmptyDataForCsvColumn = () => {
return [...Array(seriesMaxDataLength)].map(() => '')
}
const handleAxisRowsColumns = (s, sI) => {
if (columns.length && sI === 0) {
// It's the first series. Go ahead and create the first row with header information.
rows.push(columns.join(columnDelimiter))
}
if (s.data) {
// Use the data we have, or generate a properly sized empty array with empty data if some data is missing.
s.data = (s.data.length && s.data) || getEmptyDataForCsvColumn()
for (let i = 0; i < s.data.length; i++) {
// Reset the columns array so that we can start building columns for this row.
columns = []
let cat = getCat(i)
// current category is null, let's move on to the next one
if (cat === 'nullvalue') continue
if (!cat) {
if (dataFormat.isFormatXY()) {
cat = series[sI].data[i].x
} else if (dataFormat.isFormat2DArray()) {
cat = series[sI].data[i] ? series[sI].data[i][0] : ''
}
}
if (sI === 0) {
// It's the first series. Also handle the category.
columns.push(getFormattedCategory(cat))
for (let ci = 0; ci < w.globals.series.length; ci++) {
const value = dataFormat.isFormatXY()
? series[ci].data[i]?.y
: gSeries[ci][i]
columns.push(getFormattedValue(value))
}
}
if (
w.config.chart.type === 'candlestick' ||
(s.type && s.type === 'candlestick')
) {
columns.pop()
columns.push(w.globals.seriesCandleO[sI][i])
columns.push(w.globals.seriesCandleH[sI][i])
columns.push(w.globals.seriesCandleL[sI][i])
columns.push(w.globals.seriesCandleC[sI][i])
}
if (
w.config.chart.type === 'boxPlot' ||
(s.type && s.type === 'boxPlot')
) {
columns.pop()
columns.push(w.globals.seriesCandleO[sI][i])
columns.push(w.globals.seriesCandleH[sI][i])
columns.push(w.globals.seriesCandleM[sI][i])
columns.push(w.globals.seriesCandleL[sI][i])
columns.push(w.globals.seriesCandleC[sI][i])
}
if (w.config.chart.type === 'rangeBar') {
columns.pop()
columns.push(w.globals.seriesRangeStart[sI][i])
columns.push(w.globals.seriesRangeEnd[sI][i])
}
if (columns.length) {
rows.push(columns.join(columnDelimiter))
}
}
}
}
const handleUnequalXValues = () => {
const categories = new Set()
const data = {}
series.forEach((s, sI) => {
s?.data.forEach((dataItem) => {
let cat, value
if (dataFormat.isFormatXY()) {
cat = dataItem.x
value = dataItem.y
} else if (dataFormat.isFormat2DArray()) {
cat = dataItem[0]
value = dataItem[1]
} else {
return
}
if (!data[cat]) {
data[cat] = Array(series.length).fill('')
}
data[cat][sI] = getFormattedValue(value)
categories.add(cat)
})
})
if (columns.length) {
rows.push(columns.join(columnDelimiter))
}
Array.from(categories)
.sort()
.forEach((cat) => {
rows.push([
getFormattedCategory(cat),
data[cat].join(columnDelimiter),
])
})
}
columns.push(w.config.chart.toolbar.export.csv.headerCategory)
if (w.config.chart.type === 'boxPlot') {
columns.push('minimum')
columns.push('q1')
columns.push('median')
columns.push('q3')
columns.push('maximum')
} else if (w.config.chart.type === 'candlestick') {
columns.push('open')
columns.push('high')
columns.push('low')
columns.push('close')
} else if (w.config.chart.type === 'rangeBar') {
columns.push('minimum')
columns.push('maximum')
} else {
series.map((s, sI) => {
const sname = (s.name ? s.name : `series-${sI}`) + ''
if (w.globals.axisCharts) {
columns.push(
sname.split(columnDelimiter).join('')
? sname.split(columnDelimiter).join('')
: `series-${sI}`
)
}
})
}
if (!w.globals.axisCharts) {
columns.push(w.config.chart.toolbar.export.csv.headerValue)
rows.push(columns.join(columnDelimiter))
}
if (
!w.globals.allSeriesHasEqualX &&
w.globals.axisCharts &&
!w.config.xaxis.categories.length &&
!w.config.labels.length
) {
handleUnequalXValues()
} else {
series.map((s, sI) => {
if (w.globals.axisCharts) {
handleAxisRowsColumns(s, sI)
} else {
columns = []
columns.push(getFormattedCategory(w.globals.labels[sI]))
columns.push(getFormattedValue(gSeries[sI]))
rows.push(columns.join(columnDelimiter))
}
})
}
result += rows.join(lineDelimiter)
this.triggerDownload(
'data:text/csv; charset=utf-8,' +
encodeURIComponent(universalBOM + result),
fileName ? fileName : w.config.chart.toolbar.export.csv.filename,
'.csv'
)
}
triggerDownload(href, filename, ext) {
const downloadLink = document.createElement('a')
downloadLink.href = href
downloadLink.download = (filename ? filename : this.w.globals.chartID) + ext
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
}
export default Exports

512
frontend/node_modules/apexcharts/src/modules/Fill.js generated vendored Normal file
View File

@@ -0,0 +1,512 @@
import Graphics from './Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts Fill Class for setting fill options of the paths.
*
* @module Fill
**/
class Fill {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.opts = null
this.seriesIndex = 0
this.patternIDs = []
}
clippedImgArea(params) {
let w = this.w
let cnf = w.config
let svgW = parseInt(w.globals.gridWidth, 10)
let svgH = parseInt(w.globals.gridHeight, 10)
let size = svgW > svgH ? svgW : svgH
let fillImg = params.image
let imgWidth = 0
let imgHeight = 0
if (
typeof params.width === 'undefined' &&
typeof params.height === 'undefined'
) {
if (
cnf.fill.image.width !== undefined &&
cnf.fill.image.height !== undefined
) {
imgWidth = cnf.fill.image.width + 1
imgHeight = cnf.fill.image.height
} else {
imgWidth = size + 1
imgHeight = size
}
} else {
imgWidth = params.width
imgHeight = params.height
}
let elPattern = document.createElementNS(w.globals.SVGNS, 'pattern')
Graphics.setAttrs(elPattern, {
id: params.patternID,
patternUnits: params.patternUnits
? params.patternUnits
: 'userSpaceOnUse',
width: imgWidth + 'px',
height: imgHeight + 'px',
})
let elImage = document.createElementNS(w.globals.SVGNS, 'image')
elPattern.appendChild(elImage)
elImage.setAttributeNS(window.SVG.xlink, 'href', fillImg)
Graphics.setAttrs(elImage, {
x: 0,
y: 0,
preserveAspectRatio: 'none',
width: imgWidth + 'px',
height: imgHeight + 'px',
})
elImage.style.opacity = params.opacity
w.globals.dom.elDefs.node.appendChild(elPattern)
}
getSeriesIndex(opts) {
const w = this.w
const cType = w.config.chart.type
if (
((cType === 'bar' || cType === 'rangeBar') &&
w.config.plotOptions.bar.distributed) ||
cType === 'heatmap' ||
cType === 'treemap'
) {
this.seriesIndex = opts.seriesNumber
} else {
this.seriesIndex = opts.seriesNumber % w.globals.series.length
}
return this.seriesIndex
}
computeColorStops(data, multiColorConfig) {
const w = this.w
let maxPositive = null
let minNegative = null
for (let value of data) {
if (value >= multiColorConfig.threshold) {
if (maxPositive === null || value > maxPositive) {
maxPositive = value
}
} else {
if (minNegative === null || value < minNegative) {
minNegative = value
}
}
}
if (maxPositive === null) {
maxPositive = multiColorConfig.threshold
}
if (minNegative === null) {
minNegative = multiColorConfig.threshold
}
let totalRange =
maxPositive -
multiColorConfig.threshold +
(multiColorConfig.threshold - minNegative)
if (totalRange === 0) {
totalRange = 1
}
let negativePercentage =
((multiColorConfig.threshold - minNegative) / totalRange) * 100
let offset = 100 - negativePercentage
offset = Math.max(0, Math.min(offset, 100))
return [
{
offset: offset,
color: multiColorConfig.colorAboveThreshold,
opacity: w.config.fill.opacity,
},
{
offset: 0,
color: multiColorConfig.colorBelowThreshold,
opacity: w.config.fill.opacity,
},
]
}
fillPath(opts) {
let w = this.w
this.opts = opts
let cnf = this.w.config
let pathFill
let patternFill, gradientFill
this.seriesIndex = this.getSeriesIndex(opts)
const drawMultiColorLine =
cnf.plotOptions.line.colors.colorAboveThreshold &&
cnf.plotOptions.line.colors.colorBelowThreshold
let fillColors = this.getFillColors()
let fillColor = fillColors[this.seriesIndex]
//override fillcolor if user inputted color with data
if (w.globals.seriesColors[this.seriesIndex] !== undefined) {
fillColor = w.globals.seriesColors[this.seriesIndex]
}
if (typeof fillColor === 'function') {
fillColor = fillColor({
seriesIndex: this.seriesIndex,
dataPointIndex: opts.dataPointIndex,
value: opts.value,
w,
})
}
let fillType = opts.fillType
? opts.fillType
: this.getFillType(this.seriesIndex)
let fillOpacity = Array.isArray(cnf.fill.opacity)
? cnf.fill.opacity[this.seriesIndex]
: cnf.fill.opacity
// when line colors needs to be different based on values, we use gradient config to achieve this
const useGradient = fillType === 'gradient' || drawMultiColorLine
if (opts.color) {
fillColor = opts.color
}
if (
w.config.series[this.seriesIndex]?.data?.[opts.dataPointIndex]?.fillColor
) {
fillColor =
w.config.series[this.seriesIndex]?.data?.[opts.dataPointIndex]
?.fillColor
}
// in case a color is undefined, fallback to white color to prevent runtime error
if (!fillColor) {
fillColor = '#fff'
console.warn('undefined color - ApexCharts')
}
let defaultColor = fillColor
if (fillColor.indexOf('rgb') === -1) {
if (fillColor.indexOf('#') === -1) {
defaultColor = fillColor
} else if (fillColor.length < 9) {
// if the hex contains alpha and is of 9 digit, skip the opacity
defaultColor = Utils.hexToRgba(fillColor, fillOpacity)
}
} else {
if (fillColor.indexOf('rgba') > -1) {
fillOpacity = Utils.getOpacityFromRGBA(fillColor)
} else {
defaultColor = Utils.hexToRgba(Utils.rgb2hex(fillColor), fillOpacity)
}
}
if (opts.opacity) fillOpacity = opts.opacity
if (fillType === 'pattern') {
patternFill = this.handlePatternFill({
fillConfig: opts.fillConfig,
patternFill,
fillColor,
fillOpacity,
defaultColor,
})
}
if (useGradient) {
let colorStops = [...cnf.fill.gradient.colorStops] || []
let type = cnf.fill.gradient.type
if (drawMultiColorLine) {
colorStops[this.seriesIndex] = this.computeColorStops(
w.globals.series[this.seriesIndex],
cnf.plotOptions.line.colors
)
type = 'vertical'
}
gradientFill = this.handleGradientFill({
type,
fillConfig: opts.fillConfig,
fillColor,
fillOpacity,
colorStops,
i: this.seriesIndex,
})
}
if (fillType === 'image') {
let imgSrc = cnf.fill.image.src
let patternID = opts.patternID ? opts.patternID : ''
const patternKey = `pattern${w.globals.cuid}${
opts.seriesNumber + 1
}${patternID}`
if (this.patternIDs.indexOf(patternKey) === -1) {
this.clippedImgArea({
opacity: fillOpacity,
image: Array.isArray(imgSrc)
? opts.seriesNumber < imgSrc.length
? imgSrc[opts.seriesNumber]
: imgSrc[0]
: imgSrc,
width: opts.width ? opts.width : undefined,
height: opts.height ? opts.height : undefined,
patternUnits: opts.patternUnits,
patternID: patternKey,
})
this.patternIDs.push(patternKey)
}
pathFill = `url(#${patternKey})`
} else if (useGradient) {
pathFill = gradientFill
} else if (fillType === 'pattern') {
pathFill = patternFill
} else {
pathFill = defaultColor
}
// override pattern/gradient if opts.solid is true
if (opts.solid) {
pathFill = defaultColor
}
return pathFill
}
getFillType(seriesIndex) {
const w = this.w
if (Array.isArray(w.config.fill.type)) {
return w.config.fill.type[seriesIndex]
} else {
return w.config.fill.type
}
}
getFillColors() {
const w = this.w
const cnf = w.config
const opts = this.opts
let fillColors = []
if (w.globals.comboCharts) {
if (w.config.series[this.seriesIndex].type === 'line') {
if (Array.isArray(w.globals.stroke.colors)) {
fillColors = w.globals.stroke.colors
} else {
fillColors.push(w.globals.stroke.colors)
}
} else {
if (Array.isArray(w.globals.fill.colors)) {
fillColors = w.globals.fill.colors
} else {
fillColors.push(w.globals.fill.colors)
}
}
} else {
if (cnf.chart.type === 'line') {
if (Array.isArray(w.globals.stroke.colors)) {
fillColors = w.globals.stroke.colors
} else {
fillColors.push(w.globals.stroke.colors)
}
} else {
if (Array.isArray(w.globals.fill.colors)) {
fillColors = w.globals.fill.colors
} else {
fillColors.push(w.globals.fill.colors)
}
}
}
// colors passed in arguments
if (typeof opts.fillColors !== 'undefined') {
fillColors = []
if (Array.isArray(opts.fillColors)) {
fillColors = opts.fillColors.slice()
} else {
fillColors.push(opts.fillColors)
}
}
return fillColors
}
handlePatternFill({
fillConfig,
patternFill,
fillColor,
fillOpacity,
defaultColor,
}) {
let fillCnf = this.w.config.fill
if (fillConfig) {
fillCnf = fillConfig
}
const opts = this.opts
let graphics = new Graphics(this.ctx)
let patternStrokeWidth = Array.isArray(fillCnf.pattern.strokeWidth)
? fillCnf.pattern.strokeWidth[this.seriesIndex]
: fillCnf.pattern.strokeWidth
let patternLineColor = fillColor
if (Array.isArray(fillCnf.pattern.style)) {
if (typeof fillCnf.pattern.style[opts.seriesNumber] !== 'undefined') {
let pf = graphics.drawPattern(
fillCnf.pattern.style[opts.seriesNumber],
fillCnf.pattern.width,
fillCnf.pattern.height,
patternLineColor,
patternStrokeWidth,
fillOpacity
)
patternFill = pf
} else {
patternFill = defaultColor
}
} else {
patternFill = graphics.drawPattern(
fillCnf.pattern.style,
fillCnf.pattern.width,
fillCnf.pattern.height,
patternLineColor,
patternStrokeWidth,
fillOpacity
)
}
return patternFill
}
handleGradientFill({
type,
fillColor,
fillOpacity,
fillConfig,
colorStops,
i,
}) {
let fillCnf = this.w.config.fill
if (fillConfig) {
fillCnf = {
...fillCnf,
...fillConfig,
}
}
const opts = this.opts
let graphics = new Graphics(this.ctx)
let utils = new Utils()
type = type || fillCnf.gradient.type
let gradientFrom = fillColor
let gradientTo
let opacityFrom =
fillCnf.gradient.opacityFrom === undefined
? fillOpacity
: Array.isArray(fillCnf.gradient.opacityFrom)
? fillCnf.gradient.opacityFrom[i]
: fillCnf.gradient.opacityFrom
if (gradientFrom.indexOf('rgba') > -1) {
opacityFrom = Utils.getOpacityFromRGBA(gradientFrom)
}
let opacityTo =
fillCnf.gradient.opacityTo === undefined
? fillOpacity
: Array.isArray(fillCnf.gradient.opacityTo)
? fillCnf.gradient.opacityTo[i]
: fillCnf.gradient.opacityTo
if (
fillCnf.gradient.gradientToColors === undefined ||
fillCnf.gradient.gradientToColors.length === 0
) {
if (fillCnf.gradient.shade === 'dark') {
gradientTo = utils.shadeColor(
parseFloat(fillCnf.gradient.shadeIntensity) * -1,
fillColor.indexOf('rgb') > -1 ? Utils.rgb2hex(fillColor) : fillColor
)
} else {
gradientTo = utils.shadeColor(
parseFloat(fillCnf.gradient.shadeIntensity),
fillColor.indexOf('rgb') > -1 ? Utils.rgb2hex(fillColor) : fillColor
)
}
} else {
if (fillCnf.gradient.gradientToColors[opts.seriesNumber]) {
const gToColor = fillCnf.gradient.gradientToColors[opts.seriesNumber]
gradientTo = gToColor
if (gToColor.indexOf('rgba') > -1) {
opacityTo = Utils.getOpacityFromRGBA(gToColor)
}
} else {
gradientTo = fillColor
}
}
if (fillCnf.gradient.gradientFrom) {
gradientFrom = fillCnf.gradient.gradientFrom
}
if (fillCnf.gradient.gradientTo) {
gradientTo = fillCnf.gradient.gradientTo
}
if (fillCnf.gradient.inverseColors) {
let t = gradientFrom
gradientFrom = gradientTo
gradientTo = t
}
if (gradientFrom.indexOf('rgb') > -1) {
gradientFrom = Utils.rgb2hex(gradientFrom)
}
if (gradientTo.indexOf('rgb') > -1) {
gradientTo = Utils.rgb2hex(gradientTo)
}
return graphics.drawGradient(
type,
gradientFrom,
gradientTo,
opacityFrom,
opacityTo,
opts.size,
fillCnf.gradient.stops,
colorStops,
i
)
}
}
export default Fill

172
frontend/node_modules/apexcharts/src/modules/Filters.js generated vendored Normal file
View File

@@ -0,0 +1,172 @@
import Filter from '@svgdotjs/svg.filter.js'
import Utils from './../utils/Utils'
/**
* ApexCharts Filters Class for setting hover/active states on the paths.
*
* @module Formatters
**/
class Filters {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// create a re-usable filter which can be appended other filter effects and applied to multiple elements
getDefaultFilter(el, i) {
const w = this.w
el.unfilter(true)
let filter = new Filter()
filter.size('120%', '180%', '-5%', '-40%')
if (w.config.chart.dropShadow.enabled) {
this.dropShadow(el, w.config.chart.dropShadow, i)
}
}
applyFilter(el, i, filterType) {
const w = this.w
el.unfilter(true)
if (filterType === 'none') {
this.getDefaultFilter(el, i)
return
}
const shadowAttr = w.config.chart.dropShadow
const brightnessFactor = filterType === 'lighten' ? 2 : 0.3
el.filterWith((add) => {
add.colorMatrix({
type: 'matrix',
values: `
${brightnessFactor} 0 0 0 0
0 ${brightnessFactor} 0 0 0
0 0 ${brightnessFactor} 0 0
0 0 0 1 0
`,
in: 'SourceGraphic',
result: 'brightness',
})
if (shadowAttr.enabled) {
this.addShadow(add, i, shadowAttr, 'brightness')
}
})
if (!shadowAttr.noUserSpaceOnUse) {
el.filterer()?.node?.setAttribute('filterUnits', 'userSpaceOnUse')
}
// this scales the filter to a bigger size so that the dropshadow doesn't crops
this._scaleFilterSize(el.filterer()?.node)
}
// appends dropShadow to the filter object which can be chained with other filter effects
addShadow(add, i, attrs, source) {
const w = this.w
let { blur, top, left, color, opacity } = attrs
color = Array.isArray(color) ? color[i] : color
if (w.config.chart.dropShadow.enabledOnSeries?.length > 0) {
if (w.config.chart.dropShadow.enabledOnSeries.indexOf(i) === -1) {
return add
}
}
add.offset({
in: source,
dx: left,
dy: top,
result: 'offset',
})
add.gaussianBlur({
in: 'offset',
stdDeviation: blur,
result: 'blur',
})
add.flood({
'flood-color': color,
'flood-opacity': opacity,
result: 'flood',
})
add.composite({
in: 'flood',
in2: 'blur',
operator: 'in',
result: 'shadow',
})
add.merge(['shadow', source])
}
// directly adds dropShadow to the element and returns the same element.
dropShadow(el, attrs, i = 0) {
const w = this.w
el.unfilter(true)
if (Utils.isMsEdge() && w.config.chart.type === 'radialBar') {
// in radialbar charts, dropshadow is clipping actual drawing in IE
return el
}
if (w.config.chart.dropShadow.enabledOnSeries?.length > 0) {
if (w.config.chart.dropShadow.enabledOnSeries?.indexOf(i) === -1) {
return el
}
}
el.filterWith((add) => {
this.addShadow(add, i, attrs, 'SourceGraphic')
})
if (!attrs.noUserSpaceOnUse) {
el.filterer()?.node?.setAttribute('filterUnits', 'userSpaceOnUse')
}
// this scales the filter to a bigger size so that the dropshadow doesn't crops
this._scaleFilterSize(el.filterer()?.node)
return el
}
setSelectionFilter(el, realIndex, dataPointIndex) {
const w = this.w
if (typeof w.globals.selectedDataPoints[realIndex] !== 'undefined') {
if (
w.globals.selectedDataPoints[realIndex].indexOf(dataPointIndex) > -1
) {
el.node.setAttribute('selected', true)
let activeFilter = w.config.states.active.filter
if (activeFilter !== 'none') {
this.applyFilter(el, realIndex, activeFilter.type)
}
}
}
}
_scaleFilterSize(el) {
if (!el) return
const setAttributes = (attrs) => {
for (let key in attrs) {
if (attrs.hasOwnProperty(key)) {
el.setAttribute(key, attrs[key])
}
}
}
setAttributes({
width: '200%',
height: '200%',
x: '-50%',
y: '-50%',
})
}
}
export default Filters

View File

@@ -0,0 +1,185 @@
import DateTime from '../utils/DateTime'
import Utils from '../utils/Utils'
/**
* ApexCharts Formatter Class for setting value formatters for axes as well as tooltips.
*
* @module Formatters
**/
class Formatters {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.tooltipKeyFormat = 'dd MMM'
}
xLabelFormat(fn, val, timestamp, opts) {
let w = this.w
if (w.config.xaxis.type === 'datetime') {
if (w.config.xaxis.labels.formatter === undefined) {
// if user has not specified a custom formatter, use the default tooltip.x.format
if (w.config.tooltip.x.formatter === undefined) {
let datetimeObj = new DateTime(this.ctx)
return datetimeObj.formatDate(
datetimeObj.getDate(val),
w.config.tooltip.x.format
)
}
}
}
return fn(val, timestamp, opts)
}
defaultGeneralFormatter(val) {
if (Array.isArray(val)) {
return val.map((v) => {
return v
})
} else {
return val
}
}
defaultYFormatter(v, yaxe, i) {
let w = this.w
if (Utils.isNumber(v)) {
if (w.globals.yValueDecimal !== 0) {
v = v.toFixed(
yaxe.decimalsInFloat !== undefined
? yaxe.decimalsInFloat
: w.globals.yValueDecimal
)
} else {
// We have an integer value but the label is not an integer. We can
// deduce this is due to the number of ticks exceeding the even lower
// integer range. Add an additional decimal place only in this case.
const f = v.toFixed(0)
// Do not change the == to ===
v = v == f ? f : v.toFixed(1)
}
}
return v
}
setLabelFormatters() {
let w = this.w
w.globals.xaxisTooltipFormatter = (val) => {
return this.defaultGeneralFormatter(val)
}
w.globals.ttKeyFormatter = (val) => {
return this.defaultGeneralFormatter(val)
}
w.globals.ttZFormatter = (val) => {
return val
}
w.globals.legendFormatter = (val) => {
return this.defaultGeneralFormatter(val)
}
// formatter function will always overwrite format property
if (w.config.xaxis.labels.formatter !== undefined) {
w.globals.xLabelFormatter = w.config.xaxis.labels.formatter
} else {
w.globals.xLabelFormatter = (val) => {
if (Utils.isNumber(val)) {
if (
!w.config.xaxis.convertedCatToNumeric &&
w.config.xaxis.type === 'numeric'
) {
if (Utils.isNumber(w.config.xaxis.decimalsInFloat)) {
return val.toFixed(w.config.xaxis.decimalsInFloat)
} else {
const diff = w.globals.maxX - w.globals.minX
if (diff > 0 && diff < 100) {
return val.toFixed(1)
}
return val.toFixed(0)
}
}
if (w.globals.isBarHorizontal) {
const range = w.globals.maxY - w.globals.minYArr
if (range < 4) {
return val.toFixed(1)
}
}
return val.toFixed(0)
}
return val
}
}
if (typeof w.config.tooltip.x.formatter === 'function') {
w.globals.ttKeyFormatter = w.config.tooltip.x.formatter
} else {
w.globals.ttKeyFormatter = w.globals.xLabelFormatter
}
if (typeof w.config.xaxis.tooltip.formatter === 'function') {
w.globals.xaxisTooltipFormatter = w.config.xaxis.tooltip.formatter
}
if (Array.isArray(w.config.tooltip.y)) {
w.globals.ttVal = w.config.tooltip.y
} else {
if (w.config.tooltip.y.formatter !== undefined) {
w.globals.ttVal = w.config.tooltip.y
}
}
if (w.config.tooltip.z.formatter !== undefined) {
w.globals.ttZFormatter = w.config.tooltip.z.formatter
}
// legend formatter - if user wants to append any global values of series to legend text
if (w.config.legend.formatter !== undefined) {
w.globals.legendFormatter = w.config.legend.formatter
}
// formatter function will always overwrite format property
w.config.yaxis.forEach((yaxe, i) => {
if (yaxe.labels.formatter !== undefined) {
w.globals.yLabelFormatters[i] = yaxe.labels.formatter
} else {
w.globals.yLabelFormatters[i] = (val) => {
if (!w.globals.xyCharts) return val
if (Array.isArray(val)) {
return val.map((v) => {
return this.defaultYFormatter(v, yaxe, i)
})
} else {
return this.defaultYFormatter(val, yaxe, i)
}
}
}
})
return w.globals
}
heatmapLabelFormatters() {
const w = this.w
if (w.config.chart.type === 'heatmap') {
w.globals.yAxisScale[0].result = w.globals.seriesNames.slice()
// get the longest string from the labels array and also apply label formatter to it
let longest = w.globals.seriesNames.reduce(
(a, b) => (a.length > b.length ? a : b),
0
)
w.globals.yAxisScale[0].niceMax = longest
w.globals.yAxisScale[0].niceMin = longest
}
}
}
export default Formatters

1095
frontend/node_modules/apexcharts/src/modules/Graphics.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

277
frontend/node_modules/apexcharts/src/modules/Markers.js generated vendored Normal file
View File

@@ -0,0 +1,277 @@
import Filters from './Filters'
import Graphics from './Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts Markers Class for drawing markers on y values in axes charts.
*
* @module Markers
**/
export default class Markers {
constructor(ctx, opts) {
this.ctx = ctx
this.w = ctx.w
}
setGlobalMarkerSize() {
const w = this.w
w.globals.markers.size = Array.isArray(w.config.markers.size)
? w.config.markers.size
: [w.config.markers.size]
if (w.globals.markers.size.length > 0) {
if (w.globals.markers.size.length < w.globals.series.length + 1) {
for (let i = 0; i <= w.globals.series.length; i++) {
if (typeof w.globals.markers.size[i] === 'undefined') {
w.globals.markers.size.push(w.globals.markers.size[0])
}
}
}
} else {
w.globals.markers.size = w.config.series.map((s) => w.config.markers.size)
}
}
plotChartMarkers({
pointsPos,
seriesIndex,
j,
pSize,
alwaysDrawMarker = false,
isVirtualPoint = false,
}) {
let w = this.w
let i = seriesIndex
let p = pointsPos
let elMarkersWrap = null
let graphics = new Graphics(this.ctx)
const hasDiscreteMarkers =
w.config.markers.discrete && w.config.markers.discrete.length
if (Array.isArray(p.x)) {
for (let q = 0; q < p.x.length; q++) {
let markerElement
let dataPointIndex = j
let invalidMarker = !Utils.isNumber(p.y[q])
if (
w.globals.markers.largestSize === 0 &&
w.globals.hasNullValues &&
w.globals.series[i][j + 1] !== null &&
!isVirtualPoint
) {
invalidMarker = true
}
// a small hack as we have 2 points for the first val to connect it
if (j === 1 && q === 0) dataPointIndex = 0
if (j === 1 && q === 1) dataPointIndex = 1
let markerClasses = 'apexcharts-marker'
if (
(w.config.chart.type === 'line' || w.config.chart.type === 'area') &&
!w.globals.comboCharts &&
!w.config.tooltip.intersect
) {
markerClasses += ' no-pointer-events'
}
const shouldMarkerDraw = Array.isArray(w.config.markers.size)
? w.globals.markers.size[seriesIndex] > 0
: w.config.markers.size > 0
if (shouldMarkerDraw || alwaysDrawMarker || hasDiscreteMarkers) {
if (!invalidMarker) {
markerClasses += ` w${Utils.randomId()}`
}
let opts = this.getMarkerConfig({
cssClass: markerClasses,
seriesIndex,
dataPointIndex,
})
if (w.config.series[i].data[dataPointIndex]) {
if (w.config.series[i].data[dataPointIndex].fillColor) {
opts.pointFillColor =
w.config.series[i].data[dataPointIndex].fillColor
}
if (w.config.series[i].data[dataPointIndex].strokeColor) {
opts.pointStrokeColor =
w.config.series[i].data[dataPointIndex].strokeColor
}
}
if (typeof pSize !== 'undefined') {
opts.pSize = pSize
}
if (
p.x[q] < -w.globals.markers.largestSize ||
p.x[q] > w.globals.gridWidth + w.globals.markers.largestSize ||
p.y[q] < -w.globals.markers.largestSize ||
p.y[q] > w.globals.gridHeight + w.globals.markers.largestSize
) {
opts.pSize = 0
}
if (!invalidMarker) {
const shouldCreateMarkerWrap =
w.globals.markers.size[seriesIndex] > 0 ||
alwaysDrawMarker ||
hasDiscreteMarkers
if (shouldCreateMarkerWrap && !elMarkersWrap) {
elMarkersWrap = graphics.group({
class:
alwaysDrawMarker || hasDiscreteMarkers
? ''
: 'apexcharts-series-markers',
})
elMarkersWrap.attr(
'clip-path',
`url(#gridRectMarkerMask${w.globals.cuid})`
)
}
markerElement = graphics.drawMarker(p.x[q], p.y[q], opts)
markerElement.attr('rel', dataPointIndex)
markerElement.attr('j', dataPointIndex)
markerElement.attr('index', seriesIndex)
markerElement.node.setAttribute('default-marker-size', opts.pSize)
const filters = new Filters(this.ctx)
filters.setSelectionFilter(
markerElement,
seriesIndex,
dataPointIndex
)
this.addEvents(markerElement)
if (elMarkersWrap) {
elMarkersWrap.add(markerElement)
}
}
} else {
// dynamic array creation - multidimensional
if (typeof w.globals.pointsArray[seriesIndex] === 'undefined')
w.globals.pointsArray[seriesIndex] = []
w.globals.pointsArray[seriesIndex].push([p.x[q], p.y[q]])
}
}
}
return elMarkersWrap
}
getMarkerConfig({
cssClass,
seriesIndex,
dataPointIndex = null,
radius = null,
size = null,
strokeWidth = null,
}) {
const w = this.w
let pStyle = this.getMarkerStyle(seriesIndex)
let pSize = size === null ? w.globals.markers.size[seriesIndex] : size
const m = w.config.markers
// discrete markers is an option where user can specify a particular marker with different shape, size and color
if (dataPointIndex !== null && m.discrete.length) {
m.discrete.map((marker) => {
if (
marker.seriesIndex === seriesIndex &&
marker.dataPointIndex === dataPointIndex
) {
pStyle.pointStrokeColor = marker.strokeColor
pStyle.pointFillColor = marker.fillColor
pSize = marker.size
pStyle.pointShape = marker.shape
}
})
}
return {
pSize: radius === null ? pSize : radius,
pRadius: radius !== null ? radius : m.radius,
pointStrokeWidth:
strokeWidth !== null
? strokeWidth
: Array.isArray(m.strokeWidth)
? m.strokeWidth[seriesIndex]
: m.strokeWidth,
pointStrokeColor: pStyle.pointStrokeColor,
pointFillColor: pStyle.pointFillColor,
shape:
pStyle.pointShape ||
(Array.isArray(m.shape) ? m.shape[seriesIndex] : m.shape),
class: cssClass,
pointStrokeOpacity: Array.isArray(m.strokeOpacity)
? m.strokeOpacity[seriesIndex]
: m.strokeOpacity,
pointStrokeDashArray: Array.isArray(m.strokeDashArray)
? m.strokeDashArray[seriesIndex]
: m.strokeDashArray,
pointFillOpacity: Array.isArray(m.fillOpacity)
? m.fillOpacity[seriesIndex]
: m.fillOpacity,
seriesIndex,
}
}
addEvents(marker) {
const w = this.w
const graphics = new Graphics(this.ctx)
marker.node.addEventListener(
'mouseenter',
graphics.pathMouseEnter.bind(this.ctx, marker)
)
marker.node.addEventListener(
'mouseleave',
graphics.pathMouseLeave.bind(this.ctx, marker)
)
marker.node.addEventListener(
'mousedown',
graphics.pathMouseDown.bind(this.ctx, marker)
)
marker.node.addEventListener('click', w.config.markers.onClick)
marker.node.addEventListener('dblclick', w.config.markers.onDblClick)
marker.node.addEventListener(
'touchstart',
graphics.pathMouseDown.bind(this.ctx, marker),
{ passive: true }
)
}
getMarkerStyle(seriesIndex) {
let w = this.w
let colors = w.globals.markers.colors
let strokeColors =
w.config.markers.strokeColor || w.config.markers.strokeColors
let pointStrokeColor = Array.isArray(strokeColors)
? strokeColors[seriesIndex]
: strokeColors
let pointFillColor = Array.isArray(colors) ? colors[seriesIndex] : colors
return {
pointStrokeColor,
pointFillColor,
}
}
}

661
frontend/node_modules/apexcharts/src/modules/Range.js generated vendored Normal file
View File

@@ -0,0 +1,661 @@
import Utils from '../utils/Utils'
import DateTime from '../utils/DateTime'
import Scales from './Scales'
/**
* Range is used to generates values between min and max.
*
* @module Range
**/
class Range {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.scales = new Scales(ctx)
}
init() {
this.setYRange()
this.setXRange()
this.setZRange()
}
getMinYMaxY(
startingSeriesIndex,
lowestY = Number.MAX_VALUE,
highestY = -Number.MAX_VALUE,
endingSeriesIndex = null
) {
const cnf = this.w.config
const gl = this.w.globals
let maxY = -Number.MAX_VALUE
let minY = Number.MIN_VALUE
if (endingSeriesIndex === null) {
endingSeriesIndex = startingSeriesIndex + 1
}
let series = gl.series
let seriesMin = series
let seriesMax = series
if (cnf.chart.type === 'candlestick') {
seriesMin = gl.seriesCandleL
seriesMax = gl.seriesCandleH
} else if (cnf.chart.type === 'boxPlot') {
seriesMin = gl.seriesCandleO
seriesMax = gl.seriesCandleC
} else if (gl.isRangeData) {
seriesMin = gl.seriesRangeStart
seriesMax = gl.seriesRangeEnd
}
let autoScaleYaxis = false
if (gl.seriesX.length >= endingSeriesIndex) {
// Eventually brushSource will be set if the current chart is a target.
// That is, after the appropriate event causes us to update.
let brush = gl.brushSource?.w.config.chart.brush
if (
(cnf.chart.zoom.enabled && cnf.chart.zoom.autoScaleYaxis) ||
(brush?.enabled && brush?.autoScaleYaxis)
) {
autoScaleYaxis = true
}
}
for (let i = startingSeriesIndex; i < endingSeriesIndex; i++) {
gl.dataPoints = Math.max(gl.dataPoints, series[i].length)
const seriesType = cnf.series[i].type
if (gl.categoryLabels.length) {
gl.dataPoints = gl.categoryLabels.filter(
(label) => typeof label !== 'undefined'
).length
}
if (
gl.labels.length &&
cnf.xaxis.type !== 'datetime' &&
gl.series.reduce((a, c) => a + c.length, 0) !== 0
) {
// the condition cnf.xaxis.type !== 'datetime' fixes #3897 and #3905
gl.dataPoints = Math.max(gl.dataPoints, gl.labels.length)
}
let firstXIndex = 0
let lastXIndex = series[i].length - 1
if (autoScaleYaxis) {
// Scale the Y axis to the min..max within the possibly zoomed X axis domain.
if (cnf.xaxis.min) {
for (
;
firstXIndex < lastXIndex &&
gl.seriesX[i][firstXIndex] < cnf.xaxis.min;
firstXIndex++
) {}
}
if (cnf.xaxis.max) {
for (
;
lastXIndex > firstXIndex &&
gl.seriesX[i][lastXIndex] > cnf.xaxis.max;
lastXIndex--
) {}
}
}
for (
let j = firstXIndex;
j <= lastXIndex && j < gl.series[i].length;
j++
) {
let val = series[i][j]
if (val !== null && Utils.isNumber(val)) {
if (typeof seriesMax[i][j] !== 'undefined') {
maxY = Math.max(maxY, seriesMax[i][j])
lowestY = Math.min(lowestY, seriesMax[i][j])
}
if (typeof seriesMin[i][j] !== 'undefined') {
lowestY = Math.min(lowestY, seriesMin[i][j])
highestY = Math.max(highestY, seriesMin[i][j])
}
// These series arrays are dual purpose:
// Array : CandleO, CandleH, CandleM, CandleL, CandleC
// Candlestick: O H L C
// Boxplot : Min Q1 Median Q3 Max
switch (seriesType) {
case 'candlestick':
{
if (typeof gl.seriesCandleC[i][j] !== 'undefined') {
maxY = Math.max(maxY, gl.seriesCandleH[i][j])
lowestY = Math.min(lowestY, gl.seriesCandleL[i][j])
}
}
break
case 'boxPlot':
{
if (typeof gl.seriesCandleC[i][j] !== 'undefined') {
maxY = Math.max(maxY, gl.seriesCandleC[i][j])
lowestY = Math.min(lowestY, gl.seriesCandleO[i][j])
}
}
break
}
// there is a combo chart and the specified series in not either
// candlestick, boxplot, or rangeArea/rangeBar; find the max there.
if (
seriesType &&
seriesType !== 'candlestick' &&
seriesType !== 'boxPlot' &&
seriesType !== 'rangeArea' &&
seriesType !== 'rangeBar'
) {
maxY = Math.max(maxY, gl.series[i][j])
lowestY = Math.min(lowestY, gl.series[i][j])
}
if (
gl.seriesGoals[i] &&
gl.seriesGoals[i][j] &&
Array.isArray(gl.seriesGoals[i][j])
) {
gl.seriesGoals[i][j].forEach((g) => {
maxY = Math.max(maxY, g.value)
lowestY = Math.min(lowestY, g.value)
})
}
highestY = maxY
val = Utils.noExponents(val)
if (Utils.isFloat(val)) {
gl.yValueDecimal = Math.max(
gl.yValueDecimal,
val.toString().split('.')[1].length
)
}
if (minY > seriesMin[i][j] && seriesMin[i][j] < 0) {
minY = seriesMin[i][j]
}
} else {
gl.hasNullValues = true
}
}
if (seriesType === 'bar' || seriesType === 'column') {
if (minY < 0 && maxY < 0) {
// all negative values in a bar series, hence make the max to 0
maxY = 0
highestY = Math.max(highestY, 0)
}
if (minY === Number.MIN_VALUE) {
minY = 0
lowestY = Math.min(lowestY, 0)
}
}
}
if (
cnf.chart.type === 'rangeBar' &&
gl.seriesRangeStart.length &&
gl.isBarHorizontal
) {
minY = lowestY
}
if (cnf.chart.type === 'bar') {
if (minY < 0 && maxY < 0) {
// all negative values in a bar chart, hence make the max to 0
maxY = 0
}
if (minY === Number.MIN_VALUE) {
minY = 0
}
}
return {
minY,
maxY,
lowestY,
highestY,
}
}
setYRange() {
let gl = this.w.globals
let cnf = this.w.config
gl.maxY = -Number.MAX_VALUE
gl.minY = Number.MIN_VALUE
let lowestYInAllSeries = Number.MAX_VALUE
let minYMaxY
if (gl.isMultipleYAxis) {
// we need to get minY and maxY for multiple y axis
lowestYInAllSeries = Number.MAX_VALUE
for (let i = 0; i < gl.series.length; i++) {
minYMaxY = this.getMinYMaxY(i)
gl.minYArr[i] = minYMaxY.lowestY
gl.maxYArr[i] = minYMaxY.highestY
lowestYInAllSeries = Math.min(lowestYInAllSeries, minYMaxY.lowestY)
}
}
// and then, get the minY and maxY from all series
minYMaxY = this.getMinYMaxY(0, lowestYInAllSeries, null, gl.series.length)
if (cnf.chart.type === 'bar') {
gl.minY = minYMaxY.minY
gl.maxY = minYMaxY.maxY
} else {
gl.minY = minYMaxY.lowestY
gl.maxY = minYMaxY.highestY
}
lowestYInAllSeries = minYMaxY.lowestY
if (cnf.chart.stacked) {
this._setStackedMinMax()
}
// if the numbers are too big, reduce the range
// for eg, if number is between 100000-110000, putting 0 as the lowest
// value is not so good idea. So change the gl.minY for
// line/area/scatter/candlesticks/boxPlot/vertical rangebar
if (
cnf.chart.type === 'line' ||
cnf.chart.type === 'area' ||
cnf.chart.type === 'scatter' ||
cnf.chart.type === 'candlestick' ||
cnf.chart.type === 'boxPlot' ||
(cnf.chart.type === 'rangeBar' && !gl.isBarHorizontal)
) {
if (
gl.minY === Number.MIN_VALUE &&
lowestYInAllSeries !== -Number.MAX_VALUE &&
lowestYInAllSeries !== gl.maxY // single value possibility
) {
gl.minY = lowestYInAllSeries
}
} else {
gl.minY =
gl.minY !== Number.MIN_VALUE
? Math.min(minYMaxY.minY, gl.minY)
: minYMaxY.minY
}
cnf.yaxis.forEach((yaxe, index) => {
// override all min/max values by user defined values (y axis)
if (yaxe.max !== undefined) {
if (typeof yaxe.max === 'number') {
gl.maxYArr[index] = yaxe.max
} else if (typeof yaxe.max === 'function') {
// fixes apexcharts.js/issues/2098
gl.maxYArr[index] = yaxe.max(
gl.isMultipleYAxis ? gl.maxYArr[index] : gl.maxY
)
}
// gl.maxY is for single y-axis chart, it will be ignored in multi-yaxis
gl.maxY = gl.maxYArr[index]
}
if (yaxe.min !== undefined) {
if (typeof yaxe.min === 'number') {
gl.minYArr[index] = yaxe.min
} else if (typeof yaxe.min === 'function') {
// fixes apexcharts.js/issues/2098
gl.minYArr[index] = yaxe.min(
gl.isMultipleYAxis
? gl.minYArr[index] === Number.MIN_VALUE
? 0
: gl.minYArr[index]
: gl.minY
)
}
// gl.minY is for single y-axis chart, it will be ignored in multi-yaxis
gl.minY = gl.minYArr[index]
}
})
// for horizontal bar charts, we need to check xaxis min/max as user may have specified there
if (gl.isBarHorizontal) {
const minmax = ['min', 'max']
minmax.forEach((m) => {
if (cnf.xaxis[m] !== undefined && typeof cnf.xaxis[m] === 'number') {
m === 'min' ? (gl.minY = cnf.xaxis[m]) : (gl.maxY = cnf.xaxis[m])
}
})
}
if (gl.isMultipleYAxis) {
this.scales.scaleMultipleYAxes()
gl.minY = lowestYInAllSeries
} else {
this.scales.setYScaleForIndex(0, gl.minY, gl.maxY)
gl.minY = gl.yAxisScale[0].niceMin
gl.maxY = gl.yAxisScale[0].niceMax
gl.minYArr[0] = gl.minY
gl.maxYArr[0] = gl.maxY
}
gl.barGroups = []
gl.lineGroups = []
gl.areaGroups = []
cnf.series.forEach((s) => {
let type = s.type || cnf.chart.type
switch (type) {
case 'bar':
case 'column':
gl.barGroups.push(s.group)
break
case 'line':
gl.lineGroups.push(s.group)
break
case 'area':
gl.areaGroups.push(s.group)
break
}
})
// Uniquify the group names in each stackable chart type.
gl.barGroups = gl.barGroups.filter((v, i, a) => a.indexOf(v) === i)
gl.lineGroups = gl.lineGroups.filter((v, i, a) => a.indexOf(v) === i)
gl.areaGroups = gl.areaGroups.filter((v, i, a) => a.indexOf(v) === i)
return {
minY: gl.minY,
maxY: gl.maxY,
minYArr: gl.minYArr,
maxYArr: gl.maxYArr,
yAxisScale: gl.yAxisScale,
}
}
setXRange() {
let gl = this.w.globals
let cnf = this.w.config
const isXNumeric =
cnf.xaxis.type === 'numeric' ||
cnf.xaxis.type === 'datetime' ||
(cnf.xaxis.type === 'category' && !gl.noLabelsProvided) ||
gl.noLabelsProvided ||
gl.isXNumeric
const getInitialMinXMaxX = () => {
for (let i = 0; i < gl.series.length; i++) {
if (gl.labels[i]) {
for (let j = 0; j < gl.labels[i].length; j++) {
if (gl.labels[i][j] !== null && Utils.isNumber(gl.labels[i][j])) {
gl.maxX = Math.max(gl.maxX, gl.labels[i][j])
gl.initialMaxX = Math.max(gl.maxX, gl.labels[i][j])
gl.minX = Math.min(gl.minX, gl.labels[i][j])
gl.initialMinX = Math.min(gl.minX, gl.labels[i][j])
}
}
}
}
}
// minX maxX starts here
if (gl.isXNumeric) {
getInitialMinXMaxX()
}
if (gl.noLabelsProvided) {
if (cnf.xaxis.categories.length === 0) {
gl.maxX = gl.labels[gl.labels.length - 1]
gl.initialMaxX = gl.labels[gl.labels.length - 1]
gl.minX = 1
gl.initialMinX = 1
}
}
if (gl.isXNumeric || gl.noLabelsProvided || gl.dataFormatXNumeric) {
let ticks = 10
if (cnf.xaxis.tickAmount === undefined) {
ticks = Math.round(gl.svgWidth / 150)
// no labels provided and total number of dataPoints is less than 30
if (cnf.xaxis.type === 'numeric' && gl.dataPoints < 30) {
ticks = gl.dataPoints - 1
}
// this check is for when ticks exceeds total datapoints and that would result in duplicate labels
if (ticks > gl.dataPoints && gl.dataPoints !== 0) {
ticks = gl.dataPoints - 1
}
} else if (cnf.xaxis.tickAmount === 'dataPoints') {
if (gl.series.length > 1) {
ticks = gl.series[gl.maxValsInArrayIndex].length - 1
}
if (gl.isXNumeric) {
const diff = gl.maxX - gl.minX
if (diff < 30) {
ticks = diff - 1
}
}
} else {
ticks = cnf.xaxis.tickAmount
}
gl.xTickAmount = ticks
// override all min/max values by user defined values (x axis)
if (cnf.xaxis.max !== undefined && typeof cnf.xaxis.max === 'number') {
gl.maxX = cnf.xaxis.max
}
if (cnf.xaxis.min !== undefined && typeof cnf.xaxis.min === 'number') {
gl.minX = cnf.xaxis.min
}
// if range is provided, adjust the new minX
if (cnf.xaxis.range !== undefined) {
gl.minX = gl.maxX - cnf.xaxis.range
}
if (gl.minX !== Number.MAX_VALUE && gl.maxX !== -Number.MAX_VALUE) {
if (cnf.xaxis.convertedCatToNumeric && !gl.dataFormatXNumeric) {
let catScale = []
for (let i = gl.minX - 1; i < gl.maxX; i++) {
catScale.push(i + 1)
}
gl.xAxisScale = {
result: catScale,
niceMin: catScale[0],
niceMax: catScale[catScale.length - 1],
}
} else {
gl.xAxisScale = this.scales.setXScale(gl.minX, gl.maxX)
}
} else {
gl.xAxisScale = this.scales.linearScale(
0,
ticks,
ticks,
0,
cnf.xaxis.stepSize
)
if (gl.noLabelsProvided && gl.labels.length > 0) {
gl.xAxisScale = this.scales.linearScale(
1,
gl.labels.length,
ticks - 1,
0,
cnf.xaxis.stepSize
)
// this is the only place seriesX is again mutated
gl.seriesX = gl.labels.slice()
}
}
// we will still store these labels as the count for this will be different (to draw grid and labels placement)
if (isXNumeric) {
gl.labels = gl.xAxisScale.result.slice()
}
}
if (gl.isBarHorizontal && gl.labels.length) {
gl.xTickAmount = gl.labels.length
}
// single dataPoint
this._handleSingleDataPoint()
// minimum x difference to calculate bar width in numeric bars
this._getMinXDiff()
return {
minX: gl.minX,
maxX: gl.maxX,
}
}
setZRange() {
// minZ, maxZ starts here
let gl = this.w.globals
if (!gl.isDataXYZ) return
for (let i = 0; i < gl.series.length; i++) {
if (typeof gl.seriesZ[i] !== 'undefined') {
for (let j = 0; j < gl.seriesZ[i].length; j++) {
if (gl.seriesZ[i][j] !== null && Utils.isNumber(gl.seriesZ[i][j])) {
gl.maxZ = Math.max(gl.maxZ, gl.seriesZ[i][j])
gl.minZ = Math.min(gl.minZ, gl.seriesZ[i][j])
}
}
}
}
}
_handleSingleDataPoint() {
const gl = this.w.globals
const cnf = this.w.config
if (gl.minX === gl.maxX) {
let datetimeObj = new DateTime(this.ctx)
if (cnf.xaxis.type === 'datetime') {
const newMinX = datetimeObj.getDate(gl.minX)
if (cnf.xaxis.labels.datetimeUTC) {
newMinX.setUTCDate(newMinX.getUTCDate() - 2)
} else {
newMinX.setDate(newMinX.getDate() - 2)
}
gl.minX = new Date(newMinX).getTime()
const newMaxX = datetimeObj.getDate(gl.maxX)
if (cnf.xaxis.labels.datetimeUTC) {
newMaxX.setUTCDate(newMaxX.getUTCDate() + 2)
} else {
newMaxX.setDate(newMaxX.getDate() + 2)
}
gl.maxX = new Date(newMaxX).getTime()
} else if (
cnf.xaxis.type === 'numeric' ||
(cnf.xaxis.type === 'category' && !gl.noLabelsProvided)
) {
gl.minX = gl.minX - 2
gl.initialMinX = gl.minX
gl.maxX = gl.maxX + 2
gl.initialMaxX = gl.maxX
}
}
}
_getMinXDiff() {
const gl = this.w.globals
if (gl.isXNumeric) {
// get the least x diff if numeric x axis is present
gl.seriesX.forEach((sX, i) => {
if (sX.length) {
if (sX.length === 1) {
// a small hack to prevent overlapping multiple bars when there is just 1 datapoint in bar series.
// fix #811
sX.push(
gl.seriesX[gl.maxValsInArrayIndex][
gl.seriesX[gl.maxValsInArrayIndex].length - 1
]
)
}
// fix #983 (clone the array to avoid side effects)
const seriesX = sX.slice()
seriesX.sort((a, b) => a - b)
seriesX.forEach((s, j) => {
if (j > 0) {
let xDiff = s - seriesX[j - 1]
if (xDiff > 0) {
gl.minXDiff = Math.min(xDiff, gl.minXDiff)
}
}
})
if (gl.dataPoints === 1 || gl.minXDiff === Number.MAX_VALUE) {
// fixes apexcharts.js #1221
gl.minXDiff = 0.5
}
}
})
}
}
_setStackedMinMax() {
const gl = this.w.globals
// for stacked charts, we calculate each series's parallel values.
// i.e, series[0][j] + series[1][j] .... [series[i.length][j]]
// and get the max out of it
if (!gl.series.length) return
let seriesGroups = gl.seriesGroups
if (!seriesGroups.length) {
seriesGroups = [this.w.globals.seriesNames.map((name) => name)]
}
let stackedPoss = {}
let stackedNegs = {}
seriesGroups.forEach((group) => {
stackedPoss[group] = []
stackedNegs[group] = []
const indicesOfSeriesInGroup = this.w.config.series
.map((serie, si) =>
group.indexOf(gl.seriesNames[si]) > -1 ? si : null
)
.filter((f) => f !== null)
indicesOfSeriesInGroup.forEach((i) => {
for (let j = 0; j < gl.series[gl.maxValsInArrayIndex].length; j++) {
if (typeof stackedPoss[group][j] === 'undefined') {
stackedPoss[group][j] = 0
stackedNegs[group][j] = 0
}
let stackSeries =
(this.w.config.chart.stacked && !gl.comboCharts) ||
(this.w.config.chart.stacked &&
gl.comboCharts &&
(!this.w.config.chart.stackOnlyBar ||
this.w.config.series?.[i]?.type === 'bar' ||
this.w.config.series?.[i]?.type === 'column'))
if (stackSeries) {
if (gl.series[i][j] !== null && Utils.isNumber(gl.series[i][j])) {
gl.series[i][j] > 0
? (stackedPoss[group][j] +=
parseFloat(gl.series[i][j]) + 0.0001)
: (stackedNegs[group][j] += parseFloat(gl.series[i][j]))
}
}
}
})
})
Object.entries(stackedPoss).forEach(([key]) => {
stackedPoss[key].forEach((_, stgi) => {
gl.maxY = Math.max(gl.maxY, stackedPoss[key][stgi])
gl.minY = Math.min(gl.minY, stackedNegs[key][stgi])
})
})
}
}
export default Range

View File

@@ -0,0 +1,78 @@
import Config from './settings/Config'
import Utils from '../utils/Utils'
import CoreUtils from './CoreUtils'
/**
* ApexCharts Responsive Class to override options for different screen sizes.
*
* @module Responsive
**/
export default class Responsive {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// the opts parameter if not null has to be set overriding everything
// as the opts is set by user externally
checkResponsiveConfig(opts) {
const w = this.w
const cnf = w.config
// check if responsive config exists
if (cnf.responsive.length === 0) return
let res = cnf.responsive.slice()
res
.sort((a, b) =>
a.breakpoint > b.breakpoint ? 1 : b.breakpoint > a.breakpoint ? -1 : 0
)
.reverse()
let config = new Config({})
const iterateResponsiveOptions = (newOptions = {}) => {
let largestBreakpoint = res[0].breakpoint
const width = window.innerWidth > 0 ? window.innerWidth : screen.width
if (width > largestBreakpoint) {
let initialConfig = Utils.clone(w.globals.initialConfig)
// Retain state of series in case any have been collapsed
// (indicated by series.data === [], these series' will be zeroed later
// enabling stacking to work correctly)
initialConfig.series = Utils.clone(w.config.series)
let options = CoreUtils.extendArrayProps(
config,
initialConfig,
w
)
newOptions = Utils.extend(options, newOptions)
newOptions = Utils.extend(w.config, newOptions)
this.overrideResponsiveOptions(newOptions)
} else {
for (let i = 0; i < res.length; i++) {
if (width < res[i].breakpoint) {
newOptions = CoreUtils.extendArrayProps(config, res[i].options, w)
newOptions = Utils.extend(w.config, newOptions)
this.overrideResponsiveOptions(newOptions)
}
}
}
}
if (opts) {
let options = CoreUtils.extendArrayProps(config, opts, w)
options = Utils.extend(w.config, options)
options = Utils.extend(options, opts)
iterateResponsiveOptions(options)
} else {
iterateResponsiveOptions({})
}
}
overrideResponsiveOptions(newOptions) {
let newConfig = new Config(newOptions).init({ responsiveOverride: true })
this.w.config = newConfig
}
}

754
frontend/node_modules/apexcharts/src/modules/Scales.js generated vendored Normal file
View File

@@ -0,0 +1,754 @@
import CoreUtils from './CoreUtils'
import Utils from '../utils/Utils'
export default class Scales {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.coreUtils = new CoreUtils(this.ctx)
}
// http://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
// This routine creates the Y axis values for a graph.
niceScale(yMin, yMax, index = 0) {
// Calculate Min amd Max graphical labels and graph
// increments.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
const jsPrecision = 1e-11 // JS precision errors
const w = this.w
const gl = w.globals
let axisCnf
let maxTicks
let gotMin
let gotMax
if (gl.isBarHorizontal) {
axisCnf = w.config.xaxis
// The most ticks we can fit into the svg chart dimensions
maxTicks = Math.max((gl.svgWidth - 100) / 25, 2) // Guestimate
} else {
axisCnf = w.config.yaxis[index]
maxTicks = Math.max((gl.svgHeight - 100) / 15, 2)
}
if (!Utils.isNumber(maxTicks)) {
maxTicks = 10
}
gotMin = axisCnf.min !== undefined && axisCnf.min !== null
gotMax = axisCnf.max !== undefined && axisCnf.min !== null
let gotStepSize =
axisCnf.stepSize !== undefined && axisCnf.stepSize !== null
let gotTickAmount =
axisCnf.tickAmount !== undefined && axisCnf.tickAmount !== null
let ticks = gotTickAmount
? axisCnf.tickAmount
: gl.niceScaleDefaultTicks[
Math.min(
Math.round(maxTicks / 2),
gl.niceScaleDefaultTicks.length - 1
)
]
// In case we have a multi axis chart:
// Ensure subsequent series start with the same tickAmount as series[0],
// because the tick lines are drawn based on series[0]. This does not
// override user defined options for any yaxis.
if (gl.isMultipleYAxis && !gotTickAmount && gl.multiAxisTickAmount > 0) {
ticks = gl.multiAxisTickAmount
gotTickAmount = true
}
if (ticks === 'dataPoints') {
ticks = gl.dataPoints - 1
} else {
// Ensure ticks is an integer
ticks = Math.abs(Math.round(ticks))
}
if (
(yMin === Number.MIN_VALUE && yMax === 0) ||
(!Utils.isNumber(yMin) && !Utils.isNumber(yMax)) ||
(yMin === Number.MIN_VALUE && yMax === -Number.MAX_VALUE)
) {
// when all values are 0
yMin = Utils.isNumber(axisCnf.min) ? axisCnf.min : 0
yMax = Utils.isNumber(axisCnf.max) ? axisCnf.max : yMin + ticks
gl.allSeriesCollapsed = false
}
if (yMin > yMax) {
// if somehow due to some wrong config, user sent max less than min,
// adjust the min/max again
console.warn(
'axis.min cannot be greater than axis.max: swapping min and max'
)
let temp = yMax
yMax = yMin
yMin = temp
} else if (yMin === yMax) {
// If yMin and yMax are identical, then
// adjust the yMin and yMax values to actually
// make a graph. Also avoids division by zero errors.
yMin = yMin === 0 ? 0 : yMin - 1 // choose an integer in case yValueDecimals=0
yMax = yMax === 0 ? 2 : yMax + 1 // choose an integer in case yValueDecimals=0
}
let result = []
if (ticks < 1) {
ticks = 1
}
let tiks = ticks
// Determine Range
let range = Math.abs(yMax - yMin)
// Snap min or max to zero if close
let proximityRatio = 0.15
if (!gotMin && yMin > 0 && yMin / range < proximityRatio) {
yMin = 0
gotMin = true
}
if (!gotMax && yMax < 0 && -yMax / range < proximityRatio) {
yMax = 0
gotMax = true
}
range = Math.abs(yMax - yMin)
// Calculate a pretty step value based on ticks
// Initial stepSize
let stepSize = range / tiks
let niceStep = stepSize
let mag = Math.floor(Math.log10(niceStep))
let magPow = Math.pow(10, mag)
// ceil() is used below in conjunction with the values populating
// niceScaleAllowedMagMsd[][] to ensure that (niceStep * tiks)
// produces a range that doesn't clip data points after stretching
// the raw range out a little to match the prospective new range.
let magMsd = Math.ceil(niceStep / magPow)
// See globals.js for info on what niceScaleAllowedMagMsd does
magMsd = gl.niceScaleAllowedMagMsd[gl.yValueDecimal === 0 ? 0 : 1][magMsd]
niceStep = magMsd * magPow
// Initial stepSize
stepSize = niceStep
// Get step value
if (gl.isBarHorizontal && axisCnf.stepSize && axisCnf.type !== 'datetime') {
stepSize = axisCnf.stepSize
gotStepSize = true
} else if (gotStepSize) {
stepSize = axisCnf.stepSize
}
if (gotStepSize) {
if (axisCnf.forceNiceScale) {
// Check that given stepSize is sane with respect to the range.
//
// The user can, by setting forceNiceScale = true,
// define a stepSize that will be scaled to a useful value before
// it's checked for consistency.
//
// If, for example, the range = 4 and the user defined stepSize = 8
// (or 8000 or 0.0008, etc), then stepSize is inapplicable as
// it is. Reducing it to 0.8 will fit with 5 ticks.
//
let stepMag = Math.floor(Math.log10(stepSize))
stepSize *= Math.pow(10, mag - stepMag)
}
}
// Start applying some rules
if (gotMin && gotMax) {
let crudeStep = range / tiks
// min and max (range) cannot be changed
if (gotTickAmount) {
if (gotStepSize) {
if (Utils.mod(range, stepSize) != 0) {
// stepSize conflicts with range
let gcdStep = Utils.getGCD(stepSize, crudeStep)
// gcdStep is a multiple of range because crudeStep is a multiple.
// gcdStep is also a multiple of stepSize, so it partially honoured
// All three could be equal, which would be very nice
// if the computed stepSize generates too many ticks they will be
// reduced later, unless the number is prime, in which case,
// the chart will display all of them or just one (plus the X axis)
// depending on svg dimensions. Setting forceNiceScale: true will force
// the display of at least the default number of ticks.
if (crudeStep / gcdStep < 10) {
stepSize = gcdStep
} else {
// stepSize conflicts and no reasonable adjustment, but must
// honour tickAmount
stepSize = crudeStep
}
} else {
// stepSize fits
if (Utils.mod(stepSize, crudeStep) == 0) {
// crudeStep is a multiple of stepSize, or vice versa
// but we know that crudeStep will generate tickAmount ticks
stepSize = crudeStep
} else {
// stepSize conflicts with tickAmount
// if the user is setting up a multi-axis chart and wants
// synced axis ticks then they should not define stepSize
// or ensure there is no conflict between any of their options
// on any axis.
crudeStep = stepSize
// De-prioritizing ticks from now on
gotTickAmount = false
}
}
} else {
// no user stepSize, honour tickAmount
stepSize = crudeStep
}
} else {
// default ticks in use, tiks can change
if (gotStepSize) {
if (Utils.mod(range, stepSize) == 0) {
// user stepSize fits
crudeStep = stepSize
} else {
stepSize = crudeStep
}
} else {
// no user stepSize
if (Utils.mod(range, stepSize) == 0) {
// generated nice stepSize fits
crudeStep = stepSize
} else {
tiks = Math.ceil(range / stepSize)
crudeStep = range / tiks
let gcdStep = Utils.getGCD(range, stepSize)
if (range / gcdStep < maxTicks) {
crudeStep = gcdStep
}
stepSize = crudeStep
}
}
}
tiks = Math.round(range / stepSize)
} else {
// Snap range to ticks
if (!gotMin && !gotMax) {
if (gl.isMultipleYAxis && gotTickAmount) {
// Ensure graph doesn't clip.
let tMin = stepSize * Math.floor(yMin / stepSize)
let tMax = tMin + stepSize * tiks
if (tMax < yMax) {
stepSize *= 2
}
yMin = tMin
tMax = yMax
yMax = yMin + stepSize * tiks
// Snap min or max to zero if possible
range = Math.abs(yMax - yMin)
if (yMin > 0 && yMin < Math.abs(tMax - yMax)) {
yMin = 0
yMax = stepSize * tiks
}
if (yMax < 0 && -yMax < Math.abs(tMin - yMin)) {
yMax = 0
yMin = -stepSize * tiks
}
} else {
yMin = stepSize * Math.floor(yMin / stepSize)
yMax = stepSize * Math.ceil(yMax / stepSize)
}
} else if (gotMax) {
if (gotTickAmount) {
yMin = yMax - stepSize * tiks
} else {
let yMinPrev = yMin
yMin = stepSize * Math.floor(yMin / stepSize)
if (
Math.abs(yMax - yMin) / Utils.getGCD(range, stepSize) >
maxTicks
) {
// Use default ticks to compute yMin then shrinkwrap
yMin = yMax - stepSize * ticks
yMin += stepSize * Math.floor((yMinPrev - yMin) / stepSize)
}
}
} else if (gotMin) {
if (gotTickAmount) {
yMax = yMin + stepSize * tiks
} else {
let yMaxPrev = yMax
yMax = stepSize * Math.ceil(yMax / stepSize)
if (
Math.abs(yMax - yMin) / Utils.getGCD(range, stepSize) >
maxTicks
) {
// Use default ticks to compute yMin then shrinkwrap
yMax = yMin + stepSize * ticks
yMax += stepSize * Math.ceil((yMaxPrev - yMax) / stepSize)
}
}
}
range = Math.abs(yMax - yMin)
// Final check and possible adjustment of stepSize to prevent
// overriding the user's min or max choice.
stepSize = Utils.getGCD(range, stepSize)
tiks = Math.round(range / stepSize)
}
// Shrinkwrap ticks to the range
if (!gotTickAmount && !(gotMin || gotMax)) {
tiks = Math.ceil((range - jsPrecision) / (stepSize + jsPrecision))
// No user tickAmount, or min or max, we are free to adjust to avoid a
// prime number. This helps when reducing ticks for small svg dimensions.
if (tiks > 16 && Utils.getPrimeFactors(tiks).length < 2) {
tiks++
}
}
// Prune tiks down to range if series is all integers. Since tiks > range,
// range is very low (< 10 or so). Skip this step if gotTickAmount is true
// because either the user set tickAmount or the chart is multiscale and
// this axis is not determining the number of grid lines.
if (
!gotTickAmount &&
axisCnf.forceNiceScale &&
gl.yValueDecimal === 0 &&
tiks > range
) {
tiks = range
stepSize = Math.round(range / tiks)
}
if (
tiks > maxTicks &&
(!(gotTickAmount || gotStepSize) || axisCnf.forceNiceScale)
) {
// Reduce the number of ticks nicely if chart svg dimensions shrink too far.
// The reduced tick set should always be a subset of the full set.
//
// This following products of prime factors method works as follows:
// We compute the prime factors of the full tick count (tiks), then all the
// possible products of those factors in order from smallest to biggest,
// until we find a product P such that: tiks/P < maxTicks.
//
// Example:
// Computing products of the prime factors of 30.
//
// tiks | pf | 1 2 3 4 5 6 <-- compute order
// --------------------------------------------------
// 30 | 5 | 5 5 5 <-- Multiply all
// | 3 | 3 3 3 3 <-- primes in each
// | 2 | 2 2 2 <-- column = P
// --------------------------------------------------
// 15 10 6 5 2 1 <-- tiks/P
//
// tiks = 30 has prime factors [2, 3, 5]
// The loop below computes the products [2,3,5,6,15,30].
// The last product of P = 2*3*5 is skipped since 30/P = 1.
// This yields tiks/P = [15,10,6,5,2,1], checked in order until
// tiks/P < maxTicks.
//
// Pros:
// 1) The ticks in the reduced set are always members of the
// full set of ticks.
// Cons:
// 1) None: if tiks is prime, we get all or one, nothing between, so
// the worst case is to display all, which is the status quo. Really
// only a problem visually for larger tick numbers, say, > 7.
//
let pf = Utils.getPrimeFactors(tiks)
let last = pf.length - 1
let tt = tiks
reduceLoop: for (var xFactors = 0; xFactors < last; xFactors++) {
for (var lowest = 0; lowest <= last - xFactors; lowest++) {
let stop = Math.min(lowest + xFactors, last)
let t = tt
let div = 1
for (var next = lowest; next <= stop; next++) {
div *= pf[next]
}
t /= div
if (t < maxTicks) {
tt = t
break reduceLoop
}
}
}
if (tt === tiks) {
// Could not reduce ticks at all, go all in and display just the
// X axis and one tick.
stepSize = range
} else {
stepSize = range / tt
}
tiks = Math.round(range / stepSize)
}
// Record final tiks for use by other series that call niceScale().
// Note: some don't, like logarithmicScale(), etc.
if (
gl.isMultipleYAxis &&
gl.multiAxisTickAmount == 0 &&
gl.ignoreYAxisIndexes.indexOf(index) < 0
) {
gl.multiAxisTickAmount = tiks
}
// build Y label array.
let val = yMin - stepSize
// Ensure we don't under/over shoot due to JS precision errors.
// This also fixes (amongst others):
// https://github.com/apexcharts/apexcharts.js/issues/430
let err = stepSize * jsPrecision
do {
val += stepSize
result.push(Utils.stripNumber(val, 7))
} while (yMax - val > err)
return {
result,
niceMin: result[0],
niceMax: result[result.length - 1],
}
}
linearScale(yMin, yMax, ticks = 10, index = 0, step = undefined) {
let range = Math.abs(yMax - yMin)
let result = []
if (yMin === yMax) {
result = [yMin]
return {
result,
niceMin: result[0],
niceMax: result[result.length - 1],
}
}
ticks = this._adjustTicksForSmallRange(ticks, index, range)
if (ticks === 'dataPoints') {
ticks = this.w.globals.dataPoints - 1
}
if (!step) {
step = range / ticks
}
step = Math.round((step + Number.EPSILON) * 10) / 10
if (ticks === Number.MAX_VALUE) {
ticks = 5
step = 1
}
let v = yMin
while (ticks >= 0) {
result.push(v)
v = Utils.preciseAddition(v, step)
ticks -= 1
}
return {
result,
niceMin: result[0],
niceMax: result[result.length - 1],
}
}
logarithmicScaleNice(yMin, yMax, base) {
// Basic validation to avoid for loop starting at -inf.
if (yMax <= 0) yMax = Math.max(yMin, base)
if (yMin <= 0) yMin = Math.min(yMax, base)
const logs = []
// Get powers of base for our max and min
const logMax = Math.ceil(Math.log(yMax) / Math.log(base) + 1)
const logMin = Math.floor(Math.log(yMin) / Math.log(base))
for (let i = logMin; i < logMax; i++) {
logs.push(Math.pow(base, i))
}
return {
result: logs,
niceMin: logs[0],
niceMax: logs[logs.length - 1],
}
}
logarithmicScale(yMin, yMax, base) {
// Basic validation to avoid for loop starting at -inf.
if (yMax <= 0) yMax = Math.max(yMin, base)
if (yMin <= 0) yMin = Math.min(yMax, base)
const logs = []
// Get the logarithmic range.
const logMax = Math.log(yMax) / Math.log(base)
const logMin = Math.log(yMin) / Math.log(base)
// Get the exact logarithmic range.
// (This is the exact number of multiples of the base there are between yMin and yMax).
const logRange = logMax - logMin
// Round the logarithmic range to get the number of ticks we will create.
// If the chosen min/max values are multiples of each other WRT the base, this will be neat.
// If the chosen min/max aren't, we will at least still provide USEFUL ticks.
const ticks = Math.round(logRange)
// Get the logarithmic spacing between ticks.
const logTickSpacing = logRange / ticks
// Create as many ticks as there is range in the logs.
for (
let i = 0, logTick = logMin;
i < ticks;
i++, logTick += logTickSpacing
) {
logs.push(Math.pow(base, logTick))
}
// Add a final tick at the yMax.
logs.push(Math.pow(base, logMax))
return {
result: logs,
niceMin: yMin,
niceMax: yMax,
}
}
_adjustTicksForSmallRange(ticks, index, range) {
let newTicks = ticks
if (
typeof index !== 'undefined' &&
this.w.config.yaxis[index].labels.formatter &&
this.w.config.yaxis[index].tickAmount === undefined
) {
const formattedVal = Number(
this.w.config.yaxis[index].labels.formatter(1)
)
if (Utils.isNumber(formattedVal) && this.w.globals.yValueDecimal === 0) {
newTicks = Math.ceil(range)
}
}
return newTicks < ticks ? newTicks : ticks
}
setYScaleForIndex(index, minY, maxY) {
const gl = this.w.globals
const cnf = this.w.config
let y = gl.isBarHorizontal ? cnf.xaxis : cnf.yaxis[index]
if (typeof gl.yAxisScale[index] === 'undefined') {
gl.yAxisScale[index] = []
}
let range = Math.abs(maxY - minY)
if (y.logarithmic && range <= 5) {
gl.invalidLogScale = true
}
if (y.logarithmic && range > 5) {
gl.allSeriesCollapsed = false
gl.yAxisScale[index] = y.forceNiceScale
? this.logarithmicScaleNice(minY, maxY, y.logBase)
: this.logarithmicScale(minY, maxY, y.logBase)
} else {
if (
maxY === -Number.MAX_VALUE ||
!Utils.isNumber(maxY) ||
minY === Number.MAX_VALUE ||
!Utils.isNumber(minY)
) {
// no data in the chart.
// Either all series collapsed or user passed a blank array.
// Show the user's yaxis with their scale options but with a range.
gl.yAxisScale[index] = this.niceScale(Number.MIN_VALUE, 0, index)
} else {
// there is some data. Turn off the allSeriesCollapsed flag
gl.allSeriesCollapsed = false
gl.yAxisScale[index] = this.niceScale(minY, maxY, index)
}
}
}
setXScale(minX, maxX) {
const w = this.w
const gl = w.globals
let diff = Math.abs(maxX - minX)
if (maxX === -Number.MAX_VALUE || !Utils.isNumber(maxX)) {
// no data in the chart. Either all series collapsed or user passed a blank array
gl.xAxisScale = this.linearScale(0, 10, 10)
} else {
let ticks = gl.xTickAmount
if (diff < 10 && diff > 1) {
ticks = diff
}
gl.xAxisScale = this.linearScale(
minX,
maxX,
ticks,
0,
w.config.xaxis.stepSize
)
}
return gl.xAxisScale
}
scaleMultipleYAxes() {
const cnf = this.w.config
const gl = this.w.globals
this.coreUtils.setSeriesYAxisMappings()
let axisSeriesMap = gl.seriesYAxisMap
let minYArr = gl.minYArr
let maxYArr = gl.maxYArr
// Compute min..max for each yaxis
gl.allSeriesCollapsed = true
gl.barGroups = []
axisSeriesMap.forEach((axisSeries, ai) => {
let groupNames = []
axisSeries.forEach((as) => {
let group = cnf.series[as]?.group
if (groupNames.indexOf(group) < 0) {
groupNames.push(group)
}
})
if (axisSeries.length > 0) {
let minY = Number.MAX_VALUE
let maxY = -Number.MAX_VALUE
let lowestY = minY
let highestY = maxY
let seriesType
let seriesGroupName
if (cnf.chart.stacked) {
// Series' on this axis with the same group name will be stacked.
// Sum series in each group separately
let mapSeries = new Array(gl.dataPoints).fill(0)
let sumSeries = []
let posSeries = []
let negSeries = []
groupNames.forEach(() => {
sumSeries.push(mapSeries.map(() => Number.MIN_VALUE))
posSeries.push(mapSeries.map(() => Number.MIN_VALUE))
negSeries.push(mapSeries.map(() => Number.MIN_VALUE))
})
for (let i = 0; i < axisSeries.length; i++) {
// Assume chart type but the first series that has a type overrides.
if (!seriesType && cnf.series[axisSeries[i]].type) {
seriesType = cnf.series[axisSeries[i]].type
}
// Sum all series for this yaxis at each corresponding datapoint
// For bar and column charts we need to keep positive and negative
// values separate, for each group separately.
let si = axisSeries[i]
if (cnf.series[si].group) {
seriesGroupName = cnf.series[si].group
} else {
seriesGroupName = 'axis-'.concat(ai)
}
let collapsed = !(
gl.collapsedSeriesIndices.indexOf(si) < 0 &&
gl.ancillaryCollapsedSeriesIndices.indexOf(si) < 0
)
if (!collapsed) {
gl.allSeriesCollapsed = false
groupNames.forEach((gn, gni) => {
// Undefined group names will be grouped together as their own
// group.
if (cnf.series[si].group === gn) {
for (let j = 0; j < gl.series[si].length; j++) {
let val = gl.series[si][j]
if (val >= 0) {
posSeries[gni][j] += val
} else {
negSeries[gni][j] += val
}
sumSeries[gni][j] += val
// For non bar-like series' we need these point max/min values.
lowestY = Math.min(lowestY, val)
highestY = Math.max(highestY, val)
}
}
})
}
if (seriesType === 'bar' || seriesType === 'column') {
gl.barGroups.push(seriesGroupName)
}
}
if (!seriesType) {
seriesType = cnf.chart.type
}
if (seriesType === 'bar' || seriesType === 'column') {
groupNames.forEach((gn, gni) => {
minY = Math.min(minY, Math.min.apply(null, negSeries[gni]))
maxY = Math.max(maxY, Math.max.apply(null, posSeries[gni]))
})
} else {
groupNames.forEach((gn, gni) => {
lowestY = Math.min(lowestY, Math.min.apply(null, sumSeries[gni]))
highestY = Math.max(
highestY,
Math.max.apply(null, sumSeries[gni])
)
})
minY = lowestY
maxY = highestY
}
if (minY === Number.MIN_VALUE && maxY === Number.MIN_VALUE) {
// No series data
maxY = -Number.MAX_VALUE
}
} else {
for (let i = 0; i < axisSeries.length; i++) {
let si = axisSeries[i]
minY = Math.min(minY, minYArr[si])
maxY = Math.max(maxY, maxYArr[si])
let collapsed = !(
gl.collapsedSeriesIndices.indexOf(si) < 0 &&
gl.ancillaryCollapsedSeriesIndices.indexOf(si) < 0
)
if (!collapsed) {
gl.allSeriesCollapsed = false
}
}
}
if (cnf.yaxis[ai].min !== undefined) {
if (typeof cnf.yaxis[ai].min === 'function') {
minY = cnf.yaxis[ai].min(minY)
} else {
minY = cnf.yaxis[ai].min
}
}
if (cnf.yaxis[ai].max !== undefined) {
if (typeof cnf.yaxis[ai].max === 'function') {
maxY = cnf.yaxis[ai].max(maxY)
} else {
maxY = cnf.yaxis[ai].max
}
}
gl.barGroups = gl.barGroups.filter((v, i, a) => a.indexOf(v) === i)
// Set the scale for this yaxis
this.setYScaleForIndex(ai, minY, maxY)
// Set individual series min and max to nice values
axisSeries.forEach((si) => {
minYArr[si] = gl.yAxisScale[ai].niceMin
maxYArr[si] = gl.yAxisScale[ai].niceMax
})
} else {
// No series referenced by this yaxis
this.setYScaleForIndex(ai, 0, -Number.MAX_VALUE)
}
})
}
}

484
frontend/node_modules/apexcharts/src/modules/Series.js generated vendored Normal file
View File

@@ -0,0 +1,484 @@
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 {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.legendInactiveClass = 'legend-mouseover-inactive'
}
getAllSeriesEls() {
return this.w.globals.dom.baseEl.getElementsByClassName(`apexcharts-series`)
}
getSeriesByName(seriesName) {
return this.w.globals.dom.baseEl.querySelector(
`.apexcharts-inner .apexcharts-series[seriesName='${Utils.escapeString(
seriesName
)}']`
)
}
isSeriesHidden(seriesName) {
const targetElement = this.getSeriesByName(seriesName)
let realIndex = parseInt(targetElement.getAttribute('data:realIndex'), 10)
let isHidden = targetElement.classList.contains(
'apexcharts-series-collapsed'
)
return { isHidden, realIndex }
}
addCollapsedClassToSeries(elSeries, index) {
const w = this.w
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)
}
toggleSeries(seriesName) {
let isSeriesHidden = this.isSeriesHidden(seriesName)
this.ctx.legend.legendHelpers.toggleDataSeries(
isSeriesHidden.realIndex,
isSeriesHidden.isHidden
)
return isSeriesHidden.isHidden
}
showSeries(seriesName) {
let isSeriesHidden = this.isSeriesHidden(seriesName)
if (isSeriesHidden.isHidden) {
this.ctx.legend.legendHelpers.toggleDataSeries(
isSeriesHidden.realIndex,
true
)
}
}
hideSeries(seriesName) {
let isSeriesHidden = this.isSeriesHidden(seriesName)
if (!isSeriesHidden.isHidden) {
this.ctx.legend.legendHelpers.toggleDataSeries(
isSeriesHidden.realIndex,
false
)
}
}
resetSeries(
shouldUpdateChart = true,
shouldResetZoom = true,
shouldResetCollapsed = true
) {
const w = this.w
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.globals.zoomed = false
this.ctx.updateHelpers.revertDefaultAxisMinMax()
}
this.ctx.updateHelpers._updateSeries(
series,
w.config.chart.animations.dynamicAnimation.enabled
)
}
}
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
}
highlightSeries(seriesName) {
const w = this.w
const targetElement = this.getSeriesByName(seriesName)
let realIndex = parseInt(targetElement?.getAttribute('data:realIndex'), 10)
let allSeriesEls = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-series, .apexcharts-datalabels, .apexcharts-yaxis`
)
let seriesEl = null
let dataLabelEl = null
let yaxisEl = null
if (w.globals.axisCharts || w.config.chart.type === 'radialBar') {
if (w.globals.axisCharts) {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${realIndex}']`
)
dataLabelEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-datalabels[data\\:realIndex='${realIndex}']`
)
let yaxisIndex = w.globals.seriesYAxisReverseMap[realIndex]
yaxisEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-yaxis[rel='${yaxisIndex}']`
)
} else {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[rel='${realIndex + 1}']`
)
}
} else {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[rel='${realIndex + 1}'] path`
)
}
for (let se = 0; se < allSeriesEls.length; se++) {
allSeriesEls[se].classList.add(this.legendInactiveClass)
}
if (seriesEl) {
if (!w.globals.axisCharts) {
seriesEl.parentNode.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++) {
allSeriesEls[se].classList.remove(this.legendInactiveClass)
}
}
}
toggleSeriesOnHover(e, targetElement) {
const w = this.w
if (!targetElement) targetElement = e.target
let allSeriesEls = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-series, .apexcharts-datalabels, .apexcharts-yaxis`
)
if (e.type === 'mousemove') {
let realIndex = parseInt(targetElement.getAttribute('rel'), 10) - 1
this.highlightSeries(w.globals.seriesNames[realIndex])
} else if (e.type === 'mouseout') {
for (let se = 0; se < allSeriesEls.length; se++) {
allSeriesEls[se].classList.remove(this.legendInactiveClass)
}
}
}
highlightRangeInSeries(e, targetElement) {
const w = this.w
const allHeatMapElements = w.globals.dom.baseEl.getElementsByClassName(
'apexcharts-heatmap-rect'
)
const activeInactive = (action) => {
for (let i = 0; i < allHeatMapElements.length; i++) {
allHeatMapElements[i].classList[action](this.legendInactiveClass)
}
}
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') {
let seriesCnt = parseInt(targetElement.getAttribute('rel'), 10) - 1
activeInactive('add')
const ranges = w.config.plotOptions.heatmap.colorScale.ranges
const range = ranges[seriesCnt]
const rangeMax = ranges.reduce((acc, cur) => Math.max(acc, cur.to), 0)
removeInactiveClassFromHoveredRange(range, rangeMax)
} else if (e.type === 'mouseout') {
activeInactive('remove')
}
}
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
let activeSeriesIndex = w.config.series.map((s, index) => {
const checkChartType = () => {
if (w.globals.comboCharts) {
return (
chartTypes.length === 0 ||
(chartTypes.length &&
chartTypes.indexOf(w.config.series[index].type) > -1)
)
}
return true
}
const hasData =
s.data &&
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((s, i) => {
return s.type === 'bar' || s.type === 'column' ? i : -1
})
.filter((i) => {
return i !== -1
})
}
return this.w.config.series.map((s, i) => {
return i
})
}
getPreviousPaths() {
let w = this.w
w.globals.previousPaths = []
function pushPaths(seriesEls, i, type) {
let paths = seriesEls[i].childNodes
let dArr = {
type,
paths: [],
realIndex: seriesEls[i].getAttribute('data:realIndex'),
}
for (let j = 0; j < paths.length; j++) {
if (paths[j].hasAttribute('pathTo')) {
let d = paths[j].getAttribute('pathTo')
dArr.paths.push({
d,
})
}
}
w.globals.previousPaths.push(dArr)
}
const getPaths = (chartType) => {
return w.globals.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)
}
})
let heatTreeSeries = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-${w.config.chart.type} .apexcharts-series`
)
if (heatTreeSeries.length > 0) {
for (let h = 0; h < heatTreeSeries.length; h++) {
let seriesEls = w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-${w.config.chart.type} .apexcharts-series[data\\:realIndex='${h}'] rect`
)
let dArr = []
for (let i = 0; i < seriesEls.length; i++) {
const getAttr = (x) => {
return seriesEls[i].getAttribute(x)
}
const rect = {
x: parseFloat(getAttr('x')),
y: parseFloat(getAttr('y')),
width: parseFloat(getAttr('width')),
height: parseFloat(getAttr('height')),
}
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.globals.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.ctx)
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 !== '') {
let titleText = graphics.drawText({
x,
y,
text: noDataOpts.text,
textAnchor,
fontSize: noDataOpts.style.fontSize,
fontFamily: noDataOpts.style.fontFamily,
foreColor: noDataOpts.style.color,
opacity: 1,
class: 'apexcharts-text-nodata',
})
w.globals.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
setNullSeriesToZeroValues(series) {
let 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
const filteredSeriesX = w.globals.seriesX.map((ser) =>
ser.length > 0 ? ser : []
)
return filteredSeriesX
}
}

240
frontend/node_modules/apexcharts/src/modules/Theme.js generated vendored Normal file
View File

@@ -0,0 +1,240 @@
import Utils from '../utils/Utils'
/**
* ApexCharts Theme Class for setting the colors and palettes.
*
* @module Theme
**/
export default class Theme {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.colors = []
this.isColorFn = false
this.isHeatmapDistributed = this.checkHeatmapDistributed()
this.isBarDistributed = this.checkBarDistributed()
}
checkHeatmapDistributed() {
const { chart, plotOptions } = this.w.config
return (
(chart.type === 'treemap' &&
plotOptions.treemap &&
plotOptions.treemap.distributed) ||
(chart.type === 'heatmap' &&
plotOptions.heatmap &&
plotOptions.heatmap.distributed)
)
}
checkBarDistributed() {
const { chart, plotOptions } = this.w.config
return (
plotOptions.bar &&
plotOptions.bar.distributed &&
(chart.type === 'bar' || chart.type === 'rangeBar')
)
}
init() {
this.setDefaultColors()
}
setDefaultColors() {
const w = this.w
const utils = new Utils()
w.globals.dom.elWrap.classList.add(
`apexcharts-theme-${w.config.theme.mode}`
)
// Create a copy of config.colors array to avoid mutating the original config.colors
const configColors = [...(w.config.colors || w.config.fill.colors || [])]
w.globals.colors = this.getColors(configColors)
this.applySeriesColors(w.globals.seriesColors, w.globals.colors)
if (w.config.theme.monochrome.enabled) {
w.globals.colors = this.getMonochromeColors(
w.config.theme.monochrome,
w.globals.series,
utils
)
}
const defaultColors = w.globals.colors.slice()
this.pushExtraColors(w.globals.colors)
this.applyColorTypes(['fill', 'stroke'], defaultColors)
this.applyDataLabelsColors(defaultColors)
this.applyRadarPolygonsColors()
this.applyMarkersColors(defaultColors)
}
getColors(configColors) {
const w = this.w
if (!configColors || configColors.length === 0) {
return this.predefined()
}
if (
Array.isArray(configColors) &&
configColors.length > 0 &&
typeof configColors[0] === 'function'
) {
this.isColorFn = true
return w.config.series.map((s, i) => {
let c = configColors[i] || configColors[0]
return typeof c === 'function'
? c({
value: w.globals.axisCharts
? w.globals.series[i][0] || 0
: w.globals.series[i],
seriesIndex: i,
dataPointIndex: i,
w: this.w,
})
: c
})
}
return configColors
}
applySeriesColors(seriesColors, globalsColors) {
seriesColors.forEach((c, i) => {
if (c) {
globalsColors[i] = c
}
})
}
getMonochromeColors(monochrome, series, utils) {
const { color, shadeIntensity, shadeTo } = monochrome
const glsCnt =
this.isBarDistributed || this.isHeatmapDistributed
? series[0].length * series.length
: series.length
const part = 1 / (glsCnt / shadeIntensity)
let percent = 0
return Array.from({ length: glsCnt }, () => {
const newColor =
shadeTo === 'dark'
? utils.shadeColor(percent * -1, color)
: utils.shadeColor(percent, color)
percent += part
return newColor
})
}
applyColorTypes(colorTypes, defaultColors) {
const w = this.w
colorTypes.forEach((c) => {
w.globals[c].colors =
w.config[c].colors === undefined
? this.isColorFn
? w.config.colors
: defaultColors
: w.config[c].colors.slice()
this.pushExtraColors(w.globals[c].colors)
})
}
applyDataLabelsColors(defaultColors) {
const w = this.w
w.globals.dataLabels.style.colors =
w.config.dataLabels.style.colors === undefined
? defaultColors
: w.config.dataLabels.style.colors.slice()
this.pushExtraColors(w.globals.dataLabels.style.colors, 50)
}
applyRadarPolygonsColors() {
const w = this.w
w.globals.radarPolygons.fill.colors =
w.config.plotOptions.radar.polygons.fill.colors === undefined
? [w.config.theme.mode === 'dark' ? '#424242' : 'none']
: w.config.plotOptions.radar.polygons.fill.colors.slice()
this.pushExtraColors(w.globals.radarPolygons.fill.colors, 20)
}
applyMarkersColors(defaultColors) {
const w = this.w
w.globals.markers.colors =
w.config.markers.colors === undefined
? defaultColors
: w.config.markers.colors.slice()
this.pushExtraColors(w.globals.markers.colors)
}
pushExtraColors(colorSeries, length, distributed = null) {
const w = this.w
let len = length || w.globals.series.length
if (distributed === null) {
distributed =
this.isBarDistributed ||
this.isHeatmapDistributed ||
(w.config.chart.type === 'heatmap' &&
w.config.plotOptions.heatmap &&
w.config.plotOptions.heatmap.colorScale.inverse)
}
if (distributed && w.globals.series.length) {
len =
w.globals.series[w.globals.maxValsInArrayIndex].length *
w.globals.series.length
}
if (colorSeries.length < len) {
let diff = len - colorSeries.length
for (let i = 0; i < diff; i++) {
colorSeries.push(colorSeries[i])
}
}
}
updateThemeOptions(options) {
options.chart = options.chart || {}
options.tooltip = options.tooltip || {}
const mode = options.theme.mode
const palette =
mode === 'dark'
? 'palette4'
: mode === 'light'
? 'palette1'
: options.theme.palette || 'palette1'
const foreColor =
mode === 'dark'
? '#f6f7f8'
: mode === 'light'
? '#373d3f'
: options.chart.foreColor || '#373d3f'
options.tooltip.theme = mode || 'light'
options.chart.foreColor = foreColor
options.theme.palette = palette
return options
}
predefined() {
const palette = this.w.config.theme.palette
const palettes = {
palette1: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0'],
palette2: ['#3f51b5', '#03a9f4', '#4caf50', '#f9ce1d', '#FF9800'],
palette3: ['#33b2df', '#546E7A', '#d4526e', '#13d8aa', '#A5978B'],
palette4: ['#4ecdc4', '#c7f464', '#81D4FA', '#fd6a6a', '#546E7A'],
palette5: ['#2b908f', '#f9a3a4', '#90ee7e', '#fa4443', '#69d2e7'],
palette6: ['#449DD1', '#F86624', '#EA3546', '#662E9B', '#C5D86D'],
palette7: ['#D7263D', '#1B998B', '#2E294E', '#F46036', '#E2C044'],
palette8: ['#662E9B', '#F86624', '#F9C80E', '#EA3546', '#43BCCD'],
palette9: ['#5C4742', '#A5978B', '#8D5B4C', '#5A2A27', '#C4BBAF'],
palette10: ['#A300D6', '#7D02EB', '#5653FE', '#2983FF', '#00B1F2'],
default: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0'],
}
return palettes[palette] || palettes.default
}
}

View File

@@ -0,0 +1,939 @@
import DateTime from '../utils/DateTime'
import Dimensions from './dimensions/Dimensions'
import Graphics from './Graphics'
import Utils from '../utils/Utils'
const MINUTES_IN_DAY = 24 * 60
const SECONDS_IN_DAY = MINUTES_IN_DAY * 60
const MIN_ZOOM_DAYS = 10 / SECONDS_IN_DAY
/**
* ApexCharts TimeScale Class for generating time ticks for x-axis.
*
* @module TimeScale
**/
class TimeScale {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.timeScaleArray = []
this.utc = this.w.config.xaxis.labels.datetimeUTC
}
calculateTimeScaleTicks(minX, maxX) {
let w = this.w
// null check when no series to show
if (w.globals.allSeriesCollapsed) {
w.globals.labels = []
w.globals.timescaleLabels = []
return []
}
let dt = new DateTime(this.ctx)
const daysDiff = (maxX - minX) / (1000 * SECONDS_IN_DAY)
this.determineInterval(daysDiff)
w.globals.disableZoomIn = false
w.globals.disableZoomOut = false
if (daysDiff < MIN_ZOOM_DAYS) {
w.globals.disableZoomIn = true
} else if (daysDiff > 50000) {
w.globals.disableZoomOut = true
}
const timeIntervals = dt.getTimeUnitsfromTimestamp(minX, maxX, this.utc)
const daysWidthOnXAxis = w.globals.gridWidth / daysDiff
const hoursWidthOnXAxis = daysWidthOnXAxis / 24
const minutesWidthOnXAxis = hoursWidthOnXAxis / 60
const secondsWidthOnXAxis = minutesWidthOnXAxis / 60
let numberOfHours = Math.floor(daysDiff * 24)
let numberOfMinutes = Math.floor(daysDiff * MINUTES_IN_DAY)
let numberOfSeconds = Math.floor(daysDiff * SECONDS_IN_DAY)
let numberOfDays = Math.floor(daysDiff)
let numberOfMonths = Math.floor(daysDiff / 30)
let numberOfYears = Math.floor(daysDiff / 365)
const firstVal = {
minMillisecond: timeIntervals.minMillisecond,
minSecond: timeIntervals.minSecond,
minMinute: timeIntervals.minMinute,
minHour: timeIntervals.minHour,
minDate: timeIntervals.minDate,
minMonth: timeIntervals.minMonth,
minYear: timeIntervals.minYear,
}
let currentMillisecond = firstVal.minMillisecond
let currentSecond = firstVal.minSecond
let currentMinute = firstVal.minMinute
let currentHour = firstVal.minHour
let currentMonthDate = firstVal.minDate
let currentDate = firstVal.minDate
let currentMonth = firstVal.minMonth
let currentYear = firstVal.minYear
const params = {
firstVal,
currentMillisecond,
currentSecond,
currentMinute,
currentHour,
currentMonthDate,
currentDate,
currentMonth,
currentYear,
daysWidthOnXAxis,
hoursWidthOnXAxis,
minutesWidthOnXAxis,
secondsWidthOnXAxis,
numberOfSeconds,
numberOfMinutes,
numberOfHours,
numberOfDays,
numberOfMonths,
numberOfYears,
}
switch (this.tickInterval) {
case 'years': {
this.generateYearScale(params)
break
}
case 'months':
case 'half_year': {
this.generateMonthScale(params)
break
}
case 'months_days':
case 'months_fortnight':
case 'days':
case 'week_days': {
this.generateDayScale(params)
break
}
case 'hours': {
this.generateHourScale(params)
break
}
case 'minutes_fives':
case 'minutes':
this.generateMinuteScale(params)
break
case 'seconds_tens':
case 'seconds_fives':
case 'seconds':
this.generateSecondScale(params)
break
}
// first, we will adjust the month values index
// as in the upper function, it is starting from 0
// we will start them from 1
const adjustedMonthInTimeScaleArray = this.timeScaleArray.map((ts) => {
let defaultReturn = {
position: ts.position,
unit: ts.unit,
year: ts.year,
day: ts.day ? ts.day : 1,
hour: ts.hour ? ts.hour : 0,
month: ts.month + 1,
}
if (ts.unit === 'month') {
return {
...defaultReturn,
day: 1,
value: ts.value + 1,
}
} else if (ts.unit === 'day' || ts.unit === 'hour') {
return {
...defaultReturn,
value: ts.value,
}
} else if (ts.unit === 'minute') {
return {
...defaultReturn,
value: ts.value,
minute: ts.value,
}
} else if (ts.unit === 'second') {
return {
...defaultReturn,
value: ts.value,
minute: ts.minute,
second: ts.second,
}
}
return ts
})
const filteredTimeScale = adjustedMonthInTimeScaleArray.filter((ts) => {
let modulo = 1
let ticks = Math.ceil(w.globals.gridWidth / 120)
let value = ts.value
if (w.config.xaxis.tickAmount !== undefined) {
ticks = w.config.xaxis.tickAmount
}
if (adjustedMonthInTimeScaleArray.length > ticks) {
modulo = Math.floor(adjustedMonthInTimeScaleArray.length / ticks)
}
let shouldNotSkipUnit = false // there is a big change in unit i.e days to months
let shouldNotPrint = false // should skip these values
switch (this.tickInterval) {
case 'years':
// make years label denser
if (ts.unit === 'year') {
shouldNotSkipUnit = true
}
break
case 'half_year':
modulo = 7
if (ts.unit === 'year') {
shouldNotSkipUnit = true
}
break
case 'months':
modulo = 1
if (ts.unit === 'year') {
shouldNotSkipUnit = true
}
break
case 'months_fortnight':
modulo = 15
if (ts.unit === 'year' || ts.unit === 'month') {
shouldNotSkipUnit = true
}
if (value === 30) {
shouldNotPrint = true
}
break
case 'months_days':
modulo = 10
if (ts.unit === 'month') {
shouldNotSkipUnit = true
}
if (value === 30) {
shouldNotPrint = true
}
break
case 'week_days':
modulo = 8
if (ts.unit === 'month') {
shouldNotSkipUnit = true
}
break
case 'days':
modulo = 1
if (ts.unit === 'month') {
shouldNotSkipUnit = true
}
break
case 'hours':
if (ts.unit === 'day') {
shouldNotSkipUnit = true
}
break
case 'minutes_fives':
if (value % 5 !== 0) {
shouldNotPrint = true
}
break
case 'seconds_tens':
if (value % 10 !== 0) {
shouldNotPrint = true
}
break
case 'seconds_fives':
if (value % 5 !== 0) {
shouldNotPrint = true
}
break
}
if (
this.tickInterval === 'hours' ||
this.tickInterval === 'minutes_fives' ||
this.tickInterval === 'seconds_tens' ||
this.tickInterval === 'seconds_fives'
) {
if (!shouldNotPrint) {
return true
}
} else {
if ((value % modulo === 0 || shouldNotSkipUnit) && !shouldNotPrint) {
return true
}
}
})
return filteredTimeScale
}
recalcDimensionsBasedOnFormat(filteredTimeScale, inverted) {
const w = this.w
const reformattedTimescaleArray = this.formatDates(filteredTimeScale)
const removedOverlappingTS = this.removeOverlappingTS(
reformattedTimescaleArray
)
w.globals.timescaleLabels = removedOverlappingTS.slice()
// at this stage, we need to re-calculate coords of the grid as timeline labels may have altered the xaxis labels coords
// The reason we can't do this prior to this stage is because timeline labels depends on gridWidth, and as the ticks are calculated based on available gridWidth, there can be unknown number of ticks generated for different minX and maxX
// Dependency on Dimensions(), need to refactor correctly
// TODO - find an alternate way to avoid calling this Heavy method twice
let dimensions = new Dimensions(this.ctx)
dimensions.plotCoords()
}
determineInterval(daysDiff) {
const yearsDiff = daysDiff / 365
const hoursDiff = daysDiff * 24
const minutesDiff = hoursDiff * 60
const secondsDiff = minutesDiff * 60
switch (true) {
case yearsDiff > 5:
this.tickInterval = 'years'
break
case daysDiff > 800:
this.tickInterval = 'half_year'
break
case daysDiff > 180:
this.tickInterval = 'months'
break
case daysDiff > 90:
this.tickInterval = 'months_fortnight'
break
case daysDiff > 60:
this.tickInterval = 'months_days'
break
case daysDiff > 30:
this.tickInterval = 'week_days'
break
case daysDiff > 2:
this.tickInterval = 'days'
break
case hoursDiff > 2.4:
this.tickInterval = 'hours'
break
case minutesDiff > 15:
this.tickInterval = 'minutes_fives'
break
case minutesDiff > 5:
this.tickInterval = 'minutes'
break
case minutesDiff > 1:
this.tickInterval = 'seconds_tens'
break
case secondsDiff > 20:
this.tickInterval = 'seconds_fives'
break
default:
this.tickInterval = 'seconds'
break
}
}
generateYearScale({
firstVal,
currentMonth,
currentYear,
daysWidthOnXAxis,
numberOfYears,
}) {
let firstTickValue = firstVal.minYear
let firstTickPosition = 0
const dt = new DateTime(this.ctx)
let unit = 'year'
if (firstVal.minDate > 1 || firstVal.minMonth > 0) {
let remainingDays = dt.determineRemainingDaysOfYear(
firstVal.minYear,
firstVal.minMonth,
firstVal.minDate
)
// remainingDaysofFirstMonth is used to reacht the 2nd tick position
let remainingDaysOfFirstYear =
dt.determineDaysOfYear(firstVal.minYear) - remainingDays + 1
// calculate the first tick position
firstTickPosition = remainingDaysOfFirstYear * daysWidthOnXAxis
firstTickValue = firstVal.minYear + 1
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
year: firstTickValue,
month: Utils.monthMod(currentMonth + 1),
})
} else if (firstVal.minDate === 1 && firstVal.minMonth === 0) {
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
year: currentYear,
month: Utils.monthMod(currentMonth + 1),
})
}
let year = firstTickValue
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0; i < numberOfYears; i++) {
year++
pos = dt.determineDaysOfYear(year - 1) * daysWidthOnXAxis + pos
this.timeScaleArray.push({
position: pos,
value: year,
unit,
year,
month: 1,
})
}
}
generateMonthScale({
firstVal,
currentMonthDate,
currentMonth,
currentYear,
daysWidthOnXAxis,
numberOfMonths,
}) {
let firstTickValue = currentMonth
let firstTickPosition = 0
const dt = new DateTime(this.ctx)
let unit = 'month'
let yrCounter = 0
if (firstVal.minDate > 1) {
// remainingDaysofFirstMonth is used to reacht the 2nd tick position
let remainingDaysOfFirstMonth =
dt.determineDaysOfMonths(currentMonth + 1, firstVal.minYear) -
currentMonthDate +
1
// calculate the first tick position
firstTickPosition = remainingDaysOfFirstMonth * daysWidthOnXAxis
firstTickValue = Utils.monthMod(currentMonth + 1)
let year = currentYear + yrCounter
let month = Utils.monthMod(firstTickValue)
let value = firstTickValue
// it's Jan, so update the year
if (firstTickValue === 0) {
unit = 'year'
value = year
month = 1
yrCounter += 1
year = year + yrCounter
}
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value,
unit,
year,
month,
})
} else {
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
year: currentYear,
month: Utils.monthMod(currentMonth),
})
}
let month = firstTickValue + 1
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0, j = 1; i < numberOfMonths; i++, j++) {
month = Utils.monthMod(month)
if (month === 0) {
unit = 'year'
yrCounter += 1
} else {
unit = 'month'
}
let year = this._getYear(currentYear, month, yrCounter)
pos = dt.determineDaysOfMonths(month, year) * daysWidthOnXAxis + pos
let monthVal = month === 0 ? year : month
this.timeScaleArray.push({
position: pos,
value: monthVal,
unit,
year,
month: month === 0 ? 1 : month,
})
month++
}
}
generateDayScale({
firstVal,
currentMonth,
currentYear,
hoursWidthOnXAxis,
numberOfDays,
}) {
const dt = new DateTime(this.ctx)
let unit = 'day'
let firstTickValue = firstVal.minDate + 1
let date = firstTickValue
const changeMonth = (dateVal, month, year) => {
let monthdays = dt.determineDaysOfMonths(month + 1, year)
if (dateVal > monthdays) {
month = month + 1
date = 1
unit = 'month'
val = month
return month
}
return month
}
let remainingHours = 24 - firstVal.minHour
let yrCounter = 0
// calculate the first tick position
let firstTickPosition = remainingHours * hoursWidthOnXAxis
let val = firstTickValue
let month = changeMonth(date, currentMonth, currentYear)
if (firstVal.minHour === 0 && firstVal.minDate === 1) {
// the first value is the first day of month
firstTickPosition = 0
val = Utils.monthMod(firstVal.minMonth)
unit = 'month'
date = firstVal.minDate
// numberOfDays++
// removed the above line to fix https://github.com/apexcharts/apexcharts.js/issues/305#issuecomment-1019520513
} else if (
firstVal.minDate !== 1 &&
firstVal.minHour === 0 &&
firstVal.minMinute === 0
) {
// fixes apexcharts/apexcharts.js/issues/1730
firstTickPosition = 0
firstTickValue = firstVal.minDate
date = firstTickValue
val = firstTickValue
// in case it's the last date of month, we need to check it
month = changeMonth(date, currentMonth, currentYear)
if (val !== 1) {
unit = 'day'
}
}
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: val,
unit,
year: this._getYear(currentYear, month, yrCounter),
month: Utils.monthMod(month),
day: date,
})
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0; i < numberOfDays; i++) {
date += 1
unit = 'day'
month = changeMonth(
date,
month,
this._getYear(currentYear, month, yrCounter)
)
let year = this._getYear(currentYear, month, yrCounter)
pos = 24 * hoursWidthOnXAxis + pos
let value = date === 1 ? Utils.monthMod(month) : date
this.timeScaleArray.push({
position: pos,
value,
unit,
year,
month: Utils.monthMod(month),
day: value,
})
}
}
generateHourScale({
firstVal,
currentDate,
currentMonth,
currentYear,
minutesWidthOnXAxis,
numberOfHours,
}) {
const dt = new DateTime(this.ctx)
let yrCounter = 0
let unit = 'hour'
const changeDate = (dateVal, month) => {
let monthdays = dt.determineDaysOfMonths(month + 1, currentYear)
if (dateVal > monthdays) {
date = 1
month = month + 1
}
return { month, date }
}
const changeMonth = (dateVal, month) => {
let monthdays = dt.determineDaysOfMonths(month + 1, currentYear)
if (dateVal > monthdays) {
month = month + 1
return month
}
return month
}
// factor in minSeconds as well
let remainingMins = 60 - (firstVal.minMinute + firstVal.minSecond / 60.0)
let firstTickPosition = remainingMins * minutesWidthOnXAxis
let firstTickValue = firstVal.minHour + 1
let hour = firstTickValue
if (remainingMins === 60) {
firstTickPosition = 0
firstTickValue = firstVal.minHour
hour = firstTickValue
}
let date = currentDate
// we need to apply date switching logic here as well, to avoid duplicated labels
if (hour >= 24) {
hour = 0
date += 1
unit = 'day'
// Unit changed to day , Value should align unit
firstTickValue = date
}
const checkNextMonth = changeDate(date, currentMonth)
let month = checkNextMonth.month
month = changeMonth(date, month)
// Check if date is greater than 31 and change month if it is
if (firstTickValue > 31) {
date = 1
firstTickValue = date
}
// push the first tick in the array
this.timeScaleArray.push({
position: firstTickPosition,
value: firstTickValue,
unit,
day: date,
hour,
year: currentYear,
month: Utils.monthMod(month),
})
hour++
let pos = firstTickPosition
// keep drawing rest of the ticks
for (let i = 0; i < numberOfHours; i++) {
unit = 'hour'
if (hour >= 24) {
hour = 0
date += 1
unit = 'day'
const checkNextMonth = changeDate(date, month)
month = checkNextMonth.month
month = changeMonth(date, month)
}
let year = this._getYear(currentYear, month, yrCounter)
pos = 60 * minutesWidthOnXAxis + pos
let val = hour === 0 ? date : hour
this.timeScaleArray.push({
position: pos,
value: val,
unit,
hour,
day: date,
year,
month: Utils.monthMod(month),
})
hour++
}
}
generateMinuteScale({
currentMillisecond,
currentSecond,
currentMinute,
currentHour,
currentDate,
currentMonth,
currentYear,
minutesWidthOnXAxis,
secondsWidthOnXAxis,
numberOfMinutes,
}) {
let yrCounter = 0
let unit = 'minute'
let remainingSecs = 60 - currentSecond
let firstTickPosition =
(remainingSecs - currentMillisecond / 1000) * secondsWidthOnXAxis
let minute = currentMinute + 1
let date = currentDate
let month = currentMonth
let year = currentYear
let hour = currentHour
let pos = firstTickPosition
for (let i = 0; i < numberOfMinutes; i++) {
if (minute >= 60) {
minute = 0
hour += 1
if (hour === 24) {
hour = 0
}
}
this.timeScaleArray.push({
position: pos,
value: minute,
unit,
hour,
minute,
day: date,
year: this._getYear(year, month, yrCounter),
month: Utils.monthMod(month),
})
pos += minutesWidthOnXAxis
minute++
}
}
generateSecondScale({
currentMillisecond,
currentSecond,
currentMinute,
currentHour,
currentDate,
currentMonth,
currentYear,
secondsWidthOnXAxis,
numberOfSeconds,
}) {
let yrCounter = 0
let unit = 'second'
const remainingMillisecs = 1000 - currentMillisecond
let firstTickPosition = (remainingMillisecs / 1000) * secondsWidthOnXAxis
let second = currentSecond + 1
let minute = currentMinute
let date = currentDate
let month = currentMonth
let year = currentYear
let hour = currentHour
let pos = firstTickPosition
for (let i = 0; i < numberOfSeconds; i++) {
if (second >= 60) {
minute++
second = 0
if (minute >= 60) {
hour++
minute = 0
if (hour === 24) {
hour = 0
}
}
}
this.timeScaleArray.push({
position: pos,
value: second,
unit,
hour,
minute,
second,
day: date,
year: this._getYear(year, month, yrCounter),
month: Utils.monthMod(month),
})
pos += secondsWidthOnXAxis
second++
}
}
createRawDateString(ts, value) {
let raw = ts.year
if (ts.month === 0) {
// invalid month, correct it
ts.month = 1
}
raw += '-' + ('0' + ts.month.toString()).slice(-2)
// unit is day
if (ts.unit === 'day') {
raw += ts.unit === 'day' ? '-' + ('0' + value).slice(-2) : '-01'
} else {
raw += '-' + ('0' + (ts.day ? ts.day : '1')).slice(-2)
}
// unit is hour
if (ts.unit === 'hour') {
raw += ts.unit === 'hour' ? 'T' + ('0' + value).slice(-2) : 'T00'
} else {
raw += 'T' + ('0' + (ts.hour ? ts.hour : '0')).slice(-2)
}
if (ts.unit === 'minute') {
raw += ':' + ('0' + value).slice(-2)
} else {
raw += ':' + (ts.minute ? ('0' + ts.minute).slice(-2) : '00')
}
if (ts.unit === 'second') {
raw += ':' + ('0' + value).slice(-2)
} else {
raw += ':00'
}
if (this.utc) {
raw += '.000Z'
}
return raw
}
formatDates(filteredTimeScale) {
const w = this.w
const reformattedTimescaleArray = filteredTimeScale.map((ts) => {
let value = ts.value.toString()
let dt = new DateTime(this.ctx)
const raw = this.createRawDateString(ts, value)
let dateToFormat = dt.getDate(dt.parseDate(raw))
if (!this.utc) {
// Fixes #1726, #1544, #1485, #1255
dateToFormat = dt.getDate(dt.parseDateWithTimezone(raw))
}
if (w.config.xaxis.labels.format === undefined) {
let customFormat = 'dd MMM'
const dtFormatter = w.config.xaxis.labels.datetimeFormatter
if (ts.unit === 'year') customFormat = dtFormatter.year
if (ts.unit === 'month') customFormat = dtFormatter.month
if (ts.unit === 'day') customFormat = dtFormatter.day
if (ts.unit === 'hour') customFormat = dtFormatter.hour
if (ts.unit === 'minute') customFormat = dtFormatter.minute
if (ts.unit === 'second') customFormat = dtFormatter.second
value = dt.formatDate(dateToFormat, customFormat)
} else {
value = dt.formatDate(dateToFormat, w.config.xaxis.labels.format)
}
return {
dateString: raw,
position: ts.position,
value,
unit: ts.unit,
year: ts.year,
month: ts.month,
}
})
return reformattedTimescaleArray
}
removeOverlappingTS(arr) {
const graphics = new Graphics(this.ctx)
let equalLabelLengthFlag = false // These labels got same length?
let constantLabelWidth // If true, what is the constant length to use
if (
arr.length > 0 && // check arr length
arr[0].value && // check arr[0] contains value
arr.every((lb) => lb.value.length === arr[0].value.length) // check every arr label value is the same as the first one
) {
equalLabelLengthFlag = true // These labels got same length
constantLabelWidth = graphics.getTextRects(arr[0].value).width // The constant label width to use
}
let lastDrawnIndex = 0
let filteredArray = arr.map((item, index) => {
if (index > 0 && this.w.config.xaxis.labels.hideOverlappingLabels) {
const prevLabelWidth = !equalLabelLengthFlag // if vary in label length
? graphics.getTextRects(arr[lastDrawnIndex].value).width // get individual length
: constantLabelWidth // else: use constant length
const prevPos = arr[lastDrawnIndex].position
const pos = item.position
if (pos > prevPos + prevLabelWidth + 10) {
lastDrawnIndex = index
return item
} else {
return null
}
} else {
return item
}
})
filteredArray = filteredArray.filter((f) => f !== null)
return filteredArray
}
_getYear(currentYear, month, yrCounter) {
return currentYear + Math.floor(month / 12) + yrCounter
}
}
export default TimeScale

View File

@@ -0,0 +1,52 @@
import Graphics from './Graphics'
export default class TitleSubtitle {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
draw() {
this.drawTitleSubtitle('title')
this.drawTitleSubtitle('subtitle')
}
drawTitleSubtitle(type) {
let w = this.w
const tsConfig = type === 'title' ? w.config.title : w.config.subtitle
let x = w.globals.svgWidth / 2
let y = tsConfig.offsetY
let textAnchor = 'middle'
if (tsConfig.align === 'left') {
x = 10
textAnchor = 'start'
} else if (tsConfig.align === 'right') {
x = w.globals.svgWidth - 10
textAnchor = 'end'
}
x = x + tsConfig.offsetX
y = y + parseInt(tsConfig.style.fontSize, 10) + tsConfig.margin / 2
if (tsConfig.text !== undefined) {
let graphics = new Graphics(this.ctx)
let titleText = graphics.drawText({
x,
y,
text: tsConfig.text,
textAnchor,
fontSize: tsConfig.style.fontSize,
fontFamily: tsConfig.style.fontFamily,
fontWeight: tsConfig.style.fontWeight,
foreColor: tsConfig.style.color,
opacity: 1
})
titleText.node.setAttribute('class', `apexcharts-${type}-text`)
w.globals.dom.Paper.add(titleText)
}
}
}

521
frontend/node_modules/apexcharts/src/modules/Toolbar.js generated vendored Normal file
View File

@@ -0,0 +1,521 @@
import Graphics from './Graphics'
import Exports from './Exports'
import Scales from './Scales'
import Utils from './../utils/Utils'
import icoPan from './../assets/ico-pan-hand.svg'
import icoZoom from './../assets/ico-zoom-in.svg'
import icoReset from './../assets/ico-home.svg'
import icoZoomIn from './../assets/ico-plus.svg'
import icoZoomOut from './../assets/ico-minus.svg'
import icoSelect from './../assets/ico-select.svg'
import icoMenu from './../assets/ico-menu.svg'
/**
* ApexCharts Toolbar Class for creating toolbar in axis based charts.
*
* @module Toolbar
**/
export default class Toolbar {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
const w = this.w
this.ev = this.w.config.chart.events
this.selectedClass = 'apexcharts-selected'
this.localeValues = this.w.globals.locale.toolbar
this.minX = w.globals.minX
this.maxX = w.globals.maxX
}
createToolbar() {
let w = this.w
const createDiv = () => {
return document.createElement('div')
}
const elToolbarWrap = createDiv()
elToolbarWrap.setAttribute('class', 'apexcharts-toolbar')
elToolbarWrap.style.top = w.config.chart.toolbar.offsetY + 'px'
elToolbarWrap.style.right = -w.config.chart.toolbar.offsetX + 3 + 'px'
w.globals.dom.elWrap.appendChild(elToolbarWrap)
this.elZoom = createDiv()
this.elZoomIn = createDiv()
this.elZoomOut = createDiv()
this.elPan = createDiv()
this.elSelection = createDiv()
this.elZoomReset = createDiv()
this.elMenuIcon = createDiv()
this.elMenu = createDiv()
this.elCustomIcons = []
this.t = w.config.chart.toolbar.tools
if (Array.isArray(this.t.customIcons)) {
for (let i = 0; i < this.t.customIcons.length; i++) {
this.elCustomIcons.push(createDiv())
}
}
let toolbarControls = []
const appendZoomControl = (type, el, ico) => {
const tool = type.toLowerCase()
if (this.t[tool] && w.config.chart.zoom.enabled) {
toolbarControls.push({
el,
icon: typeof this.t[tool] === 'string' ? this.t[tool] : ico,
title: this.localeValues[type],
class: `apexcharts-${tool}-icon`,
})
}
}
appendZoomControl('zoomIn', this.elZoomIn, icoZoomIn)
appendZoomControl('zoomOut', this.elZoomOut, icoZoomOut)
const zoomSelectionCtrls = (z) => {
if (this.t[z] && w.config.chart[z].enabled) {
toolbarControls.push({
el: z === 'zoom' ? this.elZoom : this.elSelection,
icon:
typeof this.t[z] === 'string'
? this.t[z]
: z === 'zoom'
? icoZoom
: icoSelect,
title:
this.localeValues[z === 'zoom' ? 'selectionZoom' : 'selection'],
class: w.globals.isTouchDevice
? 'apexcharts-element-hidden'
: `apexcharts-${z}-icon`,
})
}
}
zoomSelectionCtrls('zoom')
zoomSelectionCtrls('selection')
if (this.t.pan && w.config.chart.zoom.enabled) {
toolbarControls.push({
el: this.elPan,
icon: typeof this.t.pan === 'string' ? this.t.pan : icoPan,
title: this.localeValues.pan,
class: w.globals.isTouchDevice
? 'apexcharts-element-hidden'
: 'apexcharts-pan-icon',
})
}
appendZoomControl('reset', this.elZoomReset, icoReset)
if (this.t.download) {
toolbarControls.push({
el: this.elMenuIcon,
icon: typeof this.t.download === 'string' ? this.t.download : icoMenu,
title: this.localeValues.menu,
class: 'apexcharts-menu-icon',
})
}
for (let i = 0; i < this.elCustomIcons.length; i++) {
toolbarControls.push({
el: this.elCustomIcons[i],
icon: this.t.customIcons[i].icon,
title: this.t.customIcons[i].title,
index: this.t.customIcons[i].index,
class: 'apexcharts-toolbar-custom-icon ' + this.t.customIcons[i].class,
})
}
toolbarControls.forEach((t, index) => {
if (t.index) {
Utils.moveIndexInArray(toolbarControls, index, t.index)
}
})
for (let i = 0; i < toolbarControls.length; i++) {
Graphics.setAttrs(toolbarControls[i].el, {
class: toolbarControls[i].class,
title: toolbarControls[i].title,
})
toolbarControls[i].el.innerHTML = toolbarControls[i].icon
elToolbarWrap.appendChild(toolbarControls[i].el)
}
this._createHamburgerMenu(elToolbarWrap)
if (w.globals.zoomEnabled) {
this.elZoom.classList.add(this.selectedClass)
} else if (w.globals.panEnabled) {
this.elPan.classList.add(this.selectedClass)
} else if (w.globals.selectionEnabled) {
this.elSelection.classList.add(this.selectedClass)
}
this.addToolbarEventListeners()
}
_createHamburgerMenu(parent) {
this.elMenuItems = []
parent.appendChild(this.elMenu)
Graphics.setAttrs(this.elMenu, {
class: 'apexcharts-menu',
})
const menuItems = [
{
name: 'exportSVG',
title: this.localeValues.exportToSVG,
},
{
name: 'exportPNG',
title: this.localeValues.exportToPNG,
},
{
name: 'exportCSV',
title: this.localeValues.exportToCSV,
},
]
for (let i = 0; i < menuItems.length; i++) {
this.elMenuItems.push(document.createElement('div'))
this.elMenuItems[i].innerHTML = menuItems[i].title
Graphics.setAttrs(this.elMenuItems[i], {
class: `apexcharts-menu-item ${menuItems[i].name}`,
title: menuItems[i].title,
})
this.elMenu.appendChild(this.elMenuItems[i])
}
}
addToolbarEventListeners() {
this.elZoomReset.addEventListener('click', this.handleZoomReset.bind(this))
this.elSelection.addEventListener(
'click',
this.toggleZoomSelection.bind(this, 'selection')
)
this.elZoom.addEventListener(
'click',
this.toggleZoomSelection.bind(this, 'zoom')
)
this.elZoomIn.addEventListener('click', this.handleZoomIn.bind(this))
this.elZoomOut.addEventListener('click', this.handleZoomOut.bind(this))
this.elPan.addEventListener('click', this.togglePanning.bind(this))
this.elMenuIcon.addEventListener('click', this.toggleMenu.bind(this))
this.elMenuItems.forEach((m) => {
if (m.classList.contains('exportSVG')) {
m.addEventListener('click', this.handleDownload.bind(this, 'svg'))
} else if (m.classList.contains('exportPNG')) {
m.addEventListener('click', this.handleDownload.bind(this, 'png'))
} else if (m.classList.contains('exportCSV')) {
m.addEventListener('click', this.handleDownload.bind(this, 'csv'))
}
})
for (let i = 0; i < this.t.customIcons.length; i++) {
this.elCustomIcons[i].addEventListener(
'click',
this.t.customIcons[i].click.bind(this, this.ctx, this.ctx.w)
)
}
}
toggleZoomSelection(type) {
const charts = this.ctx.getSyncedCharts()
charts.forEach((ch) => {
ch.ctx.toolbar.toggleOtherControls()
let el =
type === 'selection'
? ch.ctx.toolbar.elSelection
: ch.ctx.toolbar.elZoom
let enabledType =
type === 'selection' ? 'selectionEnabled' : 'zoomEnabled'
ch.w.globals[enabledType] = !ch.w.globals[enabledType]
if (!el.classList.contains(ch.ctx.toolbar.selectedClass)) {
el.classList.add(ch.ctx.toolbar.selectedClass)
} else {
el.classList.remove(ch.ctx.toolbar.selectedClass)
}
})
}
getToolbarIconsReference() {
const w = this.w
if (!this.elZoom) {
this.elZoom = w.globals.dom.baseEl.querySelector('.apexcharts-zoom-icon')
}
if (!this.elPan) {
this.elPan = w.globals.dom.baseEl.querySelector('.apexcharts-pan-icon')
}
if (!this.elSelection) {
this.elSelection = w.globals.dom.baseEl.querySelector(
'.apexcharts-selection-icon'
)
}
}
enableZoomPanFromToolbar(type) {
this.toggleOtherControls()
type === 'pan'
? (this.w.globals.panEnabled = true)
: (this.w.globals.zoomEnabled = true)
const el = type === 'pan' ? this.elPan : this.elZoom
const el2 = type === 'pan' ? this.elZoom : this.elPan
if (el) {
el.classList.add(this.selectedClass)
}
if (el2) {
el2.classList.remove(this.selectedClass)
}
}
togglePanning() {
const charts = this.ctx.getSyncedCharts()
charts.forEach((ch) => {
ch.ctx.toolbar.toggleOtherControls()
ch.w.globals.panEnabled = !ch.w.globals.panEnabled
if (
!ch.ctx.toolbar.elPan.classList.contains(ch.ctx.toolbar.selectedClass)
) {
ch.ctx.toolbar.elPan.classList.add(ch.ctx.toolbar.selectedClass)
} else {
ch.ctx.toolbar.elPan.classList.remove(ch.ctx.toolbar.selectedClass)
}
})
}
toggleOtherControls() {
const w = this.w
w.globals.panEnabled = false
w.globals.zoomEnabled = false
w.globals.selectionEnabled = false
this.getToolbarIconsReference()
const toggleEls = [this.elPan, this.elSelection, this.elZoom]
toggleEls.forEach((el) => {
if (el) {
el.classList.remove(this.selectedClass)
}
})
}
handleZoomIn() {
const w = this.w
if (w.globals.isRangeBar) {
this.minX = w.globals.minY
this.maxX = w.globals.maxY
}
const centerX = (this.minX + this.maxX) / 2
let newMinX = (this.minX + centerX) / 2
let newMaxX = (this.maxX + centerX) / 2
const newMinXMaxX = this._getNewMinXMaxX(newMinX, newMaxX)
if (!w.globals.disableZoomIn) {
this.zoomUpdateOptions(newMinXMaxX.minX, newMinXMaxX.maxX)
}
}
handleZoomOut() {
const w = this.w
if (w.globals.isRangeBar) {
this.minX = w.globals.minY
this.maxX = w.globals.maxY
}
// avoid zooming out beyond 1000 which may result in NaN values being printed on x-axis
if (
w.config.xaxis.type === 'datetime' &&
new Date(this.minX).getUTCFullYear() < 1000
) {
return
}
const centerX = (this.minX + this.maxX) / 2
let newMinX = this.minX - (centerX - this.minX)
let newMaxX = this.maxX - (centerX - this.maxX)
const newMinXMaxX = this._getNewMinXMaxX(newMinX, newMaxX)
if (!w.globals.disableZoomOut) {
this.zoomUpdateOptions(newMinXMaxX.minX, newMinXMaxX.maxX)
}
}
_getNewMinXMaxX(newMinX, newMaxX) {
const shouldFloor = this.w.config.xaxis.convertedCatToNumeric
return {
minX: shouldFloor ? Math.floor(newMinX) : newMinX,
maxX: shouldFloor ? Math.floor(newMaxX) : newMaxX,
}
}
zoomUpdateOptions(newMinX, newMaxX) {
const w = this.w
if (newMinX === undefined && newMaxX === undefined) {
this.handleZoomReset()
return
}
if (w.config.xaxis.convertedCatToNumeric) {
// in category charts, avoid zooming out beyond min and max
if (newMinX < 1) {
newMinX = 1
newMaxX = w.globals.dataPoints
}
if (newMaxX - newMinX < 2) {
return
}
}
let xaxis = {
min: newMinX,
max: newMaxX,
}
const beforeZoomRange = this.getBeforeZoomRange(xaxis)
if (beforeZoomRange) {
xaxis = beforeZoomRange.xaxis
}
let options = {
xaxis,
}
let yaxis = Utils.clone(w.globals.initialConfig.yaxis)
if (!w.config.chart.group) {
// if chart in a group, prevent yaxis update here
// fix issue #650
options.yaxis = yaxis
}
this.w.globals.zoomed = true
this.ctx.updateHelpers._updateOptions(
options,
false,
this.w.config.chart.animations.dynamicAnimation.enabled
)
this.zoomCallback(xaxis, yaxis)
}
zoomCallback(xaxis, yaxis) {
if (typeof this.ev.zoomed === 'function') {
this.ev.zoomed(this.ctx, { xaxis, yaxis })
}
}
getBeforeZoomRange(xaxis, yaxis) {
let newRange = null
if (typeof this.ev.beforeZoom === 'function') {
newRange = this.ev.beforeZoom(this, { xaxis, yaxis })
}
return newRange
}
toggleMenu() {
window.setTimeout(() => {
if (this.elMenu.classList.contains('apexcharts-menu-open')) {
this.elMenu.classList.remove('apexcharts-menu-open')
} else {
this.elMenu.classList.add('apexcharts-menu-open')
}
}, 0)
}
handleDownload(type) {
const w = this.w
const exprt = new Exports(this.ctx)
switch (type) {
case 'svg':
exprt.exportToSVG(this.ctx)
break
case 'png':
exprt.exportToPng(this.ctx)
break
case 'csv':
exprt.exportToCSV({
series: w.config.series,
columnDelimiter: w.config.chart.toolbar.export.csv.columnDelimiter,
})
break
}
}
handleZoomReset(e) {
const charts = this.ctx.getSyncedCharts()
charts.forEach((ch) => {
let w = ch.w
// forget lastXAxis min/max as reset button isn't resetting the x-axis completely if zoomX is called before
w.globals.lastXAxis.min = w.globals.initialConfig.xaxis.min
w.globals.lastXAxis.max = w.globals.initialConfig.xaxis.max
ch.updateHelpers.revertDefaultAxisMinMax()
if (typeof w.config.chart.events.beforeResetZoom === 'function') {
// here, user get an option to control xaxis and yaxis when resetZoom is called
// at this point, whatever is returned from w.config.chart.events.beforeResetZoom
// is set as the new xaxis/yaxis min/max
const resetZoomRange = w.config.chart.events.beforeResetZoom(ch, w)
if (resetZoomRange) {
ch.updateHelpers.revertDefaultAxisMinMax(resetZoomRange)
}
}
if (typeof w.config.chart.events.zoomed === 'function') {
ch.ctx.toolbar.zoomCallback({
min: w.config.xaxis.min,
max: w.config.xaxis.max,
})
}
w.globals.zoomed = false
// if user has some series collapsed before hitting zoom reset button,
// those series should stay collapsed
let series = ch.ctx.series.emptyCollapsedSeries(
Utils.clone(w.globals.initialSeries)
)
ch.updateHelpers._updateSeries(
series,
w.config.chart.animations.dynamicAnimation.enabled
)
})
}
destroy() {
this.elZoom = null
this.elZoomIn = null
this.elZoomOut = null
this.elPan = null
this.elSelection = null
this.elZoomReset = null
this.elMenuIcon = null
}
}

View File

@@ -0,0 +1,949 @@
import Graphics from './Graphics'
import Utils from './../utils/Utils'
import Toolbar from './Toolbar'
import { Box } from '@svgdotjs/svg.js'
/**
* ApexCharts Zoom Class for handling zooming and panning on axes based charts.
*
* @module ZoomPanSelection
**/
export default class ZoomPanSelection extends Toolbar {
constructor(ctx) {
super(ctx)
this.ctx = ctx
this.w = ctx.w
this.dragged = false
this.graphics = new Graphics(this.ctx)
this.eventList = [
'mousedown',
'mouseleave',
'mousemove',
'touchstart',
'touchmove',
'mouseup',
'touchend',
'wheel',
]
this.clientX = 0
this.clientY = 0
this.startX = 0
this.endX = 0
this.dragX = 0
this.startY = 0
this.endY = 0
this.dragY = 0
this.moveDirection = 'none'
this.debounceTimer = null
this.debounceDelay = 100
this.wheelDelay = 400
}
init({ xyRatios }) {
let w = this.w
let me = this
this.xyRatios = xyRatios
this.zoomRect = this.graphics.drawRect(0, 0, 0, 0)
this.selectionRect = this.graphics.drawRect(0, 0, 0, 0)
this.gridRect = w.globals.dom.baseEl.querySelector('.apexcharts-grid')
this.constraints = new Box(0, 0, w.globals.gridWidth, w.globals.gridHeight)
this.zoomRect.node.classList.add('apexcharts-zoom-rect')
this.selectionRect.node.classList.add('apexcharts-selection-rect')
w.globals.dom.Paper.add(this.zoomRect)
w.globals.dom.Paper.add(this.selectionRect)
if (w.config.chart.selection.type === 'x') {
this.slDraggableRect = this.selectionRect
.draggable({
minX: 0,
minY: 0,
maxX: w.globals.gridWidth,
maxY: w.globals.gridHeight,
})
.on('dragmove.namespace', this.selectionDragging.bind(this, 'dragging'))
} else if (w.config.chart.selection.type === 'y') {
this.slDraggableRect = this.selectionRect
.draggable({
minX: 0,
maxX: w.globals.gridWidth,
})
.on('dragmove.namespace', this.selectionDragging.bind(this, 'dragging'))
} else {
this.slDraggableRect = this.selectionRect
.draggable()
.on('dragmove.namespace', this.selectionDragging.bind(this, 'dragging'))
}
this.preselectedSelection()
this.hoverArea = w.globals.dom.baseEl.querySelector(
`${w.globals.chartClass} .apexcharts-svg`
)
this.hoverArea.classList.add('apexcharts-zoomable')
this.eventList.forEach((event) => {
this.hoverArea.addEventListener(
event,
me.svgMouseEvents.bind(me, xyRatios),
{
capture: false,
passive: true,
}
)
})
if (
w.config.chart.zoom.enabled &&
w.config.chart.zoom.allowMouseWheelZoom
) {
this.hoverArea.addEventListener('wheel', me.mouseWheelEvent.bind(me), {
capture: false,
passive: false,
})
}
}
// remove the event listeners which were previously added on hover area
destroy() {
if (this.slDraggableRect) {
this.slDraggableRect.draggable(false)
this.slDraggableRect.off()
this.selectionRect.off()
}
this.selectionRect = null
this.zoomRect = null
this.gridRect = null
}
svgMouseEvents(xyRatios, e) {
let w = this.w
const toolbar = this.ctx.toolbar
let zoomtype = w.globals.zoomEnabled
? w.config.chart.zoom.type
: w.config.chart.selection.type
const autoSelected = w.config.chart.toolbar.autoSelected
if (e.shiftKey) {
this.shiftWasPressed = true
toolbar.enableZoomPanFromToolbar(autoSelected === 'pan' ? 'zoom' : 'pan')
} else {
if (this.shiftWasPressed) {
toolbar.enableZoomPanFromToolbar(autoSelected)
this.shiftWasPressed = false
}
}
if (!e.target) return
const tc = e.target.classList
let pc
if (e.target.parentNode && e.target.parentNode !== null) {
pc = e.target.parentNode.classList
}
const falsePositives =
tc.contains('apexcharts-legend-marker') ||
tc.contains('apexcharts-legend-text') ||
(pc && pc.contains('apexcharts-toolbar'))
if (falsePositives) return
this.clientX =
e.type === 'touchmove' || e.type === 'touchstart'
? e.touches[0].clientX
: e.type === 'touchend'
? e.changedTouches[0].clientX
: e.clientX
this.clientY =
e.type === 'touchmove' || e.type === 'touchstart'
? e.touches[0].clientY
: e.type === 'touchend'
? e.changedTouches[0].clientY
: e.clientY
if ((e.type === 'mousedown' && e.which === 1) || e.type === 'touchstart') {
let gridRectDim = this.gridRect.getBoundingClientRect()
this.startX =
this.clientX - gridRectDim.left - w.globals.barPadForNumericAxis
this.startY = this.clientY - gridRectDim.top
this.dragged = false
this.w.globals.mousedown = true
}
if ((e.type === 'mousemove' && e.which === 1) || e.type === 'touchmove') {
this.dragged = true
if (w.globals.panEnabled) {
w.globals.selection = null
if (this.w.globals.mousedown) {
this.panDragging({
context: this,
zoomtype,
xyRatios,
})
}
} else {
if (
(this.w.globals.mousedown && w.globals.zoomEnabled) ||
(this.w.globals.mousedown && w.globals.selectionEnabled)
) {
this.selection = this.selectionDrawing({
context: this,
zoomtype,
})
}
}
}
if (
e.type === 'mouseup' ||
e.type === 'touchend' ||
e.type === 'mouseleave'
) {
this.handleMouseUp({ zoomtype })
}
this.makeSelectionRectDraggable()
}
handleMouseUp({ zoomtype, isResized }) {
const w = this.w
// we will be calling getBoundingClientRect on each mousedown/mousemove/mouseup
let gridRectDim = this.gridRect?.getBoundingClientRect()
if (gridRectDim && (this.w.globals.mousedown || isResized)) {
// user released the drag, now do all the calculations
this.endX =
this.clientX - gridRectDim.left - w.globals.barPadForNumericAxis
this.endY = this.clientY - gridRectDim.top
this.dragX = Math.abs(this.endX - this.startX)
this.dragY = Math.abs(this.endY - this.startY)
if (w.globals.zoomEnabled || w.globals.selectionEnabled) {
this.selectionDrawn({
context: this,
zoomtype,
})
}
if (w.globals.panEnabled && w.config.xaxis.convertedCatToNumeric) {
this.delayedPanScrolled()
}
}
if (w.globals.zoomEnabled) {
this.hideSelectionRect(this.selectionRect)
}
this.dragged = false
this.w.globals.mousedown = false
}
mouseWheelEvent(e) {
const w = this.w
e.preventDefault()
const now = Date.now()
// Execute immediately if it's the first action or enough time has passed
if (now - w.globals.lastWheelExecution > this.wheelDelay) {
this.executeMouseWheelZoom(e)
w.globals.lastWheelExecution = now
}
if (this.debounceTimer) clearTimeout(this.debounceTimer)
this.debounceTimer = setTimeout(() => {
if (now - w.globals.lastWheelExecution > this.wheelDelay) {
this.executeMouseWheelZoom(e)
w.globals.lastWheelExecution = now
}
}, this.debounceDelay)
}
executeMouseWheelZoom(e) {
const w = this.w
this.minX = w.globals.isRangeBar ? w.globals.minY : w.globals.minX
this.maxX = w.globals.isRangeBar ? w.globals.maxY : w.globals.maxX
// Calculate the relative position of the mouse on the chart
const gridRectDim = this.gridRect?.getBoundingClientRect()
if (!gridRectDim) return
const mouseX = (e.clientX - gridRectDim.left) / gridRectDim.width
const currentMinX = this.minX
const currentMaxX = this.maxX
const totalX = currentMaxX - currentMinX
// Determine zoom factor
const zoomFactorIn = 0.5
const zoomFactorOut = 1.5
let zoomRange
let newMinX, newMaxX
if (e.deltaY < 0) {
// Zoom In
zoomRange = zoomFactorIn * totalX
const midPoint = currentMinX + mouseX * totalX
newMinX = midPoint - zoomRange / 2
newMaxX = midPoint + zoomRange / 2
} else {
// Zoom Out
zoomRange = zoomFactorOut * totalX
newMinX = currentMinX - zoomRange / 2
newMaxX = currentMaxX + zoomRange / 2
}
// Constrain within original chart bounds
if (!w.globals.isRangeBar) {
newMinX = Math.max(newMinX, w.globals.initialMinX)
newMaxX = Math.min(newMaxX, w.globals.initialMaxX)
// Ensure minimum range
const minRange = (w.globals.initialMaxX - w.globals.initialMinX) * 0.01
if (newMaxX - newMinX < minRange) {
const midPoint = (newMinX + newMaxX) / 2
newMinX = midPoint - minRange / 2
newMaxX = midPoint + minRange / 2
}
}
const newMinXMaxX = this._getNewMinXMaxX(newMinX, newMaxX)
// Apply zoom if valid
if (!isNaN(newMinXMaxX.minX) && !isNaN(newMinXMaxX.maxX)) {
this.zoomUpdateOptions(newMinXMaxX.minX, newMinXMaxX.maxX)
}
}
makeSelectionRectDraggable() {
const w = this.w
if (!this.selectionRect) return
const rectDim = this.selectionRect.node.getBoundingClientRect()
if (rectDim.width > 0 && rectDim.height > 0) {
this.selectionRect.select(false).resize(false)
this.selectionRect
.select({
createRot: () => {},
updateRot: () => {},
createHandle: (group, p, index, pointArr, handleName) => {
if (handleName === 'l' || handleName === 'r')
return group
.circle(8)
.css({ 'stroke-width': 1, stroke: '#333', fill: '#fff' })
return group.circle(0)
},
updateHandle: (group, p) => {
return group.center(p[0], p[1])
},
})
.resize()
.on('resize', () => {
let zoomtype = w.globals.zoomEnabled
? w.config.chart.zoom.type
: w.config.chart.selection.type
this.handleMouseUp({ zoomtype, isResized: true })
})
}
}
preselectedSelection() {
const w = this.w
const xyRatios = this.xyRatios
if (!w.globals.zoomEnabled) {
if (
typeof w.globals.selection !== 'undefined' &&
w.globals.selection !== null
) {
this.drawSelectionRect({
...w.globals.selection,
translateX: w.globals.translateX,
translateY: w.globals.translateY,
})
} else {
if (
w.config.chart.selection.xaxis.min !== undefined &&
w.config.chart.selection.xaxis.max !== undefined
) {
let x =
(w.config.chart.selection.xaxis.min - w.globals.minX) /
xyRatios.xRatio
let width =
w.globals.gridWidth -
(w.globals.maxX - w.config.chart.selection.xaxis.max) /
xyRatios.xRatio -
x
if (w.globals.isRangeBar) {
// rangebars put datetime data in y axis
x =
(w.config.chart.selection.xaxis.min -
w.globals.yAxisScale[0].niceMin) /
xyRatios.invertedYRatio
width =
(w.config.chart.selection.xaxis.max -
w.config.chart.selection.xaxis.min) /
xyRatios.invertedYRatio
}
let selectionRect = {
x,
y: 0,
width,
height: w.globals.gridHeight,
translateX: w.globals.translateX,
translateY: w.globals.translateY,
selectionEnabled: true,
}
this.drawSelectionRect(selectionRect)
this.makeSelectionRectDraggable()
if (typeof w.config.chart.events.selection === 'function') {
w.config.chart.events.selection(this.ctx, {
xaxis: {
min: w.config.chart.selection.xaxis.min,
max: w.config.chart.selection.xaxis.max,
},
yaxis: {},
})
}
}
}
}
}
drawSelectionRect({ x, y, width, height, translateX = 0, translateY = 0 }) {
const w = this.w
const zoomRect = this.zoomRect
const selectionRect = this.selectionRect
if (this.dragged || w.globals.selection !== null) {
let scalingAttrs = {
transform: 'translate(' + translateX + ', ' + translateY + ')',
}
// change styles based on zoom or selection
// zoom is Enabled and user has dragged, so draw blue rect
if (w.globals.zoomEnabled && this.dragged) {
if (width < 0) width = 1 // fixes apexcharts.js#1168
zoomRect.attr({
x,
y,
width,
height,
fill: w.config.chart.zoom.zoomedArea.fill.color,
'fill-opacity': w.config.chart.zoom.zoomedArea.fill.opacity,
stroke: w.config.chart.zoom.zoomedArea.stroke.color,
'stroke-width': w.config.chart.zoom.zoomedArea.stroke.width,
'stroke-opacity': w.config.chart.zoom.zoomedArea.stroke.opacity,
})
Graphics.setAttrs(zoomRect.node, scalingAttrs)
}
// selection is enabled
if (w.globals.selectionEnabled) {
selectionRect.attr({
x,
y,
width: width > 0 ? width : 0,
height: height > 0 ? height : 0,
fill: w.config.chart.selection.fill.color,
'fill-opacity': w.config.chart.selection.fill.opacity,
stroke: w.config.chart.selection.stroke.color,
'stroke-width': w.config.chart.selection.stroke.width,
'stroke-dasharray': w.config.chart.selection.stroke.dashArray,
'stroke-opacity': w.config.chart.selection.stroke.opacity,
})
Graphics.setAttrs(selectionRect.node, scalingAttrs)
}
}
}
hideSelectionRect(rect) {
if (rect) {
rect.attr({
x: 0,
y: 0,
width: 0,
height: 0,
})
}
}
selectionDrawing({ context, zoomtype }) {
const w = this.w
let me = context
let gridRectDim = this.gridRect.getBoundingClientRect()
let startX = me.startX - 1
let startY = me.startY
let inversedX = false
let inversedY = false
const left = me.clientX - gridRectDim.left - w.globals.barPadForNumericAxis
const top = me.clientY - gridRectDim.top
let selectionWidth = left - startX
let selectionHeight = top - startY
let selectionRect = {
translateX: w.globals.translateX,
translateY: w.globals.translateY,
}
if (Math.abs(selectionWidth + startX) > w.globals.gridWidth) {
// user dragged the mouse outside drawing area to the right
selectionWidth = w.globals.gridWidth - startX
} else if (left < 0) {
// user dragged the mouse outside drawing area to the left
selectionWidth = startX
}
// inverse selection X
if (startX > left) {
inversedX = true
selectionWidth = Math.abs(selectionWidth)
}
// inverse selection Y
if (startY > top) {
inversedY = true
selectionHeight = Math.abs(selectionHeight)
}
if (zoomtype === 'x') {
selectionRect = {
x: inversedX ? startX - selectionWidth : startX,
y: 0,
width: selectionWidth,
height: w.globals.gridHeight,
}
} else if (zoomtype === 'y') {
selectionRect = {
x: 0,
y: inversedY ? startY - selectionHeight : startY,
width: w.globals.gridWidth,
height: selectionHeight,
}
} else {
selectionRect = {
x: inversedX ? startX - selectionWidth : startX,
y: inversedY ? startY - selectionHeight : startY,
width: selectionWidth,
height: selectionHeight,
}
}
selectionRect = {
...selectionRect,
translateX: w.globals.translateX,
translateY: w.globals.translateY,
}
me.drawSelectionRect(selectionRect)
me.selectionDragging('resizing')
return selectionRect
}
selectionDragging(type, e) {
const w = this.w
if (!e) return
e.preventDefault()
const { handler, box } = e.detail
let { x, y } = box
if (x < this.constraints.x) {
x = this.constraints.x
}
if (y < this.constraints.y) {
y = this.constraints.y
}
if (box.x2 > this.constraints.x2) {
x = this.constraints.x2 - box.w
}
if (box.y2 > this.constraints.y2) {
y = this.constraints.y2 - box.h
}
handler.move(x, y)
const xyRatios = this.xyRatios
const selRect = this.selectionRect
let timerInterval = 0
if (type === 'resizing') {
timerInterval = 30
}
// update selection when selection rect is dragged
const getSelAttr = (attr) => {
return parseFloat(selRect.node.getAttribute(attr))
}
const draggedProps = {
x: getSelAttr('x'),
y: getSelAttr('y'),
width: getSelAttr('width'),
height: getSelAttr('height'),
}
w.globals.selection = draggedProps
// update selection ends
if (
typeof w.config.chart.events.selection === 'function' &&
w.globals.selectionEnabled
) {
// a small debouncer is required when resizing to avoid freezing the chart
clearTimeout(this.w.globals.selectionResizeTimer)
this.w.globals.selectionResizeTimer = window.setTimeout(() => {
const gridRectDim = this.gridRect.getBoundingClientRect()
const selectionRect = selRect.node.getBoundingClientRect()
let minX, maxX, minY, maxY
if (!w.globals.isRangeBar) {
// normal XY charts
minX =
w.globals.xAxisScale.niceMin +
(selectionRect.left - gridRectDim.left) * xyRatios.xRatio
maxX =
w.globals.xAxisScale.niceMin +
(selectionRect.right - gridRectDim.left) * xyRatios.xRatio
minY =
w.globals.yAxisScale[0].niceMin +
(gridRectDim.bottom - selectionRect.bottom) * xyRatios.yRatio[0]
maxY =
w.globals.yAxisScale[0].niceMax -
(selectionRect.top - gridRectDim.top) * xyRatios.yRatio[0]
} else {
// rangeBars use y for datetime
minX =
w.globals.yAxisScale[0].niceMin +
(selectionRect.left - gridRectDim.left) * xyRatios.invertedYRatio
maxX =
w.globals.yAxisScale[0].niceMin +
(selectionRect.right - gridRectDim.left) * xyRatios.invertedYRatio
minY = 0
maxY = 1
}
const xyAxis = {
xaxis: {
min: minX,
max: maxX,
},
yaxis: {
min: minY,
max: maxY,
},
}
w.config.chart.events.selection(this.ctx, xyAxis)
if (
w.config.chart.brush.enabled &&
w.config.chart.events.brushScrolled !== undefined
) {
w.config.chart.events.brushScrolled(this.ctx, xyAxis)
}
}, timerInterval)
}
}
selectionDrawn({ context, zoomtype }) {
const w = this.w
const me = context
const xyRatios = this.xyRatios
const toolbar = this.ctx.toolbar
// Use boundingRect for final selection area
const selRect = w.globals.zoomEnabled
? me.zoomRect.node.getBoundingClientRect()
: me.selectionRect.node.getBoundingClientRect()
const gridRectDim = me.gridRect.getBoundingClientRect()
// Local coords in the chart's grid
const localStartX =
selRect.left - gridRectDim.left - w.globals.barPadForNumericAxis
const localEndX =
selRect.right - gridRectDim.left - w.globals.barPadForNumericAxis
const localStartY = selRect.top - gridRectDim.top
const localEndY = selRect.bottom - gridRectDim.top
// Convert those local coords to actual data values
let xLowestValue, xHighestValue
if (!w.globals.isRangeBar) {
xLowestValue =
w.globals.xAxisScale.niceMin + localStartX * xyRatios.xRatio
xHighestValue = w.globals.xAxisScale.niceMin + localEndX * xyRatios.xRatio
} else {
xLowestValue =
w.globals.yAxisScale[0].niceMin + localStartX * xyRatios.invertedYRatio
xHighestValue =
w.globals.yAxisScale[0].niceMin + localEndX * xyRatios.invertedYRatio
}
// For Y values, pick from the first y-axis, but handle multi-axis
let yHighestValue = []
let yLowestValue = []
w.config.yaxis.forEach((yaxe, index) => {
// pick whichever series is mapped to this y-axis
let seriesIndex = w.globals.seriesYAxisMap[index][0]
let highestVal =
w.globals.yAxisScale[index].niceMax -
xyRatios.yRatio[seriesIndex] * localStartY
let lowestVal =
w.globals.yAxisScale[index].niceMax -
xyRatios.yRatio[seriesIndex] * localEndY
yHighestValue.push(highestVal)
yLowestValue.push(lowestVal)
})
// Only apply if user actually dragged far enough to consider it a selection
if (
me.dragged &&
(me.dragX > 10 || me.dragY > 10) &&
xLowestValue !== xHighestValue
) {
if (w.globals.zoomEnabled) {
let yaxis = Utils.clone(w.globals.initialConfig.yaxis)
let xaxis = Utils.clone(w.globals.initialConfig.xaxis)
w.globals.zoomed = true
if (w.config.xaxis.convertedCatToNumeric) {
xLowestValue = Math.floor(xLowestValue)
xHighestValue = Math.floor(xHighestValue)
if (xLowestValue < 1) {
xLowestValue = 1
xHighestValue = w.globals.dataPoints
}
if (xHighestValue - xLowestValue < 2) {
xHighestValue = xLowestValue + 1
}
}
if (zoomtype === 'xy' || zoomtype === 'x') {
xaxis = {
min: xLowestValue,
max: xHighestValue,
}
}
if (zoomtype === 'xy' || zoomtype === 'y') {
yaxis.forEach((yaxe, index) => {
yaxis[index].min = yLowestValue[index]
yaxis[index].max = yHighestValue[index]
})
}
if (toolbar) {
let beforeZoomRange = toolbar.getBeforeZoomRange(xaxis, yaxis)
if (beforeZoomRange) {
xaxis = beforeZoomRange.xaxis ? beforeZoomRange.xaxis : xaxis
yaxis = beforeZoomRange.yaxis ? beforeZoomRange.yaxis : yaxis
}
}
let options = {
xaxis,
}
if (!w.config.chart.group) {
// if chart in a group, prevent yaxis update here
// fix issue #650
options.yaxis = yaxis
}
me.ctx.updateHelpers._updateOptions(
options,
false,
me.w.config.chart.animations.dynamicAnimation.enabled
)
if (typeof w.config.chart.events.zoomed === 'function') {
toolbar.zoomCallback(xaxis, yaxis)
}
} else if (w.globals.selectionEnabled) {
let yaxis = null
let xaxis = null
xaxis = {
min: xLowestValue,
max: xHighestValue,
}
if (zoomtype === 'xy' || zoomtype === 'y') {
yaxis = Utils.clone(w.config.yaxis)
yaxis.forEach((yaxe, index) => {
yaxis[index].min = yLowestValue[index]
yaxis[index].max = yHighestValue[index]
})
}
w.globals.selection = me.selection
if (typeof w.config.chart.events.selection === 'function') {
w.config.chart.events.selection(me.ctx, {
xaxis,
yaxis,
})
}
}
}
}
panDragging({ context }) {
const w = this.w
let me = context
// check to make sure there is data to compare against
if (typeof w.globals.lastClientPosition.x !== 'undefined') {
// get the change from last position to this position
const deltaX = w.globals.lastClientPosition.x - me.clientX
const deltaY = w.globals.lastClientPosition.y - me.clientY
// check which direction had the highest amplitude
if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 0) {
this.moveDirection = 'left'
} else if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX < 0) {
this.moveDirection = 'right'
} else if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY > 0) {
this.moveDirection = 'up'
} else if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY < 0) {
this.moveDirection = 'down'
}
}
// set the new last position to the current for next time (to get the position of drag)
w.globals.lastClientPosition = {
x: me.clientX,
y: me.clientY,
}
let xLowestValue = w.globals.isRangeBar ? w.globals.minY : w.globals.minX
let xHighestValue = w.globals.isRangeBar ? w.globals.maxY : w.globals.maxX
// on a category, we don't pan continuously as it causes bugs
if (!w.config.xaxis.convertedCatToNumeric) {
me.panScrolled(xLowestValue, xHighestValue)
}
}
delayedPanScrolled() {
const w = this.w
let newMinX = w.globals.minX
let newMaxX = w.globals.maxX
const centerX = (w.globals.maxX - w.globals.minX) / 2
if (this.moveDirection === 'left') {
newMinX = w.globals.minX + centerX
newMaxX = w.globals.maxX + centerX
} else if (this.moveDirection === 'right') {
newMinX = w.globals.minX - centerX
newMaxX = w.globals.maxX - centerX
}
newMinX = Math.floor(newMinX)
newMaxX = Math.floor(newMaxX)
this.updateScrolledChart(
{ xaxis: { min: newMinX, max: newMaxX } },
newMinX,
newMaxX
)
}
panScrolled(xLowestValue, xHighestValue) {
const w = this.w
const xyRatios = this.xyRatios
let yaxis = Utils.clone(w.globals.initialConfig.yaxis)
let xRatio = xyRatios.xRatio
let minX = w.globals.minX
let maxX = w.globals.maxX
if (w.globals.isRangeBar) {
xRatio = xyRatios.invertedYRatio
minX = w.globals.minY
maxX = w.globals.maxY
}
if (this.moveDirection === 'left') {
xLowestValue = minX + (w.globals.gridWidth / 15) * xRatio
xHighestValue = maxX + (w.globals.gridWidth / 15) * xRatio
} else if (this.moveDirection === 'right') {
xLowestValue = minX - (w.globals.gridWidth / 15) * xRatio
xHighestValue = maxX - (w.globals.gridWidth / 15) * xRatio
}
if (!w.globals.isRangeBar) {
if (
xLowestValue < w.globals.initialMinX ||
xHighestValue > w.globals.initialMaxX
) {
xLowestValue = minX
xHighestValue = maxX
}
}
let xaxis = {
min: xLowestValue,
max: xHighestValue,
}
let options = {
xaxis,
}
if (!w.config.chart.group) {
// if chart in a group, prevent yaxis update here
// fix issue #650
options.yaxis = yaxis
}
this.updateScrolledChart(options, xLowestValue, xHighestValue)
}
updateScrolledChart(options, xLowestValue, xHighestValue) {
const w = this.w
this.ctx.updateHelpers._updateOptions(options, false, false)
if (typeof w.config.chart.events.scrolled === 'function') {
w.config.chart.events.scrolled(this.ctx, {
xaxis: {
min: xLowestValue,
max: xHighestValue,
},
})
}
}
}

View File

@@ -0,0 +1,325 @@
import Graphics from '../../modules/Graphics'
import Utils from '../../utils/Utils'
import Helpers from './Helpers'
import XAxisAnnotations from './XAxisAnnotations'
import YAxisAnnotations from './YAxisAnnotations'
import PointsAnnotations from './PointsAnnotations'
import Options from './../settings/Options'
/**
* ApexCharts Annotations Class for drawing lines/rects on both xaxis and yaxis.
*
* @module Annotations
**/
export default class Annotations {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.graphics = new Graphics(this.ctx)
if (this.w.globals.isBarHorizontal) {
this.invertAxis = true
}
this.helpers = new Helpers(this)
this.xAxisAnnotations = new XAxisAnnotations(this)
this.yAxisAnnotations = new YAxisAnnotations(this)
this.pointsAnnotations = new PointsAnnotations(this)
if (this.w.globals.isBarHorizontal && this.w.config.yaxis[0].reversed) {
this.inversedReversedAxis = true
}
this.xDivision = this.w.globals.gridWidth / this.w.globals.dataPoints
}
drawAxesAnnotations() {
const w = this.w
if (w.globals.axisCharts && w.globals.dataPoints) {
// w.globals.dataPoints check added to fix #1832
let yAnnotations = this.yAxisAnnotations.drawYAxisAnnotations()
let xAnnotations = this.xAxisAnnotations.drawXAxisAnnotations()
let pointAnnotations = this.pointsAnnotations.drawPointAnnotations()
const initialAnim = w.config.chart.animations.enabled
const annoArray = [yAnnotations, xAnnotations, pointAnnotations]
const annoElArray = [
xAnnotations.node,
yAnnotations.node,
pointAnnotations.node,
]
for (let i = 0; i < 3; i++) {
w.globals.dom.elGraphical.add(annoArray[i])
if (initialAnim && !w.globals.resized && !w.globals.dataChanged) {
// fixes apexcharts/apexcharts.js#685
if (
w.config.chart.type !== 'scatter' &&
w.config.chart.type !== 'bubble' &&
w.globals.dataPoints > 1
) {
annoElArray[i].classList.add('apexcharts-element-hidden')
}
}
w.globals.delayedElements.push({ el: annoElArray[i], index: 0 })
}
// background sizes needs to be calculated after text is drawn, so calling them last
this.helpers.annotationsBackground()
}
}
drawImageAnnos() {
const w = this.w
w.config.annotations.images.map((s, index) => {
this.addImage(s, index)
})
}
drawTextAnnos() {
const w = this.w
w.config.annotations.texts.map((t, index) => {
this.addText(t, index)
})
}
addXaxisAnnotation(anno, parent, index) {
this.xAxisAnnotations.addXaxisAnnotation(anno, parent, index)
}
addYaxisAnnotation(anno, parent, index) {
this.yAxisAnnotations.addYaxisAnnotation(anno, parent, index)
}
addPointAnnotation(anno, parent, index) {
this.pointsAnnotations.addPointAnnotation(anno, parent, index)
}
addText(params, index) {
const {
x,
y,
text,
textAnchor,
foreColor,
fontSize,
fontFamily,
fontWeight,
cssClass,
backgroundColor,
borderWidth,
strokeDashArray,
borderRadius,
borderColor,
appendTo = '.apexcharts-svg',
paddingLeft = 4,
paddingRight = 4,
paddingBottom = 2,
paddingTop = 2,
} = params
const w = this.w
let elText = this.graphics.drawText({
x,
y,
text,
textAnchor: textAnchor || 'start',
fontSize: fontSize || '12px',
fontWeight: fontWeight || 'regular',
fontFamily: fontFamily || w.config.chart.fontFamily,
foreColor: foreColor || w.config.chart.foreColor,
cssClass: 'apexcharts-text ' + cssClass ? cssClass : '',
})
const parent = w.globals.dom.baseEl.querySelector(appendTo)
if (parent) {
parent.appendChild(elText.node)
}
const textRect = elText.bbox()
if (text) {
const elRect = this.graphics.drawRect(
textRect.x - paddingLeft,
textRect.y - paddingTop,
textRect.width + paddingLeft + paddingRight,
textRect.height + paddingBottom + paddingTop,
borderRadius,
backgroundColor ? backgroundColor : 'transparent',
1,
borderWidth,
borderColor,
strokeDashArray
)
parent.insertBefore(elRect.node, elText.node)
}
}
addImage(params, index) {
const w = this.w
const {
path,
x = 0,
y = 0,
width = 20,
height = 20,
appendTo = '.apexcharts-svg',
} = params
let img = w.globals.dom.Paper.image(path)
img.size(width, height).move(x, y)
const parent = w.globals.dom.baseEl.querySelector(appendTo)
if (parent) {
parent.appendChild(img.node)
}
return img
}
// The addXaxisAnnotation method requires a parent class, and user calling this method externally on the chart instance may not specify parent, hence a different method
addXaxisAnnotationExternal(params, pushToMemory, context) {
this.addAnnotationExternal({
params,
pushToMemory,
context,
type: 'xaxis',
contextMethod: context.addXaxisAnnotation,
})
return context
}
addYaxisAnnotationExternal(params, pushToMemory, context) {
this.addAnnotationExternal({
params,
pushToMemory,
context,
type: 'yaxis',
contextMethod: context.addYaxisAnnotation,
})
return context
}
addPointAnnotationExternal(params, pushToMemory, context) {
if (typeof this.invertAxis === 'undefined') {
this.invertAxis = context.w.globals.isBarHorizontal
}
this.addAnnotationExternal({
params,
pushToMemory,
context,
type: 'point',
contextMethod: context.addPointAnnotation,
})
return context
}
addAnnotationExternal({
params,
pushToMemory,
context,
type,
contextMethod,
}) {
const me = context
const w = me.w
const parent = w.globals.dom.baseEl.querySelector(
`.apexcharts-${type}-annotations`
)
const index = parent.childNodes.length + 1
const options = new Options()
const axesAnno = Object.assign(
{},
type === 'xaxis'
? options.xAxisAnnotation
: type === 'yaxis'
? options.yAxisAnnotation
: options.pointAnnotation
)
const anno = Utils.extend(axesAnno, params)
switch (type) {
case 'xaxis':
this.addXaxisAnnotation(anno, parent, index)
break
case 'yaxis':
this.addYaxisAnnotation(anno, parent, index)
break
case 'point':
this.addPointAnnotation(anno, parent, index)
break
}
// add background
let axesAnnoLabel = w.globals.dom.baseEl.querySelector(
`.apexcharts-${type}-annotations .apexcharts-${type}-annotation-label[rel='${index}']`
)
const elRect = this.helpers.addBackgroundToAnno(axesAnnoLabel, anno)
if (elRect) {
parent.insertBefore(elRect.node, axesAnnoLabel)
}
if (pushToMemory) {
w.globals.memory.methodsToExec.push({
context: me,
id: anno.id ? anno.id : Utils.randomId(),
method: contextMethod,
label: 'addAnnotation',
params,
})
}
return context
}
clearAnnotations(ctx) {
const w = ctx.w
let annos = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-yaxis-annotations, .apexcharts-xaxis-annotations, .apexcharts-point-annotations'
)
// annotations added externally should be cleared out too
for (let i = w.globals.memory.methodsToExec.length - 1; i >= 0; i--) {
if (
w.globals.memory.methodsToExec[i].label === 'addText' ||
w.globals.memory.methodsToExec[i].label === 'addAnnotation'
) {
w.globals.memory.methodsToExec.splice(i, 1)
}
}
annos = Utils.listToArray(annos)
// delete the DOM elements
Array.prototype.forEach.call(annos, (a) => {
while (a.firstChild) {
a.removeChild(a.firstChild)
}
})
}
removeAnnotation(ctx, id) {
const w = ctx.w
let annos = w.globals.dom.baseEl.querySelectorAll(`.${id}`)
if (annos) {
w.globals.memory.methodsToExec.map((m, i) => {
if (m.id === id) {
w.globals.memory.methodsToExec.splice(i, 1)
}
})
Array.prototype.forEach.call(annos, (a) => {
a.parentElement.removeChild(a)
})
}
}
}

View File

@@ -0,0 +1,258 @@
import CoreUtils from '../CoreUtils'
export default class Helpers {
constructor(annoCtx) {
this.w = annoCtx.w
this.annoCtx = annoCtx
}
setOrientations(anno, annoIndex = null) {
const w = this.w
if (anno.label.orientation === 'vertical') {
const i = annoIndex !== null ? annoIndex : 0
const xAnno = w.globals.dom.baseEl.querySelector(
`.apexcharts-xaxis-annotations .apexcharts-xaxis-annotation-label[rel='${i}']`
)
if (xAnno !== null) {
const xAnnoCoord = xAnno.getBoundingClientRect()
xAnno.setAttribute(
'x',
parseFloat(xAnno.getAttribute('x')) - xAnnoCoord.height + 4
)
const yOffset =
anno.label.position === 'top' ? xAnnoCoord.width : -xAnnoCoord.width
xAnno.setAttribute('y', parseFloat(xAnno.getAttribute('y')) + yOffset)
const { x, y } = this.annoCtx.graphics.rotateAroundCenter(xAnno)
xAnno.setAttribute('transform', `rotate(-90 ${x} ${y})`)
}
}
}
addBackgroundToAnno(annoEl, anno) {
const w = this.w
if (!annoEl || !anno.label.text || !String(anno.label.text).trim()) {
return null
}
const elGridRect = w.globals.dom.baseEl
.querySelector('.apexcharts-grid')
.getBoundingClientRect()
const coords = annoEl.getBoundingClientRect()
let {
left: pleft,
right: pright,
top: ptop,
bottom: pbottom,
} = anno.label.style.padding
if (anno.label.orientation === 'vertical') {
;[ptop, pbottom, pleft, pright] = [pleft, pright, ptop, pbottom]
}
const x1 = coords.left - elGridRect.left - pleft
const y1 = coords.top - elGridRect.top - ptop
const elRect = this.annoCtx.graphics.drawRect(
x1 - w.globals.barPadForNumericAxis,
y1,
coords.width + pleft + pright,
coords.height + ptop + pbottom,
anno.label.borderRadius,
anno.label.style.background,
1,
anno.label.borderWidth,
anno.label.borderColor,
0
)
if (anno.id) {
elRect.node.classList.add(anno.id)
}
return elRect
}
annotationsBackground() {
const w = this.w
const add = (anno, i, type) => {
const annoLabel = w.globals.dom.baseEl.querySelector(
`.apexcharts-${type}-annotations .apexcharts-${type}-annotation-label[rel='${i}']`
)
if (annoLabel) {
const parent = annoLabel.parentNode
const elRect = this.addBackgroundToAnno(annoLabel, anno)
if (elRect) {
parent.insertBefore(elRect.node, annoLabel)
if (anno.label.mouseEnter) {
elRect.node.addEventListener(
'mouseenter',
anno.label.mouseEnter.bind(this, anno)
)
}
if (anno.label.mouseLeave) {
elRect.node.addEventListener(
'mouseleave',
anno.label.mouseLeave.bind(this, anno)
)
}
if (anno.label.click) {
elRect.node.addEventListener(
'click',
anno.label.click.bind(this, anno)
)
}
}
}
}
w.config.annotations.xaxis.forEach((anno, i) => add(anno, i, 'xaxis'))
w.config.annotations.yaxis.forEach((anno, i) => add(anno, i, 'yaxis'))
w.config.annotations.points.forEach((anno, i) => add(anno, i, 'point'))
}
getY1Y2(type, anno) {
const w = this.w
let y = type === 'y1' ? anno.y : anno.y2
let yP
let clipped = false
if (this.annoCtx.invertAxis) {
const labels = w.config.xaxis.convertedCatToNumeric
? w.globals.categoryLabels
: w.globals.labels
const catIndex = labels.indexOf(y)
const xLabel = w.globals.dom.baseEl.querySelector(
`.apexcharts-yaxis-texts-g text:nth-child(${catIndex + 1})`
)
yP = xLabel
? parseFloat(xLabel.getAttribute('y'))
: (w.globals.gridHeight / labels.length - 1) * (catIndex + 1) -
w.globals.barHeight
if (anno.seriesIndex !== undefined && w.globals.barHeight) {
yP -=
(w.globals.barHeight / 2) * (w.globals.series.length - 1) -
w.globals.barHeight * anno.seriesIndex
}
} else {
const seriesIndex = w.globals.seriesYAxisMap[anno.yAxisIndex][0]
const yPos = w.config.yaxis[anno.yAxisIndex].logarithmic
? new CoreUtils(this.annoCtx.ctx).getLogVal(
w.config.yaxis[anno.yAxisIndex].logBase,
y,
seriesIndex
) / w.globals.yLogRatio[seriesIndex]
: (y - w.globals.minYArr[seriesIndex]) /
(w.globals.yRange[seriesIndex] / w.globals.gridHeight)
yP =
w.globals.gridHeight - Math.min(Math.max(yPos, 0), w.globals.gridHeight)
clipped = yPos > w.globals.gridHeight || yPos < 0
if (anno.marker && (anno.y === undefined || anno.y === null)) {
yP = 0
}
if (w.config.yaxis[anno.yAxisIndex]?.reversed) {
yP = yPos
}
}
if (typeof y === 'string' && y.includes('px')) {
yP = parseFloat(y)
}
return { yP, clipped }
}
getX1X2(type, anno) {
const w = this.w
const x = type === 'x1' ? anno.x : anno.x2
const min = this.annoCtx.invertAxis ? w.globals.minY : w.globals.minX
const max = this.annoCtx.invertAxis ? w.globals.maxY : w.globals.maxX
const range = this.annoCtx.invertAxis
? w.globals.yRange[0]
: w.globals.xRange
let clipped = false
let xP = this.annoCtx.inversedReversedAxis
? (max - x) / (range / w.globals.gridWidth)
: (x - min) / (range / w.globals.gridWidth)
if (
(w.config.xaxis.type === 'category' ||
w.config.xaxis.convertedCatToNumeric) &&
!this.annoCtx.invertAxis &&
!w.globals.dataFormatXNumeric
) {
if (!w.config.chart.sparkline.enabled) {
xP = this.getStringX(x)
}
}
if (typeof x === 'string' && x.includes('px')) {
xP = parseFloat(x)
}
if ((x === undefined || x === null) && anno.marker) {
xP = w.globals.gridWidth
}
if (
anno.seriesIndex !== undefined &&
w.globals.barWidth &&
!this.annoCtx.invertAxis
) {
xP -=
(w.globals.barWidth / 2) * (w.globals.series.length - 1) -
w.globals.barWidth * anno.seriesIndex
}
if (xP > w.globals.gridWidth) {
xP = w.globals.gridWidth
clipped = true
} else if (xP < 0) {
xP = 0
clipped = true
}
return { x: xP, clipped }
}
getStringX(x) {
const w = this.w
let rX = x
if (
w.config.xaxis.convertedCatToNumeric &&
w.globals.categoryLabels.length
) {
x = w.globals.categoryLabels.indexOf(x) + 1
}
const catIndex = w.globals.labels
.map((item) => (Array.isArray(item) ? item.join(' ') : item))
.indexOf(x)
const xLabel = w.globals.dom.baseEl.querySelector(
`.apexcharts-xaxis-texts-g text:nth-child(${catIndex + 1})`
)
if (xLabel) {
rX = parseFloat(xLabel.getAttribute('x'))
}
return rX
}
}

View File

@@ -0,0 +1,136 @@
import Utils from '../../utils/Utils'
import Helpers from './Helpers'
export default class PointAnnotations {
constructor(annoCtx) {
this.w = annoCtx.w
this.annoCtx = annoCtx
this.helpers = new Helpers(this.annoCtx)
}
addPointAnnotation(anno, parent, index) {
const w = this.w
if (w.globals.collapsedSeriesIndices.indexOf(anno.seriesIndex) > -1) {
return
}
let result = this.helpers.getX1X2('x1', anno)
let x = result.x
let clipX = result.clipped
result = this.helpers.getY1Y2('y1', anno)
let y = result.yP
let clipY = result.clipped
if (!Utils.isNumber(x)) return
if (!(clipY || clipX)) {
let optsPoints = {
pSize: anno.marker.size,
pointStrokeWidth: anno.marker.strokeWidth,
pointFillColor: anno.marker.fillColor,
pointStrokeColor: anno.marker.strokeColor,
shape: anno.marker.shape,
pRadius: anno.marker.radius,
class: `apexcharts-point-annotation-marker ${anno.marker.cssClass} ${
anno.id ? anno.id : ''
}`,
}
let point = this.annoCtx.graphics.drawMarker(
x + anno.marker.offsetX,
y + anno.marker.offsetY,
optsPoints
)
parent.appendChild(point.node)
const text = anno.label.text ? anno.label.text : ''
let elText = this.annoCtx.graphics.drawText({
x: x + anno.label.offsetX,
y:
y +
anno.label.offsetY -
anno.marker.size -
parseFloat(anno.label.style.fontSize) / 1.6,
text,
textAnchor: anno.label.textAnchor,
fontSize: anno.label.style.fontSize,
fontFamily: anno.label.style.fontFamily,
fontWeight: anno.label.style.fontWeight,
foreColor: anno.label.style.color,
cssClass: `apexcharts-point-annotation-label ${
anno.label.style.cssClass
} ${anno.id ? anno.id : ''}`,
})
elText.attr({
rel: index,
})
parent.appendChild(elText.node)
// TODO: deprecate this as we will use custom
if (anno.customSVG.SVG) {
let g = this.annoCtx.graphics.group({
class:
'apexcharts-point-annotations-custom-svg ' + anno.customSVG.cssClass,
})
g.attr({
transform: `translate(${x + anno.customSVG.offsetX}, ${
y + anno.customSVG.offsetY
})`,
})
g.node.innerHTML = anno.customSVG.SVG
parent.appendChild(g.node)
}
if (anno.image.path) {
let imgWidth = anno.image.width ? anno.image.width : 20
let imgHeight = anno.image.height ? anno.image.height : 20
point = this.annoCtx.addImage({
x: x + anno.image.offsetX - imgWidth / 2,
y: y + anno.image.offsetY - imgHeight / 2,
width: imgWidth,
height: imgHeight,
path: anno.image.path,
appendTo: '.apexcharts-point-annotations',
})
}
if (anno.mouseEnter) {
point.node.addEventListener(
'mouseenter',
anno.mouseEnter.bind(this, anno)
)
}
if (anno.mouseLeave) {
point.node.addEventListener(
'mouseleave',
anno.mouseLeave.bind(this, anno)
)
}
if (anno.click) {
point.node.addEventListener('click', anno.click.bind(this, anno))
}
}
}
drawPointAnnotations() {
let w = this.w
let elg = this.annoCtx.graphics.group({
class: 'apexcharts-point-annotations',
})
w.config.annotations.points.map((anno, index) => {
this.addPointAnnotation(anno, elg.node, index)
})
return elg
}
}

View File

@@ -0,0 +1,133 @@
import Utils from '../../utils/Utils'
import Helpers from './Helpers'
export default class XAnnotations {
constructor(annoCtx) {
this.w = annoCtx.w
this.annoCtx = annoCtx
this.invertAxis = this.annoCtx.invertAxis
this.helpers = new Helpers(this.annoCtx)
}
addXaxisAnnotation(anno, parent, index) {
let w = this.w
let result = this.helpers.getX1X2('x1', anno)
let x1 = result.x
let clipX1 = result.clipped
let clipX2 = true
let x2
const text = anno.label.text
let strokeDashArray = anno.strokeDashArray
if (!Utils.isNumber(x1)) return
if (anno.x2 === null || typeof anno.x2 === 'undefined') {
if (!clipX1) {
let line = this.annoCtx.graphics.drawLine(
x1 + anno.offsetX, // x1
0 + anno.offsetY, // y1
x1 + anno.offsetX, // x2
w.globals.gridHeight + anno.offsetY, // y2
anno.borderColor, // lineColor
strokeDashArray, //dashArray
anno.borderWidth
)
parent.appendChild(line.node)
if (anno.id) {
line.node.classList.add(anno.id)
}
}
} else {
let result = this.helpers.getX1X2('x2', anno)
x2 = result.x
clipX2 = result.clipped
if (x2 < x1) {
let temp = x1
x1 = x2
x2 = temp
}
let rect = this.annoCtx.graphics.drawRect(
x1 + anno.offsetX, // x1
0 + anno.offsetY, // y1
x2 - x1, // x2
w.globals.gridHeight + anno.offsetY, // y2
0, // radius
anno.fillColor, // color
anno.opacity, // opacity,
1, // strokeWidth
anno.borderColor, // strokeColor
strokeDashArray // stokeDashArray
)
rect.node.classList.add('apexcharts-annotation-rect')
rect.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
parent.appendChild(rect.node)
if (anno.id) {
rect.node.classList.add(anno.id)
}
}
if (!(clipX1 && clipX2)) {
let textRects = this.annoCtx.graphics.getTextRects(
text,
parseFloat(anno.label.style.fontSize)
)
let textY =
anno.label.position === 'top'
? 4
: anno.label.position === 'center'
? w.globals.gridHeight / 2 +
(anno.label.orientation === 'vertical' ? textRects.width / 2 : 0)
: w.globals.gridHeight
let elText = this.annoCtx.graphics.drawText({
x: x1 + anno.label.offsetX,
y:
textY +
anno.label.offsetY -
(anno.label.orientation === 'vertical'
? anno.label.position === 'top'
? textRects.width / 2 - 12
: -textRects.width / 2
: 0),
text,
textAnchor: anno.label.textAnchor,
fontSize: anno.label.style.fontSize,
fontFamily: anno.label.style.fontFamily,
fontWeight: anno.label.style.fontWeight,
foreColor: anno.label.style.color,
cssClass: `apexcharts-xaxis-annotation-label ${
anno.label.style.cssClass
} ${anno.id ? anno.id : ''}`,
})
elText.attr({
rel: index,
})
parent.appendChild(elText.node)
// after placing the annotations on svg, set any vertically placed annotations
this.annoCtx.helpers.setOrientations(anno, index)
}
}
drawXAxisAnnotations() {
let w = this.w
let elg = this.annoCtx.graphics.group({
class: 'apexcharts-xaxis-annotations',
})
w.config.annotations.xaxis.map((anno, index) => {
this.addXaxisAnnotation(anno, elg.node, index)
})
return elg
}
}

View File

@@ -0,0 +1,140 @@
import Helpers from './Helpers'
import AxesUtils from '../axes/AxesUtils'
export default class YAnnotations {
constructor(annoCtx) {
this.w = annoCtx.w
this.annoCtx = annoCtx
this.helpers = new Helpers(this.annoCtx)
this.axesUtils = new AxesUtils(this.annoCtx)
}
addYaxisAnnotation(anno, parent, index) {
let w = this.w
let strokeDashArray = anno.strokeDashArray
let result = this.helpers.getY1Y2('y1', anno)
let y1 = result.yP
let clipY1 = result.clipped
let y2
let clipY2 = true
let drawn = false
const text = anno.label.text
if (anno.y2 === null || typeof anno.y2 === 'undefined') {
if (!clipY1) {
drawn = true
let line = this.annoCtx.graphics.drawLine(
0 + anno.offsetX, // x1
y1 + anno.offsetY, // y1
this._getYAxisAnnotationWidth(anno), // x2
y1 + anno.offsetY, // y2
anno.borderColor, // lineColor
strokeDashArray, // dashArray
anno.borderWidth
)
parent.appendChild(line.node)
if (anno.id) {
line.node.classList.add(anno.id)
}
}
} else {
result = this.helpers.getY1Y2('y2', anno)
y2 = result.yP
clipY2 = result.clipped
if (y2 > y1) {
let temp = y1
y1 = y2
y2 = temp
}
if (!(clipY1 && clipY2)) {
drawn = true
let rect = this.annoCtx.graphics.drawRect(
0 + anno.offsetX, // x1
y2 + anno.offsetY, // y1
this._getYAxisAnnotationWidth(anno), // x2
y1 - y2, // y2
0, // radius
anno.fillColor, // color
anno.opacity, // opacity,
1, // strokeWidth
anno.borderColor, // strokeColor
strokeDashArray // stokeDashArray
)
rect.node.classList.add('apexcharts-annotation-rect')
rect.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
parent.appendChild(rect.node)
if (anno.id) {
rect.node.classList.add(anno.id)
}
}
}
if (drawn) {
let textX =
anno.label.position === 'right'
? w.globals.gridWidth
: anno.label.position === 'center'
? w.globals.gridWidth / 2
: 0
let elText = this.annoCtx.graphics.drawText({
x: textX + anno.label.offsetX,
y: (y2 != null ? y2 : y1) + anno.label.offsetY - 3,
text,
textAnchor: anno.label.textAnchor,
fontSize: anno.label.style.fontSize,
fontFamily: anno.label.style.fontFamily,
fontWeight: anno.label.style.fontWeight,
foreColor: anno.label.style.color,
cssClass: `apexcharts-yaxis-annotation-label ${
anno.label.style.cssClass
} ${anno.id ? anno.id : ''}`
})
elText.attr({
rel: index
})
parent.appendChild(elText.node)
}
}
_getYAxisAnnotationWidth(anno) {
// issue apexcharts.js#2009
const w = this.w
let width = w.globals.gridWidth
if (anno.width.indexOf('%') > -1) {
width = (w.globals.gridWidth * parseInt(anno.width, 10)) / 100
} else {
width = parseInt(anno.width, 10)
}
return width + anno.offsetX
}
drawYAxisAnnotations() {
const w = this.w
let elg = this.annoCtx.graphics.group({
class: 'apexcharts-yaxis-annotations'
})
w.config.annotations.yaxis.forEach((anno, index) => {
anno.yAxisIndex = this.axesUtils.translateYAxisIndex(anno.yAxisIndex)
if (
!(this.axesUtils.isYAxisHidden(anno.yAxisIndex)
&& this.axesUtils.yAxisAllSeriesCollapsed(anno.yAxisIndex))
) {
this.addYaxisAnnotation(anno, elg.node, index)
}
})
return elg
}
}

View File

@@ -0,0 +1,45 @@
import XAxis from './XAxis'
import YAxis from './YAxis'
export default class Axes {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
drawAxis(type, elgrid) {
let gl = this.w.globals
let cnf = this.w.config
let xAxis = new XAxis(this.ctx, elgrid)
let yAxis = new YAxis(this.ctx, elgrid)
if (gl.axisCharts && type !== 'radar') {
let elXaxis, elYaxis
if (gl.isBarHorizontal) {
elYaxis = yAxis.drawYaxisInversed(0)
elXaxis = xAxis.drawXaxisInversed(0)
gl.dom.elGraphical.add(elXaxis)
gl.dom.elGraphical.add(elYaxis)
} else {
elXaxis = xAxis.drawXaxis()
gl.dom.elGraphical.add(elXaxis)
cnf.yaxis.map((yaxe, index) => {
if (gl.ignoreYAxisIndexes.indexOf(index) === -1) {
elYaxis = yAxis.drawYaxis(index)
gl.dom.Paper.add(elYaxis)
if (this.w.config.grid.position === 'back') {
const inner = gl.dom.Paper.children()[1]
inner.remove()
gl.dom.Paper.add(inner)
}
}
})
}
}
}
}

View File

@@ -0,0 +1,271 @@
import Formatters from '../Formatters'
import Graphics from '../Graphics'
import CoreUtils from '../CoreUtils'
import DateTime from '../../utils/DateTime'
export default class AxesUtils {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
// Based on the formatter function, get the label text and position
getLabel(
labels,
timescaleLabels,
x,
i,
drawnLabels = [],
fontSize = '12px',
isLeafGroup = true
) {
const w = this.w
let rawLabel = typeof labels[i] === 'undefined' ? '' : labels[i]
let label = rawLabel
let xlbFormatter = w.globals.xLabelFormatter
let customFormatter = w.config.xaxis.labels.formatter
let isBold = false
let xFormat = new Formatters(this.ctx)
let timestamp = rawLabel
if (isLeafGroup) {
label = xFormat.xLabelFormat(xlbFormatter, rawLabel, timestamp, {
i,
dateFormatter: new DateTime(this.ctx).formatDate,
w,
})
if (customFormatter !== undefined) {
label = customFormatter(rawLabel, labels[i], {
i,
dateFormatter: new DateTime(this.ctx).formatDate,
w,
})
}
}
const determineHighestUnit = (unit) => {
let highestUnit = null
timescaleLabels.forEach((t) => {
if (t.unit === 'month') {
highestUnit = 'year'
} else if (t.unit === 'day') {
highestUnit = 'month'
} else if (t.unit === 'hour') {
highestUnit = 'day'
} else if (t.unit === 'minute') {
highestUnit = 'hour'
}
})
return highestUnit === unit
}
if (timescaleLabels.length > 0) {
isBold = determineHighestUnit(timescaleLabels[i].unit)
x = timescaleLabels[i].position
label = timescaleLabels[i].value
} else {
if (w.config.xaxis.type === 'datetime' && customFormatter === undefined) {
label = ''
}
}
if (typeof label === 'undefined') label = ''
label = Array.isArray(label) ? label : label.toString()
let graphics = new Graphics(this.ctx)
let textRect = {}
if (w.globals.rotateXLabels && isLeafGroup) {
textRect = graphics.getTextRects(
label,
parseInt(fontSize, 10),
null,
`rotate(${w.config.xaxis.labels.rotate} 0 0)`,
false
)
} else {
textRect = graphics.getTextRects(label, parseInt(fontSize, 10))
}
const allowDuplicatesInTimeScale =
!w.config.xaxis.labels.showDuplicates && this.ctx.timeScale
if (
!Array.isArray(label) &&
(String(label) === 'NaN' ||
(drawnLabels.indexOf(label) >= 0 && allowDuplicatesInTimeScale))
) {
label = ''
}
return {
x,
text: label,
textRect,
isBold,
}
}
checkLabelBasedOnTickamount(i, label, labelsLen) {
const w = this.w
let ticks = w.config.xaxis.tickAmount
if (ticks === 'dataPoints') ticks = Math.round(w.globals.gridWidth / 120)
if (ticks > labelsLen) return label
let tickMultiple = Math.round(labelsLen / (ticks + 1))
if (i % tickMultiple === 0) {
return label
} else {
label.text = ''
}
return label
}
checkForOverflowingLabels(
i,
label,
labelsLen,
drawnLabels,
drawnLabelsRects
) {
const w = this.w
if (i === 0) {
// check if first label is being truncated
if (w.globals.skipFirstTimelinelabel) {
label.text = ''
}
}
if (i === labelsLen - 1) {
// check if last label is being truncated
if (w.globals.skipLastTimelinelabel) {
label.text = ''
}
}
if (w.config.xaxis.labels.hideOverlappingLabels && drawnLabels.length > 0) {
const prev = drawnLabelsRects[drawnLabelsRects.length - 1]
if (
label.x <
prev.textRect.width /
(w.globals.rotateXLabels
? Math.abs(w.config.xaxis.labels.rotate) / 12
: 1.01) +
prev.x
) {
label.text = ''
}
}
return label
}
checkForReversedLabels(i, labels) {
const w = this.w
if (w.config.yaxis[i] && w.config.yaxis[i].reversed) {
labels.reverse()
}
return labels
}
yAxisAllSeriesCollapsed(index) {
const gl = this.w.globals
return !gl.seriesYAxisMap[index].some((si) => {
return gl.collapsedSeriesIndices.indexOf(si) === -1
})
}
// Method to translate annotation.yAxisIndex values from
// seriesName-as-a-string values to seriesName-as-an-array values (old style
// series mapping to new style).
translateYAxisIndex(index) {
const w = this.w
const gl = w.globals
const yaxis = w.config.yaxis
let newStyle =
gl.series.length > yaxis.length
|| yaxis.some((a) => Array.isArray(a.seriesName))
if (newStyle) {
return index
} else {
return gl.seriesYAxisReverseMap[index]
}
}
isYAxisHidden(index) {
const w = this.w
const yaxis = w.config.yaxis[index]
if (!yaxis.show || this.yAxisAllSeriesCollapsed(index)
) {
return true
}
if (!yaxis.showForNullSeries) {
const seriesIndices = w.globals.seriesYAxisMap[index]
const coreUtils = new CoreUtils(this.ctx)
return seriesIndices.every((si) => coreUtils.isSeriesNull(si))
}
return false
}
// get the label color for y-axis
// realIndex is the actual series index, while i is the tick Index
getYAxisForeColor(yColors, realIndex) {
const w = this.w
if (Array.isArray(yColors) && w.globals.yAxisScale[realIndex]) {
this.ctx.theme.pushExtraColors(
yColors,
w.globals.yAxisScale[realIndex].result.length,
false
)
}
return yColors
}
drawYAxisTicks(
x,
tickAmount,
axisBorder,
axisTicks,
realIndex,
labelsDivider,
elYaxis
) {
let w = this.w
let graphics = new Graphics(this.ctx)
// initial label position = 0;
let tY = w.globals.translateY + w.config.yaxis[realIndex].labels.offsetY
if (w.globals.isBarHorizontal) {
tY = 0
} else if (w.config.chart.type === 'heatmap') {
tY += labelsDivider / 2
}
if (axisTicks.show && tickAmount > 0) {
if (w.config.yaxis[realIndex].opposite === true) x = x + axisTicks.width
for (let i = tickAmount; i >= 0; i--) {
let elTick = graphics.drawLine(
x + axisBorder.offsetX - axisTicks.width + axisTicks.offsetX,
tY + axisTicks.offsetY,
x + axisBorder.offsetX + axisTicks.offsetX,
tY + axisTicks.offsetY,
axisTicks.color
)
elYaxis.add(elTick)
tY += labelsDivider
}
}
}
}

View File

@@ -0,0 +1,512 @@
import Graphics from '../Graphics'
import XAxis from './XAxis'
import AxesUtils from './AxesUtils'
/**
* ApexCharts Grid Class for drawing Cartesian Grid.
*
* @module Grid
**/
class Grid {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
const w = this.w
this.xaxisLabels = w.globals.labels.slice()
this.axesUtils = new AxesUtils(ctx)
this.isRangeBar = w.globals.seriesRange.length && w.globals.isBarHorizontal
if (w.globals.timescaleLabels.length > 0) {
// timescaleLabels labels are there
this.xaxisLabels = w.globals.timescaleLabels.slice()
}
}
drawGridArea(elGrid = null) {
const w = this.w
const graphics = new Graphics(this.ctx)
if (!elGrid) {
elGrid = graphics.group({ class: 'apexcharts-grid' })
}
const elVerticalLine = graphics.drawLine(
w.globals.padHorizontal,
1,
w.globals.padHorizontal,
w.globals.gridHeight,
'transparent'
)
const elHorzLine = graphics.drawLine(
w.globals.padHorizontal,
w.globals.gridHeight,
w.globals.gridWidth,
w.globals.gridHeight,
'transparent'
)
elGrid.add(elHorzLine)
elGrid.add(elVerticalLine)
return elGrid
}
drawGrid() {
const gl = this.w.globals
if (gl.axisCharts) {
const elgrid = this.renderGrid()
this.drawGridArea(elgrid.el)
return elgrid
}
return null
}
createGridMask() {
const w = this.w
const gl = w.globals
const graphics = new Graphics(this.ctx)
const strokeSize = Array.isArray(w.config.stroke.width)
? Math.max(...w.config.stroke.width)
: w.config.stroke.width
const createClipPath = (id) => {
const clipPath = document.createElementNS(gl.SVGNS, 'clipPath')
clipPath.setAttribute('id', id)
return clipPath
}
gl.dom.elGridRectMask = createClipPath(`gridRectMask${gl.cuid}`)
gl.dom.elGridRectBarMask = createClipPath(`gridRectBarMask${gl.cuid}`)
gl.dom.elGridRectMarkerMask = createClipPath(`gridRectMarkerMask${gl.cuid}`)
gl.dom.elForecastMask = createClipPath(`forecastMask${gl.cuid}`)
gl.dom.elNonForecastMask = createClipPath(`nonForecastMask${gl.cuid}`)
const hasBar =
['bar', 'rangeBar', 'candlestick', 'boxPlot'].includes(
w.config.chart.type
) || w.globals.comboBarCount > 0
let barWidthLeft = 0
let barWidthRight = 0
if (hasBar && w.globals.isXNumeric && !w.globals.isBarHorizontal) {
barWidthLeft = Math.max(
w.config.grid.padding.left,
gl.barPadForNumericAxis
)
barWidthRight = Math.max(
w.config.grid.padding.right,
gl.barPadForNumericAxis
)
}
gl.dom.elGridRect = graphics.drawRect(
-strokeSize / 2 - 2,
-strokeSize / 2 - 2,
gl.gridWidth + strokeSize + 4,
gl.gridHeight + strokeSize + 4,
0,
'#fff'
)
gl.dom.elGridRectBar = graphics.drawRect(
-strokeSize / 2 - barWidthLeft - 2,
-strokeSize / 2 - 2,
gl.gridWidth + strokeSize + barWidthRight + barWidthLeft + 4,
gl.gridHeight + strokeSize + 4,
0,
'#fff'
)
const markerSize = w.globals.markers.largestSize
gl.dom.elGridRectMarker = graphics.drawRect(
-markerSize,
-markerSize,
gl.gridWidth + markerSize * 2,
gl.gridHeight + markerSize * 2,
0,
'#fff'
)
gl.dom.elGridRectMask.appendChild(gl.dom.elGridRect.node)
gl.dom.elGridRectBarMask.appendChild(gl.dom.elGridRectBar.node)
gl.dom.elGridRectMarkerMask.appendChild(gl.dom.elGridRectMarker.node)
const defs = gl.dom.baseEl.querySelector('defs')
defs.appendChild(gl.dom.elGridRectMask)
defs.appendChild(gl.dom.elGridRectBarMask)
defs.appendChild(gl.dom.elGridRectMarkerMask)
defs.appendChild(gl.dom.elForecastMask)
defs.appendChild(gl.dom.elNonForecastMask)
}
_drawGridLines({ i, x1, y1, x2, y2, xCount, parent }) {
const w = this.w
const shouldDraw = () => {
if (i === 0 && w.globals.skipFirstTimelinelabel) return false
if (
i === xCount - 1 &&
w.globals.skipLastTimelinelabel &&
!w.config.xaxis.labels.formatter
)
return false
if (w.config.chart.type === 'radar') return false
return true
}
if (shouldDraw()) {
if (w.config.grid.xaxis.lines.show) {
this._drawGridLine({ i, x1, y1, x2, y2, xCount, parent })
}
let y_2 = 0
if (
w.globals.hasXaxisGroups &&
w.config.xaxis.tickPlacement === 'between'
) {
const groups = w.globals.groups
if (groups) {
let gacc = 0
for (let gi = 0; gacc < i && gi < groups.length; gi++) {
gacc += groups[gi].cols
}
if (gacc === i) {
y_2 = w.globals.xAxisLabelsHeight * 0.6
}
}
}
const xAxis = new XAxis(this.ctx)
xAxis.drawXaxisTicks(x1, y_2, w.globals.dom.elGraphical)
}
}
_drawGridLine({ i, x1, y1, x2, y2, xCount, parent }) {
const w = this.w
const isHorzLine = parent.node.classList.contains(
'apexcharts-gridlines-horizontal'
)
const offX = w.globals.barPadForNumericAxis
const excludeBorders =
(y1 === 0 && y2 === 0) ||
(x1 === 0 && x2 === 0) ||
(y1 === w.globals.gridHeight && y2 === w.globals.gridHeight) ||
(w.globals.isBarHorizontal && (i === 0 || i === xCount - 1))
const graphics = new Graphics(this)
const line = graphics.drawLine(
x1 - (isHorzLine ? offX : 0),
y1,
x2 + (isHorzLine ? offX : 0),
y2,
w.config.grid.borderColor,
w.config.grid.strokeDashArray
)
line.node.classList.add('apexcharts-gridline')
if (excludeBorders && w.config.grid.show) {
this.elGridBorders.add(line)
} else {
parent.add(line)
}
}
_drawGridBandRect({ c, x1, y1, x2, y2, type }) {
const w = this.w
const graphics = new Graphics(this.ctx)
const offX = w.globals.barPadForNumericAxis
const color = w.config.grid[type].colors[c]
const rect = graphics.drawRect(
x1 - (type === 'row' ? offX : 0),
y1,
x2 + (type === 'row' ? offX * 2 : 0),
y2,
0,
color,
w.config.grid[type].opacity
)
this.elg.add(rect)
rect.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
rect.node.classList.add(`apexcharts-grid-${type}`)
}
_drawXYLines({ xCount, tickAmount }) {
const w = this.w
const datetimeLines = ({ xC, x1, y1, x2, y2 }) => {
for (let i = 0; i < xC; i++) {
x1 = this.xaxisLabels[i].position
x2 = this.xaxisLabels[i].position
this._drawGridLines({
i,
x1,
y1,
x2,
y2,
xCount,
parent: this.elgridLinesV,
})
}
}
const categoryLines = ({ xC, x1, y1, x2, y2 }) => {
for (let i = 0; i < xC + (w.globals.isXNumeric ? 0 : 1); i++) {
if (i === 0 && xC === 1 && w.globals.dataPoints === 1) {
x1 = w.globals.gridWidth / 2
x2 = x1
}
this._drawGridLines({
i,
x1,
y1,
x2,
y2,
xCount,
parent: this.elgridLinesV,
})
x1 += w.globals.gridWidth / (w.globals.isXNumeric ? xC - 1 : xC)
x2 = x1
}
}
if (w.config.grid.xaxis.lines.show || w.config.xaxis.axisTicks.show) {
let x1 = w.globals.padHorizontal
let y1 = 0
let x2
let y2 = w.globals.gridHeight
if (w.globals.timescaleLabels.length) {
datetimeLines({ xC: xCount, x1, y1, x2, y2 })
} else {
if (w.globals.isXNumeric) {
xCount = w.globals.xAxisScale.result.length
}
categoryLines({ xC: xCount, x1, y1, x2, y2 })
}
}
if (w.config.grid.yaxis.lines.show) {
let x1 = 0
let y1 = 0
let y2 = 0
let x2 = w.globals.gridWidth
let tA = tickAmount + 1
if (this.isRangeBar) {
tA = w.globals.labels.length
}
for (let i = 0; i < tA + (this.isRangeBar ? 1 : 0); i++) {
this._drawGridLine({
i,
xCount: tA + (this.isRangeBar ? 1 : 0),
x1,
y1,
x2,
y2,
parent: this.elgridLinesH,
})
y1 += w.globals.gridHeight / (this.isRangeBar ? tA : tickAmount)
y2 = y1
}
}
}
_drawInvertedXYLines({ xCount }) {
const w = this.w
if (w.config.grid.xaxis.lines.show || w.config.xaxis.axisTicks.show) {
let x1 = w.globals.padHorizontal
let y1 = 0
let x2
let y2 = w.globals.gridHeight
for (let i = 0; i < xCount + 1; i++) {
if (w.config.grid.xaxis.lines.show) {
this._drawGridLine({
i,
xCount: xCount + 1,
x1,
y1,
x2,
y2,
parent: this.elgridLinesV,
})
}
const xAxis = new XAxis(this.ctx)
xAxis.drawXaxisTicks(x1, 0, w.globals.dom.elGraphical)
x1 += w.globals.gridWidth / xCount
x2 = x1
}
}
if (w.config.grid.yaxis.lines.show) {
let x1 = 0
let y1 = 0
let y2 = 0
let x2 = w.globals.gridWidth
for (let i = 0; i < w.globals.dataPoints + 1; i++) {
this._drawGridLine({
i,
xCount: w.globals.dataPoints + 1,
x1,
y1,
x2,
y2,
parent: this.elgridLinesH,
})
y1 += w.globals.gridHeight / w.globals.dataPoints
y2 = y1
}
}
}
renderGrid() {
const w = this.w
const gl = w.globals
const graphics = new Graphics(this.ctx)
this.elg = graphics.group({ class: 'apexcharts-grid' })
this.elgridLinesH = graphics.group({
class: 'apexcharts-gridlines-horizontal',
})
this.elgridLinesV = graphics.group({
class: 'apexcharts-gridlines-vertical',
})
this.elGridBorders = graphics.group({ class: 'apexcharts-grid-borders' })
this.elg.add(this.elgridLinesH)
this.elg.add(this.elgridLinesV)
if (!w.config.grid.show) {
this.elgridLinesV.hide()
this.elgridLinesH.hide()
this.elGridBorders.hide()
}
let gridAxisIndex = 0
while (
gridAxisIndex < gl.seriesYAxisMap.length &&
gl.ignoreYAxisIndexes.includes(gridAxisIndex)
) {
gridAxisIndex++
}
if (gridAxisIndex === gl.seriesYAxisMap.length) {
gridAxisIndex = 0
}
let yTickAmount = gl.yAxisScale[gridAxisIndex].result.length - 1
let xCount
if (!gl.isBarHorizontal || this.isRangeBar) {
xCount = this.xaxisLabels.length
if (this.isRangeBar) {
yTickAmount = gl.labels.length
if (w.config.xaxis.tickAmount && w.config.xaxis.labels.formatter) {
xCount = w.config.xaxis.tickAmount
}
if (
gl.yAxisScale?.[gridAxisIndex]?.result?.length > 0 &&
w.config.xaxis.type !== 'datetime'
) {
xCount = gl.yAxisScale[gridAxisIndex].result.length - 1
}
}
this._drawXYLines({ xCount, tickAmount: yTickAmount })
} else {
xCount = yTickAmount
// for horizontal bar chart, get the xaxis tickamount
yTickAmount = gl.xTickAmount
this._drawInvertedXYLines({ xCount, tickAmount: yTickAmount })
}
this.drawGridBands(xCount, yTickAmount)
return {
el: this.elg,
elGridBorders: this.elGridBorders,
xAxisTickWidth: gl.gridWidth / xCount,
}
}
drawGridBands(xCount, tickAmount) {
const w = this.w
const drawBands = (type, count, x1, y1, x2, y2) => {
for (let i = 0, c = 0; i < count; i++, c++) {
if (c >= w.config.grid[type].colors.length) {
c = 0
}
this._drawGridBandRect({ c, x1, y1, x2, y2, type })
y1 += w.globals.gridHeight / tickAmount
}
}
if (w.config.grid.row.colors?.length > 0) {
drawBands(
'row',
tickAmount,
0,
0,
w.globals.gridWidth,
w.globals.gridHeight / tickAmount
)
}
if (w.config.grid.column.colors?.length > 0) {
let xc =
!w.globals.isBarHorizontal &&
w.config.xaxis.tickPlacement === 'on' &&
(w.config.xaxis.type === 'category' ||
w.config.xaxis.convertedCatToNumeric)
? xCount - 1
: xCount
if (w.globals.isXNumeric) {
xc = w.globals.xAxisScale.result.length - 1
}
let x1 = w.globals.padHorizontal
let y1 = 0
let x2 = w.globals.padHorizontal + w.globals.gridWidth / xc
let y2 = w.globals.gridHeight
for (let i = 0, c = 0; i < xCount; i++, c++) {
if (c >= w.config.grid.column.colors.length) {
c = 0
}
if (w.config.xaxis.type === 'datetime') {
x1 = this.xaxisLabels[i].position
x2 =
(this.xaxisLabels[i + 1]?.position || w.globals.gridWidth) -
this.xaxisLabels[i].position
}
this._drawGridBandRect({ c, x1, y1, x2, y2, type: 'column' })
x1 += w.globals.gridWidth / xc
}
}
}
}
export default Grid

View File

@@ -0,0 +1,686 @@
import Graphics from '../Graphics'
import AxesUtils from './AxesUtils'
/**
* ApexCharts XAxis Class for drawing X-Axis.
*
* @module XAxis
**/
export default class XAxis {
constructor(ctx, elgrid) {
this.ctx = ctx
this.elgrid = elgrid
this.w = ctx.w
const w = this.w
this.axesUtils = new AxesUtils(ctx)
this.xaxisLabels = w.globals.labels.slice()
if (w.globals.timescaleLabels.length > 0 && !w.globals.isBarHorizontal) {
// timeline labels are there and chart is not rangeabr timeline
this.xaxisLabels = w.globals.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.globals.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 (this.xaxisBorderWidth.indexOf('%') > -1) {
this.xaxisBorderWidth =
(w.globals.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() {
let w = this.w
let graphics = new Graphics(this.ctx)
let elXaxis = graphics.group({
class: 'apexcharts-xaxis',
transform: `translate(${w.config.xaxis.offsetX}, ${w.config.xaxis.offsetY})`,
})
let elXaxisTexts = graphics.group({
class: 'apexcharts-xaxis-texts-g',
transform: `translate(${w.globals.translateXAxisX}, ${w.globals.translateXAxisY})`,
})
elXaxis.add(elXaxisTexts)
let labels = []
for (let i = 0; i < this.xaxisLabels.length; i++) {
labels.push(this.xaxisLabels[i])
}
this.drawXAxisLabelAndGroup(
true,
graphics,
elXaxisTexts,
labels,
w.globals.isXNumeric,
(i, colWidth) => colWidth
)
if (w.globals.hasXaxisGroups) {
let labelsGroup = w.globals.groups
labels = []
for (let i = 0; i < labelsGroup.length; i++) {
labels.push(labelsGroup[i].title)
}
let overwriteStyles = {}
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
}
this.drawXAxisLabelAndGroup(
false,
graphics,
elXaxisTexts,
labels,
false,
(i, colWidth) => labelsGroup[i].cols * colWidth,
overwriteStyles
)
}
if (w.config.xaxis.title.text !== undefined) {
let elXaxisTitle = graphics.group({
class: 'apexcharts-xaxis-title',
})
let elXAxisTitleText = graphics.drawText({
x: w.globals.gridWidth / 2 + w.config.xaxis.title.offsetX,
y:
this.offY +
parseFloat(this.xaxisFontSize) +
(w.config.xaxis.position === 'bottom'
? w.globals.xAxisLabelsHeight
: -w.globals.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
let 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
}
drawXAxisLabelAndGroup(
isLeafGroup,
graphics,
elXaxisTexts,
labels,
isXNumeric,
colWidthCb,
overwriteStyles = {}
) {
let drawnLabels = []
let drawnLabelsRects = []
let 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
let 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) {
let len = Math.max(
Number(w.config.xaxis.tickAmount) || 1,
dataPoints > 1 ? dataPoints - 1 : dataPoints
)
colWidth = w.globals.gridWidth / Math.min(len, labelsLen - 1)
xPos = xPos + colWidthCb(0, colWidth) / 2 + w.config.xaxis.labels.offsetX
} else {
colWidth = w.globals.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.globals.gridWidth / 2
}
let label = this.axesUtils.getLabel(
labels,
w.globals.timescaleLabels,
x,
i,
drawnLabels,
xaxisFontSize,
isLeafGroup
)
let offsetYCorrection = 28
if (w.globals.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.globals.xAxisLabelsHeight - w.globals.xAxisGroupLabelsHeight) +
(w.globals.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) {
let elText = graphics.drawText({
x: label.x,
y:
this.offY +
w.config.xaxis.labels.offsetY +
offsetYCorrection -
(w.config.xaxis.position === 'top'
? w.globals.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)
elText.on('click', (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) {
let elTooltipTitle = document.createElementNS(
w.globals.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)
drawXaxisInversed(realIndex) {
let w = this.w
let graphics = new Graphics(this.ctx)
let translateYAxisX = w.config.yaxis[0].opposite
? w.globals.translateYAxisX[realIndex]
: 0
let elYaxis = graphics.group({
class: 'apexcharts-yaxis apexcharts-xaxis-inversed',
rel: realIndex,
})
let elYaxisTexts = graphics.group({
class: 'apexcharts-yaxis-texts-g apexcharts-xaxis-inversed-texts-g',
transform: 'translate(' + translateYAxisX + ', 0)',
})
elYaxis.add(elYaxisTexts)
let colHeight
// initial x Position (keep adding column width in the loop)
let yPos
let labels = []
if (w.config.yaxis[realIndex].show) {
for (let i = 0; i < this.xaxisLabels.length; i++) {
labels.push(this.xaxisLabels[i])
}
}
colHeight = w.globals.gridHeight / labels.length
yPos = -(colHeight / 2.2)
let lbFormatter = w.globals.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'
}
let 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)
elLabel.on('click', (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)
}
})
let elTooltipTitle = document.createElementNS(w.globals.SVGNS, 'title')
elTooltipTitle.textContent = Array.isArray(label)
? label.join(' ')
: label
elLabel.node.appendChild(elTooltipTitle)
if (w.config.yaxis[realIndex].labels.rotate !== 0) {
let 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) {
let elXaxisTitle = graphics.group({
class: 'apexcharts-yaxis-title apexcharts-xaxis-title-inversed',
transform: 'translate(' + translateYAxisX + ', 0)',
})
let elXAxisTitleText = graphics.drawText({
x: w.config.yaxis[0].title.offsetX,
y: w.globals.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.globals.gridWidth
}
const axisBorder = w.config.xaxis.axisBorder
if (axisBorder.show) {
let elVerticalLine = graphics.drawLine(
w.globals.padHorizontal + axisBorder.offsetX + offX,
1 + axisBorder.offsetY,
w.globals.padHorizontal + axisBorder.offsetX + offX,
w.globals.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
}
drawXaxisTicks(x1, y2, appendToElement) {
let w = this.w
let x2 = x1
if (x1 < 0 || x1 - 2 > w.globals.gridWidth) return
let 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) {
let graphics = new Graphics(this.ctx)
let 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
let xAxisTicksPositions = []
const xCount = this.xaxisLabels.length
let x1 = w.globals.padHorizontal
if (w.globals.timescaleLabels.length > 0) {
for (let i = 0; i < xCount; i++) {
x1 = this.xaxisLabels[i].position
xAxisTicksPositions.push(x1)
}
} else {
let xCountForCategoryCharts = xCount
for (let i = 0; i < xCountForCategoryCharts; i++) {
let x1Count = xCountForCategoryCharts
if (w.globals.isXNumeric && w.config.chart.type !== 'bar') {
x1Count -= 1
}
x1 = x1 + w.globals.gridWidth / x1Count
xAxisTicksPositions.push(x1)
}
}
return xAxisTicksPositions
}
// to rotate x-axis labels or to put ... for longer text in xaxis
xAxisLabelCorrections() {
let w = this.w
let graphics = new Graphics(this.ctx)
let xAxis = w.globals.dom.baseEl.querySelector('.apexcharts-xaxis-texts-g')
let xAxisTexts = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-xaxis-texts-g text:not(.apexcharts-xaxis-group-label)'
)
let yAxisTextsInversed = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-yaxis-inversed text'
)
let xAxisTextsInversed = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-xaxis-inversed-texts-g text tspan'
)
if (w.globals.rotateXLabels || w.config.xaxis.labels.rotateAlways) {
for (let xat = 0; xat < xAxisTexts.length; xat++) {
let 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`)
let offsetHeight = 10
xAxis.setAttribute('transform', `translate(0, ${-offsetHeight})`)
let tSpan = xAxisTexts[xat].childNodes
if (w.config.xaxis.labels.trim) {
Array.prototype.forEach.call(tSpan, (ts) => {
graphics.placeTextWithEllipsis(
ts,
ts.textContent,
w.globals.xAxisLabelsHeight -
(w.config.legend.position === 'bottom' ? 20 : 10)
)
})
}
}
} else {
let width = w.globals.gridWidth / (w.globals.labels.length + 1)
for (let xat = 0; xat < xAxisTexts.length; xat++) {
let tSpan = xAxisTexts[xat].childNodes
if (w.config.xaxis.labels.trim && w.config.xaxis.type !== 'datetime') {
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)
let firstLabelPosX =
yAxisTextsInversed[yAxisTextsInversed.length - 1].getBBox()
let lastLabelPosX = yAxisTextsInversed[0].getBBox()
if (firstLabelPosX.x < -20) {
yAxisTextsInversed[
yAxisTextsInversed.length - 1
].parentNode.removeChild(
yAxisTextsInversed[yAxisTextsInversed.length - 1]
)
}
if (
lastLabelPosX.x + lastLabelPosX.width > w.globals.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(w.globals.SVGNS, 'rect')
// w.globals.dom.elGraphical.add(plotBand)
// }
}

View File

@@ -0,0 +1,506 @@
import Graphics from '../Graphics'
import Utils from '../../utils/Utils'
import AxesUtils from './AxesUtils'
/**
* ApexCharts YAxis Class for drawing Y-Axis.
*
* @module YAxis
**/
export default class YAxis {
constructor(ctx, elgrid) {
this.ctx = ctx
this.elgrid = elgrid
this.w = ctx.w
const w = this.w
this.xaxisFontSize = w.config.xaxis.labels.style.fontSize
this.axisFontFamily = w.config.xaxis.labels.style.fontFamily
this.xaxisForeColors = w.config.xaxis.labels.style.colors
this.isCategoryBarHorizontal =
w.config.chart.type === 'bar' && w.config.plotOptions.bar.horizontal
this.xAxisoffX =
w.config.xaxis.position === 'bottom' ? w.globals.gridHeight : 0
this.drawnLabels = []
this.axesUtils = new AxesUtils(ctx)
}
drawYaxis(realIndex) {
const w = this.w
const graphics = new Graphics(this.ctx)
const yaxisStyle = w.config.yaxis[realIndex].labels.style
const {
fontSize: yaxisFontSize,
fontFamily: yaxisFontFamily,
fontWeight: yaxisFontWeight,
} = yaxisStyle
const elYaxis = graphics.group({
class: 'apexcharts-yaxis',
rel: realIndex,
transform: `translate(${w.globals.translateYAxisX[realIndex]}, 0)`,
})
if (this.axesUtils.isYAxisHidden(realIndex)) return elYaxis
const elYaxisTexts = graphics.group({ class: 'apexcharts-yaxis-texts-g' })
elYaxis.add(elYaxisTexts)
const tickAmount = w.globals.yAxisScale[realIndex].result.length - 1
const labelsDivider = w.globals.gridHeight / tickAmount
const lbFormatter = w.globals.yLabelFormatters[realIndex]
let labels = this.axesUtils.checkForReversedLabels(
realIndex,
w.globals.yAxisScale[realIndex].result.slice()
)
if (w.config.yaxis[realIndex].labels.show) {
let lY = w.globals.translateY + w.config.yaxis[realIndex].labels.offsetY
if (w.globals.isBarHorizontal) lY = 0
else if (w.config.chart.type === 'heatmap') lY -= labelsDivider / 2
lY += parseInt(yaxisFontSize, 10) / 3
for (let i = tickAmount; i >= 0; i--) {
let val = lbFormatter(labels[i], i, w)
let xPad = w.config.yaxis[realIndex].labels.padding
if (w.config.yaxis[realIndex].opposite && w.config.yaxis.length !== 0)
xPad *= -1
const textAnchor = this.getTextAnchor(
w.config.yaxis[realIndex].labels.align,
w.config.yaxis[realIndex].opposite
)
const yColors = this.axesUtils.getYAxisForeColor(
yaxisStyle.colors,
realIndex
)
const foreColor = Array.isArray(yColors) ? yColors[i] : yColors
const existingYLabels = Utils.listToArray(
w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-yaxis[rel='${realIndex}'] .apexcharts-yaxis-label tspan`
)
).map((label) => label.textContent)
const label = graphics.drawText({
x: xPad,
y: lY,
text:
existingYLabels.includes(val) &&
!w.config.yaxis[realIndex].labels.showDuplicates
? ''
: val,
textAnchor,
fontSize: yaxisFontSize,
fontFamily: yaxisFontFamily,
fontWeight: yaxisFontWeight,
maxWidth: w.config.yaxis[realIndex].labels.maxWidth,
foreColor,
isPlainText: false,
cssClass: `apexcharts-yaxis-label ${yaxisStyle.cssClass}`,
})
elYaxisTexts.add(label)
this.addTooltip(label, val)
if (w.config.yaxis[realIndex].labels.rotate !== 0) {
this.rotateLabel(
graphics,
label,
firstLabel,
w.config.yaxis[realIndex].labels.rotate
)
}
lY += labelsDivider
}
}
this.addYAxisTitle(graphics, elYaxis, realIndex)
this.addAxisBorder(graphics, elYaxis, realIndex, tickAmount, labelsDivider)
return elYaxis
}
getTextAnchor(align, opposite) {
if (align === 'left') return 'start'
if (align === 'center') return 'middle'
if (align === 'right') return 'end'
return opposite ? 'start' : 'end'
}
addTooltip(label, val) {
const elTooltipTitle = document.createElementNS(
this.w.globals.SVGNS,
'title'
)
elTooltipTitle.textContent = Array.isArray(val) ? val.join(' ') : val
label.node.appendChild(elTooltipTitle)
}
rotateLabel(graphics, label, firstLabel, rotate) {
const firstLabelCenter = graphics.rotateAroundCenter(firstLabel.node)
const labelCenter = graphics.rotateAroundCenter(label.node)
label.node.setAttribute(
'transform',
`rotate(${rotate} ${firstLabelCenter.x} ${labelCenter.y})`
)
}
addYAxisTitle(graphics, elYaxis, realIndex) {
const w = this.w
if (w.config.yaxis[realIndex].title.text !== undefined) {
const elYaxisTitle = graphics.group({ class: 'apexcharts-yaxis-title' })
const x = w.config.yaxis[realIndex].opposite
? w.globals.translateYAxisX[realIndex]
: 0
const elYAxisTitleText = graphics.drawText({
x,
y:
w.globals.gridHeight / 2 +
w.globals.translateY +
w.config.yaxis[realIndex].title.offsetY,
text: w.config.yaxis[realIndex].title.text,
textAnchor: 'end',
foreColor: w.config.yaxis[realIndex].title.style.color,
fontSize: w.config.yaxis[realIndex].title.style.fontSize,
fontWeight: w.config.yaxis[realIndex].title.style.fontWeight,
fontFamily: w.config.yaxis[realIndex].title.style.fontFamily,
cssClass: `apexcharts-yaxis-title-text ${w.config.yaxis[realIndex].title.style.cssClass}`,
})
elYaxisTitle.add(elYAxisTitleText)
elYaxis.add(elYaxisTitle)
}
}
addAxisBorder(graphics, elYaxis, realIndex, tickAmount, labelsDivider) {
const w = this.w
const axisBorder = w.config.yaxis[realIndex].axisBorder
let x = 31 + axisBorder.offsetX
if (w.config.yaxis[realIndex].opposite) x = -31 - axisBorder.offsetX
if (axisBorder.show) {
const elVerticalLine = graphics.drawLine(
x,
w.globals.translateY + axisBorder.offsetY - 2,
x,
w.globals.gridHeight + w.globals.translateY + axisBorder.offsetY + 2,
axisBorder.color,
0,
axisBorder.width
)
elYaxis.add(elVerticalLine)
}
if (w.config.yaxis[realIndex].axisTicks.show) {
this.axesUtils.drawYAxisTicks(
x,
tickAmount,
axisBorder,
w.config.yaxis[realIndex].axisTicks,
realIndex,
labelsDivider,
elYaxis
)
}
}
drawYaxisInversed(realIndex) {
const w = this.w
const graphics = new Graphics(this.ctx)
const elXaxis = graphics.group({
class: 'apexcharts-xaxis apexcharts-yaxis-inversed',
})
const elXaxisTexts = graphics.group({
class: 'apexcharts-xaxis-texts-g',
transform: `translate(${w.globals.translateXAxisX}, ${w.globals.translateXAxisY})`,
})
elXaxis.add(elXaxisTexts)
let tickAmount = w.globals.yAxisScale[realIndex].result.length - 1
const labelsDivider = w.globals.gridWidth / tickAmount + 0.1
let l = labelsDivider + w.config.xaxis.labels.offsetX
const lbFormatter = w.globals.xLabelFormatter
let labels = this.axesUtils.checkForReversedLabels(
realIndex,
w.globals.yAxisScale[realIndex].result.slice()
)
const timescaleLabels = w.globals.timescaleLabels
if (timescaleLabels.length > 0) {
this.xaxisLabels = timescaleLabels.slice()
labels = timescaleLabels.slice()
tickAmount = labels.length
}
if (w.config.xaxis.labels.show) {
for (
let i = timescaleLabels.length ? 0 : tickAmount;
timescaleLabels.length ? i < timescaleLabels.length : i >= 0;
timescaleLabels.length ? i++ : i--
) {
let val = lbFormatter(labels[i], i, w)
let x =
w.globals.gridWidth +
w.globals.padHorizontal -
(l - labelsDivider + w.config.xaxis.labels.offsetX)
if (timescaleLabels.length) {
const label = this.axesUtils.getLabel(
labels,
timescaleLabels,
x,
i,
this.drawnLabels,
this.xaxisFontSize
)
x = label.x
val = label.text
this.drawnLabels.push(label.text)
if (i === 0 && w.globals.skipFirstTimelinelabel) val = ''
if (i === labels.length - 1 && w.globals.skipLastTimelinelabel)
val = ''
}
const elTick = graphics.drawText({
x,
y:
this.xAxisoffX +
w.config.xaxis.labels.offsetY +
30 -
(w.config.xaxis.position === 'top'
? w.globals.xAxisHeight + w.config.xaxis.axisTicks.height - 2
: 0),
text: val,
textAnchor: 'middle',
foreColor: Array.isArray(this.xaxisForeColors)
? this.xaxisForeColors[realIndex]
: this.xaxisForeColors,
fontSize: this.xaxisFontSize,
fontFamily: this.xaxisFontFamily,
fontWeight: w.config.xaxis.labels.style.fontWeight,
isPlainText: false,
cssClass: `apexcharts-xaxis-label ${w.config.xaxis.labels.style.cssClass}`,
})
elXaxisTexts.add(elTick)
elTick.tspan(val)
this.addTooltip(elTick, val)
l += labelsDivider
}
}
this.inversedYAxisTitleText(elXaxis)
this.inversedYAxisBorder(elXaxis)
return elXaxis
}
inversedYAxisBorder(parent) {
const w = this.w
const graphics = new Graphics(this.ctx)
const axisBorder = w.config.xaxis.axisBorder
if (axisBorder.show) {
let lineCorrection = 0
if (w.config.chart.type === 'bar' && w.globals.isXNumeric)
lineCorrection -= 15
const elHorzLine = graphics.drawLine(
w.globals.padHorizontal + lineCorrection + axisBorder.offsetX,
this.xAxisoffX,
w.globals.gridWidth,
this.xAxisoffX,
axisBorder.color,
0,
axisBorder.height
)
if (this.elgrid && this.elgrid.elGridBorders && w.config.grid.show) {
this.elgrid.elGridBorders.add(elHorzLine)
} else {
parent.add(elHorzLine)
}
}
}
inversedYAxisTitleText(parent) {
const w = this.w
const graphics = new Graphics(this.ctx)
if (w.config.xaxis.title.text !== undefined) {
const elYaxisTitle = graphics.group({
class: 'apexcharts-xaxis-title apexcharts-yaxis-title-inversed',
})
const elYAxisTitleText = graphics.drawText({
x: w.globals.gridWidth / 2 + w.config.xaxis.title.offsetX,
y:
this.xAxisoffX +
parseFloat(this.xaxisFontSize) +
parseFloat(w.config.xaxis.title.style.fontSize) +
w.config.xaxis.title.offsetY +
20,
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}`,
})
elYaxisTitle.add(elYAxisTitleText)
parent.add(elYaxisTitle)
}
}
yAxisTitleRotate(realIndex, yAxisOpposite) {
const w = this.w
const graphics = new Graphics(this.ctx)
const elYAxisLabelsWrap = w.globals.dom.baseEl.querySelector(
`.apexcharts-yaxis[rel='${realIndex}'] .apexcharts-yaxis-texts-g`
)
const yAxisLabelsCoord = elYAxisLabelsWrap
? elYAxisLabelsWrap.getBoundingClientRect()
: { width: 0, height: 0 }
const yAxisTitle = w.globals.dom.baseEl.querySelector(
`.apexcharts-yaxis[rel='${realIndex}'] .apexcharts-yaxis-title text`
)
const yAxisTitleCoord = yAxisTitle
? yAxisTitle.getBoundingClientRect()
: { width: 0, height: 0 }
if (yAxisTitle) {
const x = this.xPaddingForYAxisTitle(
realIndex,
yAxisLabelsCoord,
yAxisTitleCoord,
yAxisOpposite
)
yAxisTitle.setAttribute('x', x.xPos - (yAxisOpposite ? 10 : 0))
const titleRotatingCenter = graphics.rotateAroundCenter(yAxisTitle)
yAxisTitle.setAttribute(
'transform',
`rotate(${
yAxisOpposite
? w.config.yaxis[realIndex].title.rotate * -1
: w.config.yaxis[realIndex].title.rotate
} ${titleRotatingCenter.x} ${titleRotatingCenter.y})`
)
}
}
xPaddingForYAxisTitle(
realIndex,
yAxisLabelsCoord,
yAxisTitleCoord,
yAxisOpposite
) {
const w = this.w
let x = 0
let padd = 10
if (w.config.yaxis[realIndex].title.text === undefined || realIndex < 0) {
return { xPos: x, padd: 0 }
}
if (yAxisOpposite) {
x =
yAxisLabelsCoord.width +
w.config.yaxis[realIndex].title.offsetX +
yAxisTitleCoord.width / 2 +
padd / 2
} else {
x =
yAxisLabelsCoord.width * -1 +
w.config.yaxis[realIndex].title.offsetX +
padd / 2 +
yAxisTitleCoord.width / 2
if (w.globals.isBarHorizontal) {
padd = 25
x =
yAxisLabelsCoord.width * -1 -
w.config.yaxis[realIndex].title.offsetX -
padd
}
}
return { xPos: x, padd }
}
setYAxisXPosition(yaxisLabelCoords, yTitleCoords) {
const w = this.w
let xLeft = 0
let xRight = 0
let leftOffsetX = 18
let rightOffsetX = 1
if (w.config.yaxis.length > 1) this.multipleYs = true
w.config.yaxis.forEach((yaxe, index) => {
const shouldNotDrawAxis =
w.globals.ignoreYAxisIndexes.includes(index) ||
!yaxe.show ||
yaxe.floating ||
yaxisLabelCoords[index].width === 0
const axisWidth =
yaxisLabelCoords[index].width + yTitleCoords[index].width
if (!yaxe.opposite) {
xLeft = w.globals.translateX - leftOffsetX
if (!shouldNotDrawAxis) leftOffsetX += axisWidth + 20
w.globals.translateYAxisX[index] = xLeft + yaxe.labels.offsetX
} else {
if (w.globals.isBarHorizontal) {
xRight = w.globals.gridWidth + w.globals.translateX - 1
w.globals.translateYAxisX[index] = xRight - yaxe.labels.offsetX
} else {
xRight = w.globals.gridWidth + w.globals.translateX + rightOffsetX
if (!shouldNotDrawAxis) rightOffsetX += axisWidth + 20
w.globals.translateYAxisX[index] = xRight - yaxe.labels.offsetX + 20
}
}
})
}
setYAxisTextAlignments() {
const w = this.w
const yaxis = Utils.listToArray(
w.globals.dom.baseEl.getElementsByClassName('apexcharts-yaxis')
)
yaxis.forEach((y, index) => {
const yaxe = w.config.yaxis[index]
if (yaxe && !yaxe.floating && yaxe.labels.align !== undefined) {
const yAxisInner = w.globals.dom.baseEl.querySelector(
`.apexcharts-yaxis[rel='${index}'] .apexcharts-yaxis-texts-g`
)
const yAxisTexts = Utils.listToArray(
w.globals.dom.baseEl.querySelectorAll(
`.apexcharts-yaxis[rel='${index}'] .apexcharts-yaxis-label`
)
)
const rect = yAxisInner.getBoundingClientRect()
yAxisTexts.forEach((label) => {
label.setAttribute('text-anchor', yaxe.labels.align)
})
if (yaxe.labels.align === 'left' && !yaxe.opposite) {
yAxisInner.setAttribute('transform', `translate(-${rect.width}, 0)`)
} else if (yaxe.labels.align === 'center') {
yAxisInner.setAttribute(
'transform',
`translate(${(rect.width / 2) * (!yaxe.opposite ? -1 : 1)}, 0)`
)
} else if (yaxe.labels.align === 'right' && yaxe.opposite) {
yAxisInner.setAttribute('transform', `translate(${rect.width}, 0)`)
}
}
})
}
}

View File

@@ -0,0 +1,360 @@
import YAxis from '../axes/YAxis'
import Helpers from './Helpers'
import DimXAxis from './XAxis'
import DimYAxis from './YAxis'
import Grid from './Grid'
/**
* ApexCharts Dimensions Class for calculating rects of all elements that are drawn and will be drawn.
*
* @module Dimensions
**/
export default class Dimensions {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.lgRect = {}
this.yAxisWidth = 0
this.yAxisWidthLeft = 0
this.yAxisWidthRight = 0
this.xAxisHeight = 0
this.isSparkline = this.w.config.chart.sparkline.enabled
this.dimHelpers = new Helpers(this)
this.dimYAxis = new DimYAxis(this)
this.dimXAxis = new DimXAxis(this)
this.dimGrid = new Grid(this)
this.lgWidthForSideLegends = 0
this.gridPad = this.w.config.grid.padding
this.xPadRight = 0
this.xPadLeft = 0
}
/**
* @memberof Dimensions
* @param {object} w - chart context
**/
plotCoords() {
let w = this.w
let gl = w.globals
this.lgRect = this.dimHelpers.getLegendsRect()
this.datalabelsCoords = { width: 0, height: 0 }
const maxStrokeWidth = Array.isArray(w.config.stroke.width)
? Math.max(...w.config.stroke.width)
: w.config.stroke.width
if (this.isSparkline) {
if (w.config.markers.discrete.length > 0 || w.config.markers.size > 0) {
Object.entries(this.gridPad).forEach(([k, v]) => {
this.gridPad[k] = Math.max(
v,
this.w.globals.markers.largestSize / 1.5
)
})
}
this.gridPad.top = Math.max(maxStrokeWidth / 2, this.gridPad.top)
this.gridPad.bottom = Math.max(maxStrokeWidth / 2, this.gridPad.bottom)
}
if (gl.axisCharts) {
// for line / area / scatter / column
this.setDimensionsForAxisCharts()
} else {
// for pie / donuts / circle
this.setDimensionsForNonAxisCharts()
}
this.dimGrid.gridPadFortitleSubtitle()
// after calculating everything, apply padding set by user
gl.gridHeight = gl.gridHeight - this.gridPad.top - this.gridPad.bottom
gl.gridWidth =
gl.gridWidth -
this.gridPad.left -
this.gridPad.right -
this.xPadRight -
this.xPadLeft
let barWidth = this.dimGrid.gridPadForColumnsInNumericAxis(gl.gridWidth)
gl.gridWidth = gl.gridWidth - barWidth * 2
gl.translateX =
gl.translateX +
this.gridPad.left +
this.xPadLeft +
(barWidth > 0 ? barWidth : 0)
gl.translateY = gl.translateY + this.gridPad.top
}
setDimensionsForAxisCharts() {
let w = this.w
let gl = w.globals
let yaxisLabelCoords = this.dimYAxis.getyAxisLabelsCoords()
let yTitleCoords = this.dimYAxis.getyAxisTitleCoords()
if (gl.isSlopeChart) {
this.datalabelsCoords = this.dimHelpers.getDatalabelsRect()
}
w.globals.yLabelsCoords = []
w.globals.yTitleCoords = []
w.config.yaxis.map((yaxe, index) => {
// store the labels and titles coords in global vars
w.globals.yLabelsCoords.push({
width: yaxisLabelCoords[index].width,
index,
})
w.globals.yTitleCoords.push({
width: yTitleCoords[index].width,
index,
})
})
this.yAxisWidth = this.dimYAxis.getTotalYAxisWidth()
let xaxisLabelCoords = this.dimXAxis.getxAxisLabelsCoords()
let xaxisGroupLabelCoords = this.dimXAxis.getxAxisGroupLabelsCoords()
let xtitleCoords = this.dimXAxis.getxAxisTitleCoords()
this.conditionalChecksForAxisCoords(
xaxisLabelCoords,
xtitleCoords,
xaxisGroupLabelCoords
)
gl.translateXAxisY = w.globals.rotateXLabels ? this.xAxisHeight / 8 : -4
gl.translateXAxisX =
w.globals.rotateXLabels &&
w.globals.isXNumeric &&
w.config.xaxis.labels.rotate <= -45
? -this.xAxisWidth / 4
: 0
if (w.globals.isBarHorizontal) {
gl.rotateXLabels = false
gl.translateXAxisY =
-1 * (parseInt(w.config.xaxis.labels.style.fontSize, 10) / 1.5)
}
gl.translateXAxisY = gl.translateXAxisY + w.config.xaxis.labels.offsetY
gl.translateXAxisX = gl.translateXAxisX + w.config.xaxis.labels.offsetX
let yAxisWidth = this.yAxisWidth
let xAxisHeight = this.xAxisHeight
gl.xAxisLabelsHeight = this.xAxisHeight - xtitleCoords.height
gl.xAxisGroupLabelsHeight = gl.xAxisLabelsHeight - xaxisLabelCoords.height
gl.xAxisLabelsWidth = this.xAxisWidth
gl.xAxisHeight = this.xAxisHeight
let translateY = 10
if (w.config.chart.type === 'radar' || this.isSparkline) {
yAxisWidth = 0
xAxisHeight = 0
}
if (this.isSparkline) {
this.lgRect = {
height: 0,
width: 0,
}
}
if (this.isSparkline || w.config.chart.type === 'treemap') {
yAxisWidth = 0
xAxisHeight = 0
translateY = 0
}
if (!this.isSparkline && w.config.chart.type !== 'treemap') {
this.dimXAxis.additionalPaddingXLabels(xaxisLabelCoords)
}
const legendTopBottom = () => {
gl.translateX = yAxisWidth + this.datalabelsCoords.width
gl.gridHeight =
gl.svgHeight -
this.lgRect.height -
xAxisHeight -
(!this.isSparkline && w.config.chart.type !== 'treemap'
? w.globals.rotateXLabels
? 10
: 15
: 0)
gl.gridWidth = gl.svgWidth - yAxisWidth - this.datalabelsCoords.width * 2
}
if (w.config.xaxis.position === 'top')
translateY = gl.xAxisHeight - w.config.xaxis.axisTicks.height - 5
switch (w.config.legend.position) {
case 'bottom':
gl.translateY = translateY
legendTopBottom()
break
case 'top':
gl.translateY = this.lgRect.height + translateY
legendTopBottom()
break
case 'left':
gl.translateY = translateY
gl.translateX =
this.lgRect.width + yAxisWidth + this.datalabelsCoords.width
gl.gridHeight = gl.svgHeight - xAxisHeight - 12
gl.gridWidth =
gl.svgWidth -
this.lgRect.width -
yAxisWidth -
this.datalabelsCoords.width * 2
break
case 'right':
gl.translateY = translateY
gl.translateX = yAxisWidth + this.datalabelsCoords.width
gl.gridHeight = gl.svgHeight - xAxisHeight - 12
gl.gridWidth =
gl.svgWidth -
this.lgRect.width -
yAxisWidth -
this.datalabelsCoords.width * 2 -
5
break
default:
throw new Error('Legend position not supported')
}
this.dimGrid.setGridXPosForDualYAxis(yTitleCoords, yaxisLabelCoords)
// after drawing everything, set the Y axis positions
let objyAxis = new YAxis(this.ctx)
objyAxis.setYAxisXPosition(yaxisLabelCoords, yTitleCoords)
}
setDimensionsForNonAxisCharts() {
let w = this.w
let gl = w.globals
let cnf = w.config
let xPad = 0
if (w.config.legend.show && !w.config.legend.floating) {
xPad = 20
}
const type =
cnf.chart.type === 'pie' ||
cnf.chart.type === 'polarArea' ||
cnf.chart.type === 'donut'
? 'pie'
: 'radialBar'
let offY = cnf.plotOptions[type].offsetY
let offX = cnf.plotOptions[type].offsetX
if (!cnf.legend.show || cnf.legend.floating) {
gl.gridHeight = gl.svgHeight
const maxWidth = gl.dom.elWrap.getBoundingClientRect().width
gl.gridWidth = Math.min(maxWidth, gl.gridHeight)
gl.translateY = offY
gl.translateX = offX + (gl.svgWidth - gl.gridWidth) / 2
return
}
switch (cnf.legend.position) {
case 'bottom':
gl.gridHeight = gl.svgHeight - this.lgRect.height
gl.gridWidth = gl.svgWidth
gl.translateY = offY - 10
gl.translateX = offX + (gl.svgWidth - gl.gridWidth) / 2
break
case 'top':
gl.gridHeight = gl.svgHeight - this.lgRect.height
gl.gridWidth = gl.svgWidth
gl.translateY = this.lgRect.height + offY + 10
gl.translateX = offX + (gl.svgWidth - gl.gridWidth) / 2
break
case 'left':
gl.gridWidth = gl.svgWidth - this.lgRect.width - xPad
gl.gridHeight =
cnf.chart.height !== 'auto' ? gl.svgHeight : gl.gridWidth
gl.translateY = offY
gl.translateX = offX + this.lgRect.width + xPad
break
case 'right':
gl.gridWidth = gl.svgWidth - this.lgRect.width - xPad - 5
gl.gridHeight =
cnf.chart.height !== 'auto' ? gl.svgHeight : gl.gridWidth
gl.translateY = offY
gl.translateX = offX + 10
break
default:
throw new Error('Legend position not supported')
}
}
conditionalChecksForAxisCoords(
xaxisLabelCoords,
xtitleCoords,
xaxisGroupLabelCoords
) {
const w = this.w
const xAxisNum = w.globals.hasXaxisGroups ? 2 : 1
const baseXAxisHeight =
xaxisGroupLabelCoords.height +
xaxisLabelCoords.height +
xtitleCoords.height
const xAxisHeightMultiplicate = w.globals.isMultiLineX
? 1.2
: w.globals.LINE_HEIGHT_RATIO
const rotatedXAxisOffset = w.globals.rotateXLabels ? 22 : 10
const rotatedXAxisLegendOffset =
w.globals.rotateXLabels && w.config.legend.position === 'bottom'
const additionalOffset = rotatedXAxisLegendOffset ? 10 : 0
this.xAxisHeight =
baseXAxisHeight * xAxisHeightMultiplicate +
xAxisNum * rotatedXAxisOffset +
additionalOffset
this.xAxisWidth = xaxisLabelCoords.width
if (
this.xAxisHeight - xtitleCoords.height >
w.config.xaxis.labels.maxHeight
) {
this.xAxisHeight = w.config.xaxis.labels.maxHeight
}
if (
w.config.xaxis.labels.minHeight &&
this.xAxisHeight < w.config.xaxis.labels.minHeight
) {
this.xAxisHeight = w.config.xaxis.labels.minHeight
}
if (w.config.xaxis.floating) {
this.xAxisHeight = 0
}
let minYAxisWidth = 0
let maxYAxisWidth = 0
w.config.yaxis.forEach((y) => {
minYAxisWidth += y.labels.minWidth
maxYAxisWidth += y.labels.maxWidth
})
if (this.yAxisWidth < minYAxisWidth) {
this.yAxisWidth = minYAxisWidth
}
if (this.yAxisWidth > maxYAxisWidth) {
this.yAxisWidth = maxYAxisWidth
}
}
}

View File

@@ -0,0 +1,143 @@
import AxesUtils from '../axes/AxesUtils'
export default class DimGrid {
constructor(dCtx) {
this.w = dCtx.w
this.dCtx = dCtx
}
gridPadForColumnsInNumericAxis(gridWidth) {
const { w } = this
const { config: cnf, globals: gl } = w
if (
gl.noData ||
gl.collapsedSeries.length + gl.ancillaryCollapsedSeries.length ===
cnf.series.length
) {
return 0
}
const hasBar = (type) =>
['bar', 'rangeBar', 'candlestick', 'boxPlot'].includes(type)
const type = cnf.chart.type
let barWidth = 0
let seriesLen = hasBar(type) ? cnf.series.length : 1
if (gl.comboBarCount > 0) {
seriesLen = gl.comboBarCount
}
gl.collapsedSeries.forEach((c) => {
if (hasBar(c.type)) {
seriesLen -= 1
}
})
if (cnf.chart.stacked) {
seriesLen = 1
}
const barsPresent = hasBar(type) || gl.comboBarCount > 0
let xRange = Math.abs(gl.initialMaxX - gl.initialMinX)
if (
barsPresent &&
gl.isXNumeric &&
!gl.isBarHorizontal &&
seriesLen > 0 &&
xRange !== 0
) {
if (xRange <= 3) {
xRange = gl.dataPoints
}
const xRatio = xRange / gridWidth
let xDivision =
gl.minXDiff && gl.minXDiff / xRatio > 0 ? gl.minXDiff / xRatio : 0
if (xDivision > gridWidth / 2) {
xDivision /= 2
}
// Here, barWidth is assumed to be the width occupied by a group of bars.
// There will be one bar in the group for each series plotted.
// Note: This version of the following math is different to that over in
// Helpers.js. Don't assume they should be the same. Over there,
// xDivision is computed differently and it's used on different charts.
// They were the same, but the solution to
// https://github.com/apexcharts/apexcharts.js/issues/4178
// was to remove the division by seriesLen.
barWidth =
(xDivision * parseInt(cnf.plotOptions.bar.columnWidth, 10)) / 100
if (barWidth < 1) {
barWidth = 1
}
gl.barPadForNumericAxis = barWidth
}
return barWidth
}
gridPadFortitleSubtitle() {
const { w } = this
const { globals: gl } = w
let gridShrinkOffset = this.dCtx.isSparkline || !gl.axisCharts ? 0 : 10
const titleSubtitle = ['title', 'subtitle']
titleSubtitle.forEach((t) => {
if (w.config[t].text !== undefined) {
gridShrinkOffset += w.config[t].margin
} else {
gridShrinkOffset += this.dCtx.isSparkline || !gl.axisCharts ? 0 : 5
}
})
if (
w.config.legend.show &&
w.config.legend.position === 'bottom' &&
!w.config.legend.floating &&
!gl.axisCharts
) {
gridShrinkOffset += 10
}
const titleCoords = this.dCtx.dimHelpers.getTitleSubtitleCoords('title')
const subtitleCoords =
this.dCtx.dimHelpers.getTitleSubtitleCoords('subtitle')
gl.gridHeight -=
titleCoords.height + subtitleCoords.height + gridShrinkOffset
gl.translateY +=
titleCoords.height + subtitleCoords.height + gridShrinkOffset
}
setGridXPosForDualYAxis(yTitleCoords, yaxisLabelCoords) {
const { w } = this
const axesUtils = new AxesUtils(this.dCtx.ctx)
w.config.yaxis.forEach((yaxe, index) => {
if (
w.globals.ignoreYAxisIndexes.indexOf(index) === -1 &&
!yaxe.floating &&
!axesUtils.isYAxisHidden(index)
) {
if (yaxe.opposite) {
w.globals.translateX -=
yaxisLabelCoords[index].width +
yTitleCoords[index].width +
parseInt(yaxe.labels.style.fontSize, 10) / 1.2 +
12
}
// fixes apexcharts.js#1599
if (w.globals.translateX < 2) {
w.globals.translateX = 2
}
}
})
}
}

View File

@@ -0,0 +1,144 @@
import Utils from '../../utils/Utils'
import Graphics from '../Graphics'
export default class Helpers {
constructor(dCtx) {
this.w = dCtx.w
this.dCtx = dCtx
}
/**
* Get Chart Title/Subtitle Dimensions
* @memberof Dimensions
* @return {{width, height}}
**/
getTitleSubtitleCoords(type) {
let w = this.w
let width = 0
let height = 0
const floating =
type === 'title' ? w.config.title.floating : w.config.subtitle.floating
let el = w.globals.dom.baseEl.querySelector(`.apexcharts-${type}-text`)
if (el !== null && !floating) {
let coord = el.getBoundingClientRect()
width = coord.width
height = w.globals.axisCharts ? coord.height + 5 : coord.height
}
return {
width,
height,
}
}
getLegendsRect() {
let w = this.w
let elLegendWrap = w.globals.dom.elLegendWrap
if (
!w.config.legend.height &&
(w.config.legend.position === 'top' ||
w.config.legend.position === 'bottom')
) {
// avoid legend to take up all the space
elLegendWrap.style.maxHeight = w.globals.svgHeight / 2 + 'px'
}
let lgRect = Object.assign({}, Utils.getBoundingClientRect(elLegendWrap))
if (
elLegendWrap !== null &&
!w.config.legend.floating &&
w.config.legend.show
) {
this.dCtx.lgRect = {
x: lgRect.x,
y: lgRect.y,
height: lgRect.height,
width: lgRect.height === 0 ? 0 : lgRect.width,
}
} else {
this.dCtx.lgRect = {
x: 0,
y: 0,
height: 0,
width: 0,
}
}
// if legend takes up all of the chart space, we need to restrict it.
if (
w.config.legend.position === 'left' ||
w.config.legend.position === 'right'
) {
if (this.dCtx.lgRect.width * 1.5 > w.globals.svgWidth) {
this.dCtx.lgRect.width = w.globals.svgWidth / 1.5
}
}
return this.dCtx.lgRect
}
/**
* Get Y Axis Dimensions
* @memberof Dimensions
* @return {{width, height}}
**/
getDatalabelsRect() {
let w = this.w
let allLabels = []
w.config.series.forEach((serie, seriesIndex) => {
serie.data.forEach((datum, dataPointIndex) => {
const getText = (v) => {
return w.config.dataLabels.formatter(v, {
ctx: this.dCtx.ctx,
seriesIndex,
dataPointIndex,
w,
})
}
val = getText(w.globals.series[seriesIndex][dataPointIndex])
allLabels.push(val)
})
})
let val = Utils.getLargestStringFromArr(allLabels)
let graphics = new Graphics(this.dCtx.ctx)
const dataLabelsStyle = w.config.dataLabels.style
let labelrect = graphics.getTextRects(
val,
parseInt(dataLabelsStyle.fontSize),
dataLabelsStyle.fontFamily
)
return {
width: labelrect.width * 1.05,
height: labelrect.height,
}
}
getLargestStringFromMultiArr(val, arr) {
const w = this.w
let valArr = val
if (w.globals.isMultiLineX) {
// if the xaxis labels has multiline texts (array)
let maxArrs = arr.map((xl, idx) => {
return Array.isArray(xl) ? xl.length : 1
})
let maxArrLen = Math.max(...maxArrs)
let maxArrIndex = maxArrs.indexOf(maxArrLen)
valArr = arr[maxArrIndex]
}
return valArr
}
}

View File

@@ -0,0 +1,370 @@
import Formatters from '../Formatters'
import Graphics from '../Graphics'
import Utils from '../../utils/Utils'
import DateTime from '../../utils/DateTime'
export default class DimXAxis {
constructor(dCtx) {
this.w = dCtx.w
this.dCtx = dCtx
}
/**
* Get X Axis Dimensions
* @memberof Dimensions
* @return {{width, height}}
**/
getxAxisLabelsCoords() {
let w = this.w
let xaxisLabels = w.globals.labels.slice()
if (w.config.xaxis.convertedCatToNumeric && xaxisLabels.length === 0) {
xaxisLabels = w.globals.categoryLabels
}
let rect
if (w.globals.timescaleLabels.length > 0) {
const coords = this.getxAxisTimeScaleLabelsCoords()
rect = {
width: coords.width,
height: coords.height,
}
w.globals.rotateXLabels = false
} else {
this.dCtx.lgWidthForSideLegends =
(w.config.legend.position === 'left' ||
w.config.legend.position === 'right') &&
!w.config.legend.floating
? this.dCtx.lgRect.width
: 0
// get the longest string from the labels array and also apply label formatter
let xlbFormatter = w.globals.xLabelFormatter
// prevent changing xaxisLabels to avoid issues in multi-yaxes - fix #522
let val = Utils.getLargestStringFromArr(xaxisLabels)
let valArr = this.dCtx.dimHelpers.getLargestStringFromMultiArr(
val,
xaxisLabels
)
// the labels gets changed for bar charts
if (w.globals.isBarHorizontal) {
val = w.globals.yAxisScale[0].result.reduce(
(a, b) => (a.length > b.length ? a : b),
0
)
valArr = val
}
let xFormat = new Formatters(this.dCtx.ctx)
let timestamp = val
val = xFormat.xLabelFormat(xlbFormatter, val, timestamp, {
i: undefined,
dateFormatter: new DateTime(this.dCtx.ctx).formatDate,
w,
})
valArr = xFormat.xLabelFormat(xlbFormatter, valArr, timestamp, {
i: undefined,
dateFormatter: new DateTime(this.dCtx.ctx).formatDate,
w,
})
if (
(w.config.xaxis.convertedCatToNumeric && typeof val === 'undefined') ||
String(val).trim() === ''
) {
val = '1'
valArr = val
}
let graphics = new Graphics(this.dCtx.ctx)
let xLabelrect = graphics.getTextRects(
val,
w.config.xaxis.labels.style.fontSize
)
let xArrLabelrect = xLabelrect
if (val !== valArr) {
xArrLabelrect = graphics.getTextRects(
valArr,
w.config.xaxis.labels.style.fontSize
)
}
rect = {
width:
xLabelrect.width >= xArrLabelrect.width
? xLabelrect.width
: xArrLabelrect.width,
height:
xLabelrect.height >= xArrLabelrect.height
? xLabelrect.height
: xArrLabelrect.height,
}
if (
(rect.width * xaxisLabels.length >
w.globals.svgWidth -
this.dCtx.lgWidthForSideLegends -
this.dCtx.yAxisWidth -
this.dCtx.gridPad.left -
this.dCtx.gridPad.right &&
w.config.xaxis.labels.rotate !== 0) ||
w.config.xaxis.labels.rotateAlways
) {
if (!w.globals.isBarHorizontal) {
w.globals.rotateXLabels = true
const getRotatedTextRects = (text) => {
return graphics.getTextRects(
text,
w.config.xaxis.labels.style.fontSize,
w.config.xaxis.labels.style.fontFamily,
`rotate(${w.config.xaxis.labels.rotate} 0 0)`,
false
)
}
xLabelrect = getRotatedTextRects(val)
if (val !== valArr) {
xArrLabelrect = getRotatedTextRects(valArr)
}
rect.height =
(xLabelrect.height > xArrLabelrect.height
? xLabelrect.height
: xArrLabelrect.height) / 1.5
rect.width =
xLabelrect.width > xArrLabelrect.width
? xLabelrect.width
: xArrLabelrect.width
}
} else {
w.globals.rotateXLabels = false
}
}
if (!w.config.xaxis.labels.show) {
rect = {
width: 0,
height: 0,
}
}
return {
width: rect.width,
height: rect.height,
}
}
/**
* Get X Axis Label Group height
* @memberof Dimensions
* @return {{width, height}}
*/
getxAxisGroupLabelsCoords() {
let w = this.w
if (!w.globals.hasXaxisGroups) {
return { width: 0, height: 0 }
}
const fontSize =
w.config.xaxis.group.style?.fontSize ||
w.config.xaxis.labels.style.fontSize
let xaxisLabels = w.globals.groups.map((g) => g.title)
let rect
// prevent changing xaxisLabels to avoid issues in multi-yaxes - fix #522
let val = Utils.getLargestStringFromArr(xaxisLabels)
let valArr = this.dCtx.dimHelpers.getLargestStringFromMultiArr(
val,
xaxisLabels
)
let graphics = new Graphics(this.dCtx.ctx)
let xLabelrect = graphics.getTextRects(val, fontSize)
let xArrLabelrect = xLabelrect
if (val !== valArr) {
xArrLabelrect = graphics.getTextRects(valArr, fontSize)
}
rect = {
width:
xLabelrect.width >= xArrLabelrect.width
? xLabelrect.width
: xArrLabelrect.width,
height:
xLabelrect.height >= xArrLabelrect.height
? xLabelrect.height
: xArrLabelrect.height,
}
if (!w.config.xaxis.labels.show) {
rect = {
width: 0,
height: 0,
}
}
return {
width: rect.width,
height: rect.height,
}
}
/**
* Get X Axis Title Dimensions
* @memberof Dimensions
* @return {{width, height}}
**/
getxAxisTitleCoords() {
let w = this.w
let width = 0
let height = 0
if (w.config.xaxis.title.text !== undefined) {
let graphics = new Graphics(this.dCtx.ctx)
let rect = graphics.getTextRects(
w.config.xaxis.title.text,
w.config.xaxis.title.style.fontSize
)
width = rect.width
height = rect.height
}
return {
width,
height,
}
}
getxAxisTimeScaleLabelsCoords() {
let w = this.w
let rect
this.dCtx.timescaleLabels = w.globals.timescaleLabels.slice()
let labels = this.dCtx.timescaleLabels.map((label) => label.value)
// get the longest string from the labels array and also apply label formatter to it
let val = labels.reduce((a, b) => {
// if undefined, maybe user didn't pass the datetime(x) values
if (typeof a === 'undefined') {
console.error(
'You have possibly supplied invalid Date format. Please supply a valid JavaScript Date'
)
return 0
} else {
return a.length > b.length ? a : b
}
}, 0)
let graphics = new Graphics(this.dCtx.ctx)
rect = graphics.getTextRects(val, w.config.xaxis.labels.style.fontSize)
let totalWidthRotated = rect.width * 1.05 * labels.length
if (
totalWidthRotated > w.globals.gridWidth &&
w.config.xaxis.labels.rotate !== 0
) {
w.globals.overlappingXLabels = true
}
return rect
}
// In certain cases, the last labels gets cropped in xaxis.
// Hence, we add some additional padding based on the label length to avoid the last label being cropped or we don't draw it at all
additionalPaddingXLabels(xaxisLabelCoords) {
const w = this.w
const gl = w.globals
const cnf = w.config
const xtype = cnf.xaxis.type
let lbWidth = xaxisLabelCoords.width
gl.skipLastTimelinelabel = false
gl.skipFirstTimelinelabel = false
const isBarOpposite =
w.config.yaxis[0].opposite && w.globals.isBarHorizontal
const isCollapsed = (i) => gl.collapsedSeriesIndices.indexOf(i) !== -1
const rightPad = (yaxe) => {
if (this.dCtx.timescaleLabels && this.dCtx.timescaleLabels.length) {
// for timeline labels, we take the last label and check if it exceeds gridWidth
const firstimescaleLabel = this.dCtx.timescaleLabels[0]
const lastTimescaleLabel =
this.dCtx.timescaleLabels[this.dCtx.timescaleLabels.length - 1]
const lastLabelPosition =
lastTimescaleLabel.position +
lbWidth / 1.75 -
this.dCtx.yAxisWidthRight
const firstLabelPosition =
firstimescaleLabel.position -
lbWidth / 1.75 +
this.dCtx.yAxisWidthLeft
let lgRightRectWidth =
w.config.legend.position === 'right' && this.dCtx.lgRect.width > 0
? this.dCtx.lgRect.width
: 0
if (
lastLabelPosition >
gl.svgWidth - gl.translateX - lgRightRectWidth
) {
gl.skipLastTimelinelabel = true
}
if (
firstLabelPosition <
-((!yaxe.show || yaxe.floating) &&
(cnf.chart.type === 'bar' ||
cnf.chart.type === 'candlestick' ||
cnf.chart.type === 'rangeBar' ||
cnf.chart.type === 'boxPlot')
? lbWidth / 1.75
: 10)
) {
gl.skipFirstTimelinelabel = true
}
} else if (xtype === 'datetime') {
// If user has enabled DateTime, but uses own's formatter
if (this.dCtx.gridPad.right < lbWidth && !gl.rotateXLabels) {
gl.skipLastTimelinelabel = true
}
} else if (xtype !== 'datetime') {
if (
this.dCtx.gridPad.right < lbWidth / 2 - this.dCtx.yAxisWidthRight &&
!gl.rotateXLabels &&
!w.config.xaxis.labels.trim
) {
this.dCtx.xPadRight = lbWidth / 2 + 1
}
}
}
const padYAxe = (yaxe, i) => {
if (cnf.yaxis.length > 1 && isCollapsed(i)) return
rightPad(yaxe)
}
cnf.yaxis.forEach((yaxe, i) => {
if (isBarOpposite) {
if (this.dCtx.gridPad.left < lbWidth) {
this.dCtx.xPadLeft = lbWidth / 2 + 1
}
this.dCtx.xPadRight = lbWidth / 2 + 1
} else {
padYAxe(yaxe, i)
}
})
}
}

View File

@@ -0,0 +1,211 @@
import Graphics from '../Graphics'
import Utils from '../../utils/Utils'
import AxesUtils from '../axes/AxesUtils'
export default class DimYAxis {
constructor(dCtx) {
this.w = dCtx.w
this.dCtx = dCtx
}
/**
* Get Y Axis Dimensions
* @memberof Dimensions
* @return {{width, height}}
**/
getyAxisLabelsCoords() {
let w = this.w
let width = 0
let height = 0
let ret = []
let labelPad = 10
const axesUtils = new AxesUtils(this.dCtx.ctx)
w.config.yaxis.map((yaxe, index) => {
const formatterArgs = {
seriesIndex: index,
dataPointIndex: -1,
w,
}
const yS = w.globals.yAxisScale[index]
let yAxisMinWidth = 0
if (
!axesUtils.isYAxisHidden(index) &&
yaxe.labels.show &&
yaxe.labels.minWidth !== undefined
)
yAxisMinWidth = yaxe.labels.minWidth
if (
!axesUtils.isYAxisHidden(index) &&
yaxe.labels.show &&
yS.result.length
) {
let lbFormatter = w.globals.yLabelFormatters[index]
let minV = yS.niceMin === Number.MIN_VALUE ? 0 : yS.niceMin
let val = yS.result.reduce((acc, curr) => {
return String(lbFormatter(acc, formatterArgs))?.length >
String(lbFormatter(curr, formatterArgs))?.length
? acc
: curr
}, minV)
val = lbFormatter(val, formatterArgs)
// the second parameter -1 is the index of tick which user can use in the formatter
let valArr = val
// if user has specified a custom formatter, and the result is null or empty, we need to discard the formatter and take the value as it is.
if (typeof val === 'undefined' || val.length === 0) {
val = yS.niceMax
}
if (w.globals.isBarHorizontal) {
labelPad = 0
let barYaxisLabels = w.globals.labels.slice()
// get the longest string from the labels array and also apply label formatter to it
val = Utils.getLargestStringFromArr(barYaxisLabels)
val = lbFormatter(val, { seriesIndex: index, dataPointIndex: -1, w })
valArr = this.dCtx.dimHelpers.getLargestStringFromMultiArr(
val,
barYaxisLabels
)
}
let graphics = new Graphics(this.dCtx.ctx)
let rotateStr = 'rotate('.concat(yaxe.labels.rotate, ' 0 0)')
let rect = graphics.getTextRects(
val,
yaxe.labels.style.fontSize,
yaxe.labels.style.fontFamily,
rotateStr,
false
)
let arrLabelrect = rect
if (val !== valArr) {
arrLabelrect = graphics.getTextRects(
valArr,
yaxe.labels.style.fontSize,
yaxe.labels.style.fontFamily,
rotateStr,
false
)
}
ret.push({
width:
(yAxisMinWidth > arrLabelrect.width || yAxisMinWidth > rect.width
? yAxisMinWidth
: arrLabelrect.width > rect.width
? arrLabelrect.width
: rect.width) + labelPad,
height:
arrLabelrect.height > rect.height
? arrLabelrect.height
: rect.height,
})
} else {
ret.push({
width,
height,
})
}
})
return ret
}
/**
* Get Y Axis Dimensions
* @memberof Dimensions
* @return {{width, height}}
**/
getyAxisTitleCoords() {
let w = this.w
let ret = []
w.config.yaxis.map((yaxe, index) => {
if (yaxe.show && yaxe.title.text !== undefined) {
let graphics = new Graphics(this.dCtx.ctx)
let rotateStr = 'rotate('.concat(yaxe.title.rotate, ' 0 0)')
let rect = graphics.getTextRects(
yaxe.title.text,
yaxe.title.style.fontSize,
yaxe.title.style.fontFamily,
rotateStr,
false
)
ret.push({
width: rect.width,
height: rect.height,
})
} else {
ret.push({
width: 0,
height: 0,
})
}
})
return ret
}
getTotalYAxisWidth() {
let w = this.w
let yAxisWidth = 0
let yAxisWidthLeft = 0
let yAxisWidthRight = 0
let padding = w.globals.yAxisScale.length > 1 ? 10 : 0
const axesUtils = new AxesUtils(this.dCtx.ctx)
const isHiddenYAxis = function (index) {
return w.globals.ignoreYAxisIndexes.indexOf(index) > -1
}
const padForLabelTitle = (coord, index) => {
let floating = w.config.yaxis[index].floating
let width = 0
if (coord.width > 0 && !floating) {
width = coord.width + padding
if (isHiddenYAxis(index)) {
width = width - coord.width - padding
}
} else {
width = floating || axesUtils.isYAxisHidden(index) ? 0 : 5
}
w.config.yaxis[index].opposite
? (yAxisWidthRight = yAxisWidthRight + width)
: (yAxisWidthLeft = yAxisWidthLeft + width)
yAxisWidth = yAxisWidth + width
}
w.globals.yLabelsCoords.map((yLabelCoord, index) => {
padForLabelTitle(yLabelCoord, index)
})
w.globals.yTitleCoords.map((yTitleCoord, index) => {
padForLabelTitle(yTitleCoord, index)
})
if (w.globals.isBarHorizontal && !w.config.yaxis[0].floating) {
yAxisWidth =
w.globals.yLabelsCoords[0].width + w.globals.yTitleCoords[0].width + 15
}
this.dCtx.yAxisWidthLeft = yAxisWidthLeft
this.dCtx.yAxisWidthRight = yAxisWidthRight
return yAxisWidth
}
}

View File

@@ -0,0 +1,89 @@
export default class Destroy {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
clear({ isUpdating }) {
if (this.ctx.zoomPanSelection) {
this.ctx.zoomPanSelection.destroy()
}
if (this.ctx.toolbar) {
this.ctx.toolbar.destroy()
}
this.ctx.animations = null
this.ctx.axes = null
this.ctx.annotations = null
this.ctx.core = null
this.ctx.data = null
this.ctx.grid = null
this.ctx.series = null
this.ctx.responsive = null
this.ctx.theme = null
this.ctx.formatters = null
this.ctx.titleSubtitle = null
this.ctx.legend = null
this.ctx.dimensions = null
this.ctx.options = null
this.ctx.crosshairs = null
this.ctx.zoomPanSelection = null
this.ctx.updateHelpers = null
this.ctx.toolbar = null
this.ctx.localization = null
this.ctx.w.globals.tooltip = null
this.clearDomElements({ isUpdating })
}
killSVG(draw) {
draw.each(function () {
this.removeClass('*')
this.off()
// this.stop()
}, true)
// draw.ungroup()
draw.clear()
}
clearDomElements({ isUpdating }) {
const elSVG = this.w.globals.dom.Paper.node
// fixes apexcharts.js#1654 & vue-apexcharts#256
if (elSVG.parentNode && elSVG.parentNode.parentNode && !isUpdating) {
elSVG.parentNode.parentNode.style.minHeight = 'unset'
}
// detach root event
const baseEl = this.w.globals.dom.baseEl
if (baseEl) {
// see https://github.com/apexcharts/vue-apexcharts/issues/275
this.ctx.eventList.forEach((event) => {
baseEl.removeEventListener(event, this.ctx.events.documentEvent)
})
}
const domEls = this.w.globals.dom
if (this.ctx.el !== null) {
// remove all child elements - resetting the whole chart
while (this.ctx.el.firstChild) {
this.ctx.el.removeChild(this.ctx.el.firstChild)
}
}
this.killSVG(domEls.Paper)
domEls.Paper.remove()
domEls.elWrap = null
domEls.elGraphical = null
domEls.elLegendWrap = null
domEls.elLegendForeign = null
domEls.baseEl = null
domEls.elGridRect = null
domEls.elGridRectMask = null
domEls.elGridRectBarMask = null
domEls.elGridRectMarkerMask = null
domEls.elForecastMask = null
domEls.elNonForecastMask = null
domEls.elDefs = null
}
}

View File

@@ -0,0 +1,114 @@
import Events from '../Events'
import Localization from './Localization'
import Animations from '../Animations'
import Axes from '../axes/Axes'
import Config from '../settings/Config'
import CoreUtils from '../CoreUtils'
import Crosshairs from '../Crosshairs'
import Grid from '../axes/Grid'
import Graphics from '../Graphics'
import Exports from '../Exports'
import Fill from '../Fill.js'
import Options from '../settings/Options'
import Responsive from '../Responsive'
import Series from '../Series'
import Theme from '../Theme'
import Formatters from '../Formatters'
import TitleSubtitle from '../TitleSubtitle'
import Legend from '../legend/Legend'
import Toolbar from '../Toolbar'
import Dimensions from '../dimensions/Dimensions'
import ZoomPanSelection from '../ZoomPanSelection'
import Tooltip from '../tooltip/Tooltip'
import Core from '../Core'
import Data from '../Data'
import UpdateHelpers from './UpdateHelpers'
import { SVG } from '@svgdotjs/svg.js'
import '../../svgjs/svg.pathmorphing.js'
import '@svgdotjs/svg.filter.js'
import '@svgdotjs/svg.draggable.js'
import '@svgdotjs/svg.select.js'
import '@svgdotjs/svg.resize.js'
if (typeof window.SVG === 'undefined') {
window.SVG = SVG
}
// global Apex object which user can use to override chart's defaults globally
if (typeof window.Apex === 'undefined') {
window.Apex = {}
}
export default class InitCtxVariables {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
initModules() {
this.ctx.publicMethods = [
'updateOptions',
'updateSeries',
'appendData',
'appendSeries',
'isSeriesHidden',
'highlightSeries',
'toggleSeries',
'showSeries',
'hideSeries',
'setLocale',
'resetSeries',
'zoomX',
'toggleDataPointSelection',
'dataURI',
'exportToCSV',
'addXaxisAnnotation',
'addYaxisAnnotation',
'addPointAnnotation',
'clearAnnotations',
'removeAnnotation',
'paper',
'destroy',
]
this.ctx.eventList = [
'click',
'mousedown',
'mousemove',
'mouseleave',
'touchstart',
'touchmove',
'touchleave',
'mouseup',
'touchend',
]
this.ctx.animations = new Animations(this.ctx)
this.ctx.axes = new Axes(this.ctx)
this.ctx.core = new Core(this.ctx.el, this.ctx)
this.ctx.config = new Config({})
this.ctx.data = new Data(this.ctx)
this.ctx.grid = new Grid(this.ctx)
this.ctx.graphics = new Graphics(this.ctx)
this.ctx.coreUtils = new CoreUtils(this.ctx)
this.ctx.crosshairs = new Crosshairs(this.ctx)
this.ctx.events = new Events(this.ctx)
this.ctx.exports = new Exports(this.ctx)
this.ctx.fill = new Fill(this.ctx)
this.ctx.localization = new Localization(this.ctx)
this.ctx.options = new Options()
this.ctx.responsive = new Responsive(this.ctx)
this.ctx.series = new Series(this.ctx)
this.ctx.theme = new Theme(this.ctx)
this.ctx.formatters = new Formatters(this.ctx)
this.ctx.titleSubtitle = new TitleSubtitle(this.ctx)
this.ctx.legend = new Legend(this.ctx)
this.ctx.toolbar = new Toolbar(this.ctx)
this.ctx.tooltip = new Tooltip(this.ctx)
this.ctx.dimensions = new Dimensions(this.ctx)
this.ctx.updateHelpers = new UpdateHelpers(this.ctx)
this.ctx.zoomPanSelection = new ZoomPanSelection(this.ctx)
this.ctx.w.globals.tooltip = new Tooltip(this.ctx)
}
}

View File

@@ -0,0 +1,39 @@
import Utils from '../../utils/Utils'
import en from '../../locales/en.json'
export default class Localization {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
setCurrentLocaleValues(localeName) {
let locales = this.w.config.chart.locales
// check if user has specified locales in global Apex variable
// if yes - then extend those with local chart's locale
if (
window.Apex.chart &&
window.Apex.chart.locales &&
window.Apex.chart.locales.length > 0
) {
locales = this.w.config.chart.locales.concat(window.Apex.chart.locales)
}
// find the locale from the array of locales which user has set (either by chart.defaultLocale or by calling setLocale() method.)
const selectedLocale = locales.filter((c) => c.name === localeName)[0]
if (selectedLocale) {
// create a complete locale object by extending defaults so you don't get undefined errors.
let ret = Utils.extend(en, selectedLocale)
// store these locale options in global var for ease access
this.w.globals.locale = ret.options
} else {
throw new Error(
'Wrong locale name provided. Please make sure you set the correct locale name in options'
)
}
}
}

View File

@@ -0,0 +1,304 @@
import Defaults from '../settings/Defaults'
import Config from '../settings/Config'
import CoreUtils from '../CoreUtils'
import Graphics from '../Graphics'
import Utils from '../../utils/Utils'
export default class UpdateHelpers {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
/**
* private method to update Options.
*
* @param {object} options - A new config object can be passed which will be merged with the existing config object
* @param {boolean} redraw - should redraw from beginning or should use existing paths and redraw from there
* @param {boolean} animate - should animate or not on updating Options
* @param {boolean} overwriteInitialConfig - should update the initial config or not
*/
_updateOptions(
options,
redraw = false,
animate = true,
updateSyncedCharts = true,
overwriteInitialConfig = false
) {
return new Promise((resolve) => {
let charts = [this.ctx]
if (updateSyncedCharts) {
charts = this.ctx.getSyncedCharts()
}
if (this.ctx.w.globals.isExecCalled) {
// If the user called exec method, we don't want to get grouped charts as user specifically provided a chartID to update
charts = [this.ctx]
this.ctx.w.globals.isExecCalled = false
}
charts.forEach((ch, chartIndex) => {
let w = ch.w
w.globals.shouldAnimate = animate
if (!redraw) {
w.globals.resized = true
w.globals.dataChanged = true
if (animate) {
ch.series.getPreviousPaths()
}
}
if (options && typeof options === 'object') {
ch.config = new Config(options)
options = CoreUtils.extendArrayProps(ch.config, options, w)
// fixes #914, #623
if (ch.w.globals.chartID !== this.ctx.w.globals.chartID) {
// don't overwrite series of synchronized charts
delete options.series
}
w.config = Utils.extend(w.config, options)
if (overwriteInitialConfig) {
// we need to forget the lastXAxis and lastYAxis as user forcefully overwriteInitialConfig. If we do not do this, and next time when user zooms the chart after setting yaxis.min/max or xaxis.min/max - the stored lastXAxis will never allow the chart to use the updated min/max by user.
w.globals.lastXAxis = options.xaxis
? Utils.clone(options.xaxis)
: []
w.globals.lastYAxis = options.yaxis
? Utils.clone(options.yaxis)
: []
// After forgetting lastAxes, we need to restore the new config in initialConfig/initialSeries
w.globals.initialConfig = Utils.extend({}, w.config)
w.globals.initialSeries = Utils.clone(w.config.series)
if (options.series) {
// Replace the collapsed series data
for (
let i = 0;
i < w.globals.collapsedSeriesIndices.length;
i++
) {
let series =
w.config.series[w.globals.collapsedSeriesIndices[i]]
w.globals.collapsedSeries[i].data = w.globals.axisCharts
? series.data.slice()
: series
}
for (
let i = 0;
i < w.globals.ancillaryCollapsedSeriesIndices.length;
i++
) {
let series =
w.config.series[w.globals.ancillaryCollapsedSeriesIndices[i]]
w.globals.ancillaryCollapsedSeries[i].data = w.globals
.axisCharts
? series.data.slice()
: series
}
// Ensure that auto-generated axes are scaled to the visible data
ch.series.emptyCollapsedSeries(w.config.series)
}
}
}
return ch.update(options).then(() => {
if (chartIndex === charts.length - 1) {
resolve(ch)
}
})
})
})
}
/**
* Private method to update Series.
*
* @param {array} series - New series which will override the existing
*/
_updateSeries(newSeries, animate, overwriteInitialSeries = false) {
return new Promise((resolve) => {
const w = this.w
w.globals.shouldAnimate = animate
w.globals.dataChanged = true
if (animate) {
this.ctx.series.getPreviousPaths()
}
let existingSeries
// axis charts
if (w.globals.axisCharts) {
existingSeries = newSeries.map((s, i) => {
return this._extendSeries(s, i)
})
if (existingSeries.length === 0) {
existingSeries = [{ data: [] }]
}
w.config.series = existingSeries
} else {
// non-axis chart (pie/radialbar)
w.config.series = newSeries.slice()
}
if (overwriteInitialSeries) {
w.globals.initialConfig.series = Utils.clone(w.config.series)
w.globals.initialSeries = Utils.clone(w.config.series)
}
return this.ctx.update().then(() => {
resolve(this.ctx)
})
})
}
_extendSeries(s, i) {
const w = this.w
const ser = w.config.series[i]
return {
...w.config.series[i],
name: s.name ? s.name : ser?.name,
color: s.color ? s.color : ser?.color,
type: s.type ? s.type : ser?.type,
group: s.group ? s.group : ser?.group,
hidden: typeof s.hidden !== 'undefined' ? s.hidden : ser?.hidden,
data: s.data ? s.data : ser?.data,
zIndex: typeof s.zIndex !== 'undefined' ? s.zIndex : i,
}
}
toggleDataPointSelection(seriesIndex, dataPointIndex) {
const w = this.w
let elPath = null
const parent = `.apexcharts-series[data\\:realIndex='${seriesIndex}']`
if (w.globals.axisCharts) {
elPath = w.globals.dom.Paper.findOne(
`${parent} path[j='${dataPointIndex}'], ${parent} circle[j='${dataPointIndex}'], ${parent} rect[j='${dataPointIndex}']`
)
} else {
// dataPointIndex will be undefined here, hence using seriesIndex
if (typeof dataPointIndex === 'undefined') {
elPath = w.globals.dom.Paper.findOne(
`${parent} path[j='${seriesIndex}']`
)
if (
w.config.chart.type === 'pie' ||
w.config.chart.type === 'polarArea' ||
w.config.chart.type === 'donut'
) {
this.ctx.pie.pieClicked(seriesIndex)
}
}
}
if (elPath) {
const graphics = new Graphics(this.ctx)
graphics.pathMouseDown(elPath, null)
} else {
console.warn('toggleDataPointSelection: Element not found')
return null
}
return elPath.node ? elPath.node : null
}
forceXAxisUpdate(options) {
const w = this.w
const minmax = ['min', 'max']
minmax.forEach((a) => {
if (typeof options.xaxis[a] !== 'undefined') {
w.config.xaxis[a] = options.xaxis[a]
w.globals.lastXAxis[a] = options.xaxis[a]
}
})
if (options.xaxis.categories && options.xaxis.categories.length) {
w.config.xaxis.categories = options.xaxis.categories
}
if (w.config.xaxis.convertedCatToNumeric) {
const defaults = new Defaults(options)
options = defaults.convertCatToNumericXaxis(options, this.ctx)
}
return options
}
forceYAxisUpdate(options) {
if (
options.chart &&
options.chart.stacked &&
options.chart.stackType === '100%'
) {
if (Array.isArray(options.yaxis)) {
options.yaxis.forEach((yaxe, index) => {
options.yaxis[index].min = 0
options.yaxis[index].max = 100
})
} else {
options.yaxis.min = 0
options.yaxis.max = 100
}
}
return options
}
/**
* This function reverts the yaxis and xaxis min/max values to what it was when the chart was defined.
* This function fixes an important bug where a user might load a new series after zooming in/out of previous series which resulted in wrong min/max
* Also, this should never be called internally on zoom/pan - the reset should only happen when user calls the updateSeries() function externally
* The function also accepts an object {xaxis, yaxis} which when present is set as the new xaxis/yaxis
*/
revertDefaultAxisMinMax(opts) {
const w = this.w
let xaxis = w.globals.lastXAxis
let yaxis = w.globals.lastYAxis
if (opts && opts.xaxis) {
xaxis = opts.xaxis
}
if (opts && opts.yaxis) {
yaxis = opts.yaxis
}
w.config.xaxis.min = xaxis.min
w.config.xaxis.max = xaxis.max
const getLastYAxis = (index) => {
if (typeof yaxis[index] !== 'undefined') {
w.config.yaxis[index].min = yaxis[index].min
w.config.yaxis[index].max = yaxis[index].max
}
}
w.config.yaxis.map((yaxe, index) => {
if (w.globals.zoomed) {
// user has zoomed, check the last yaxis
getLastYAxis(index)
} else {
// user hasn't zoomed, check the last yaxis first
if (typeof yaxis[index] !== 'undefined') {
getLastYAxis(index)
} else {
// if last y-axis don't exist, check the original yaxis
if (typeof this.ctx.opts.yaxis[index] !== 'undefined') {
yaxe.min = this.ctx.opts.yaxis[index].min
yaxe.max = this.ctx.opts.yaxis[index].max
}
}
}
})
}
}

View File

@@ -0,0 +1,311 @@
import Graphics from '../Graphics'
import Utils from '../../utils/Utils'
export default class Helpers {
constructor(lgCtx) {
this.w = lgCtx.w
this.lgCtx = lgCtx
}
getLegendStyles() {
let stylesheet = document.createElement('style')
stylesheet.setAttribute('type', 'text/css')
const nonce =
this.lgCtx.ctx?.opts?.chart?.nonce || this.w.config.chart.nonce
if (nonce) {
stylesheet.setAttribute('nonce', nonce)
}
const text = `
.apexcharts-flip-y {
transform: scaleY(-1) translateY(-100%);
transform-origin: top;
transform-box: fill-box;
}
.apexcharts-flip-x {
transform: scaleX(-1);
transform-origin: center;
transform-box: fill-box;
}
.apexcharts-legend {
display: flex;
overflow: auto;
padding: 0 10px;
}
.apexcharts-legend.apexcharts-legend-group-horizontal {
flex-direction: column;
}
.apexcharts-legend-group {
display: flex;
}
.apexcharts-legend-group-vertical {
flex-direction: column-reverse;
}
.apexcharts-legend.apx-legend-position-bottom, .apexcharts-legend.apx-legend-position-top {
flex-wrap: wrap
}
.apexcharts-legend.apx-legend-position-right, .apexcharts-legend.apx-legend-position-left {
flex-direction: column;
bottom: 0;
}
.apexcharts-legend.apx-legend-position-bottom.apexcharts-align-left, .apexcharts-legend.apx-legend-position-top.apexcharts-align-left, .apexcharts-legend.apx-legend-position-right, .apexcharts-legend.apx-legend-position-left {
justify-content: flex-start;
align-items: flex-start;
}
.apexcharts-legend.apx-legend-position-bottom.apexcharts-align-center, .apexcharts-legend.apx-legend-position-top.apexcharts-align-center {
justify-content: center;
align-items: center;
}
.apexcharts-legend.apx-legend-position-bottom.apexcharts-align-right, .apexcharts-legend.apx-legend-position-top.apexcharts-align-right {
justify-content: flex-end;
align-items: flex-end;
}
.apexcharts-legend-series {
cursor: pointer;
line-height: normal;
display: flex;
align-items: center;
}
.apexcharts-legend-text {
position: relative;
font-size: 14px;
}
.apexcharts-legend-text *, .apexcharts-legend-marker * {
pointer-events: none;
}
.apexcharts-legend-marker {
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-right: 1px;
}
.apexcharts-legend-series.apexcharts-no-click {
cursor: auto;
}
.apexcharts-legend .apexcharts-hidden-zero-series, .apexcharts-legend .apexcharts-hidden-null-series {
display: none !important;
}
.apexcharts-inactive-legend {
opacity: 0.45;
}
`
let rules = document.createTextNode(text)
stylesheet.appendChild(rules)
return stylesheet
}
getLegendDimensions() {
const w = this.w
let currLegendsWrap =
w.globals.dom.baseEl.querySelector('.apexcharts-legend')
let { width: currLegendsWrapWidth, height: currLegendsWrapHeight } =
currLegendsWrap.getBoundingClientRect()
return {
clwh: currLegendsWrapHeight,
clww: currLegendsWrapWidth,
}
}
appendToForeignObject() {
const gl = this.w.globals
gl.dom.elLegendForeign.appendChild(this.getLegendStyles())
}
toggleDataSeries(seriesCnt, isHidden) {
const w = this.w
if (w.globals.axisCharts || w.config.chart.type === 'radialBar') {
w.globals.resized = true // we don't want initial animations again
let seriesEl = null
let realIndex = null
// yes, make it null. 1 series will rise at a time
w.globals.risingSeries = []
if (w.globals.axisCharts) {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${seriesCnt}']`
)
realIndex = parseInt(seriesEl.getAttribute('data:realIndex'), 10)
} else {
seriesEl = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[rel='${seriesCnt + 1}']`
)
realIndex = parseInt(seriesEl.getAttribute('rel'), 10) - 1
}
if (isHidden) {
const seriesToMakeVisible = [
{
cs: w.globals.collapsedSeries,
csi: w.globals.collapsedSeriesIndices,
},
{
cs: w.globals.ancillaryCollapsedSeries,
csi: w.globals.ancillaryCollapsedSeriesIndices,
},
]
seriesToMakeVisible.forEach((r) => {
this.riseCollapsedSeries(r.cs, r.csi, realIndex)
})
} else {
this.hideSeries({ seriesEl, realIndex })
}
} else {
// for non-axis charts i.e pie / donuts
let seriesEl = w.globals.dom.Paper.findOne(
` .apexcharts-series[rel='${seriesCnt + 1}'] path`
)
const type = w.config.chart.type
if (type === 'pie' || type === 'polarArea' || type === 'donut') {
let dataLabels = w.config.plotOptions.pie.donut.labels
const graphics = new Graphics(this.lgCtx.ctx)
graphics.pathMouseDown(seriesEl, null)
this.lgCtx.ctx.pie.printDataLabelsInner(seriesEl.node, dataLabels)
}
seriesEl.fire('click')
}
}
getSeriesAfterCollapsing({ realIndex }) {
const w = this.w
const gl = w.globals
let series = Utils.clone(w.config.series)
if (gl.axisCharts) {
let yaxis = w.config.yaxis[gl.seriesYAxisReverseMap[realIndex]]
const collapseData = {
index: realIndex,
data: series[realIndex].data.slice(),
type: series[realIndex].type || w.config.chart.type,
}
if (yaxis && yaxis.show && yaxis.showAlways) {
if (gl.ancillaryCollapsedSeriesIndices.indexOf(realIndex) < 0) {
gl.ancillaryCollapsedSeries.push(collapseData)
gl.ancillaryCollapsedSeriesIndices.push(realIndex)
}
} else {
if (gl.collapsedSeriesIndices.indexOf(realIndex) < 0) {
gl.collapsedSeries.push(collapseData)
gl.collapsedSeriesIndices.push(realIndex)
let removeIndexOfRising = gl.risingSeries.indexOf(realIndex)
gl.risingSeries.splice(removeIndexOfRising, 1)
}
}
} else {
gl.collapsedSeries.push({
index: realIndex,
data: series[realIndex],
})
gl.collapsedSeriesIndices.push(realIndex)
}
gl.allSeriesCollapsed =
gl.collapsedSeries.length + gl.ancillaryCollapsedSeries.length ===
w.config.series.length
return this._getSeriesBasedOnCollapsedState(series)
}
hideSeries({ seriesEl, realIndex }) {
const w = this.w
let series = this.getSeriesAfterCollapsing({
realIndex,
})
let seriesChildren = seriesEl.childNodes
for (let sc = 0; sc < seriesChildren.length; sc++) {
if (
seriesChildren[sc].classList.contains('apexcharts-series-markers-wrap')
) {
if (seriesChildren[sc].classList.contains('apexcharts-hide')) {
seriesChildren[sc].classList.remove('apexcharts-hide')
} else {
seriesChildren[sc].classList.add('apexcharts-hide')
}
}
}
this.lgCtx.ctx.updateHelpers._updateSeries(
series,
w.config.chart.animations.dynamicAnimation.enabled
)
}
riseCollapsedSeries(collapsedSeries, seriesIndices, realIndex) {
const w = this.w
let series = Utils.clone(w.config.series)
if (collapsedSeries.length > 0) {
for (let c = 0; c < collapsedSeries.length; c++) {
if (collapsedSeries[c].index === realIndex) {
if (w.globals.axisCharts) {
series[realIndex].data = collapsedSeries[c].data.slice()
} else {
series[realIndex] = collapsedSeries[c].data
}
if (typeof series[realIndex] !== 'number') {
series[realIndex].hidden = false
}
collapsedSeries.splice(c, 1)
seriesIndices.splice(c, 1)
w.globals.risingSeries.push(realIndex)
}
}
series = this._getSeriesBasedOnCollapsedState(series)
this.lgCtx.ctx.updateHelpers._updateSeries(
series,
w.config.chart.animations.dynamicAnimation.enabled
)
}
}
_getSeriesBasedOnCollapsedState(series) {
const w = this.w
let collapsed = 0
if (w.globals.axisCharts) {
series.forEach((s, sI) => {
if (
!(
w.globals.collapsedSeriesIndices.indexOf(sI) < 0 &&
w.globals.ancillaryCollapsedSeriesIndices.indexOf(sI) < 0
)
) {
series[sI].data = []
collapsed++
}
})
} else {
series.forEach((s, sI) => {
if (!w.globals.collapsedSeriesIndices.indexOf(sI) < 0) {
series[sI] = 0
collapsed++
}
})
}
w.globals.allSeriesCollapsed = collapsed === series.length
return series
}
}

View File

@@ -0,0 +1,512 @@
import { SVG } from '@svgdotjs/svg.js'
import CoreUtils from '../CoreUtils'
import Dimensions from '../dimensions/Dimensions'
import Graphics from '../Graphics'
import Series from '../Series'
import Utils from '../../utils/Utils'
import Helpers from './Helpers'
import Markers from '../Markers'
/**
* ApexCharts Legend Class to draw legend.
*
* @module Legend
**/
class Legend {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.onLegendClick = this.onLegendClick.bind(this)
this.onLegendHovered = this.onLegendHovered.bind(this)
this.isBarsDistributed =
this.w.config.chart.type === 'bar' &&
this.w.config.plotOptions.bar.distributed &&
this.w.config.series.length === 1
this.legendHelpers = new Helpers(this)
}
init() {
const w = this.w
const gl = w.globals
const cnf = w.config
const showLegendAlways =
(cnf.legend.showForSingleSeries && gl.series.length === 1) ||
this.isBarsDistributed ||
gl.series.length > 1
this.legendHelpers.appendToForeignObject()
if ((showLegendAlways || !gl.axisCharts) && cnf.legend.show) {
while (gl.dom.elLegendWrap.firstChild) {
gl.dom.elLegendWrap.removeChild(gl.dom.elLegendWrap.firstChild)
}
this.drawLegends()
if (cnf.legend.position === 'bottom' || cnf.legend.position === 'top') {
this.legendAlignHorizontal()
} else if (
cnf.legend.position === 'right' ||
cnf.legend.position === 'left'
) {
this.legendAlignVertical()
}
}
}
createLegendMarker({ i, fillcolor }) {
const w = this.w
const elMarker = document.createElement('span')
elMarker.classList.add('apexcharts-legend-marker')
let mShape = w.config.legend.markers.shape || w.config.markers.shape
let shape = mShape
if (Array.isArray(mShape)) {
shape = mShape[i]
}
let mSize = Array.isArray(w.config.legend.markers.size)
? parseFloat(w.config.legend.markers.size[i])
: parseFloat(w.config.legend.markers.size)
let mOffsetX = Array.isArray(w.config.legend.markers.offsetX)
? parseFloat(w.config.legend.markers.offsetX[i])
: parseFloat(w.config.legend.markers.offsetX)
let mOffsetY = Array.isArray(w.config.legend.markers.offsetY)
? parseFloat(w.config.legend.markers.offsetY[i])
: parseFloat(w.config.legend.markers.offsetY)
let mBorderWidth = Array.isArray(w.config.legend.markers.strokeWidth)
? parseFloat(w.config.legend.markers.strokeWidth[i])
: parseFloat(w.config.legend.markers.strokeWidth)
let mStyle = elMarker.style
mStyle.height = (mSize + mBorderWidth) * 2 + 'px'
mStyle.width = (mSize + mBorderWidth) * 2 + 'px'
mStyle.left = mOffsetX + 'px'
mStyle.top = mOffsetY + 'px'
if (w.config.legend.markers.customHTML) {
mStyle.background = 'transparent'
mStyle.color = fillcolor[i]
if (Array.isArray(w.config.legend.markers.customHTML)) {
if (w.config.legend.markers.customHTML[i]) {
elMarker.innerHTML = w.config.legend.markers.customHTML[i]()
}
} else {
elMarker.innerHTML = w.config.legend.markers.customHTML()
}
} else {
let markers = new Markers(this.ctx)
const markerConfig = markers.getMarkerConfig({
cssClass: `apexcharts-legend-marker apexcharts-marker apexcharts-marker-${shape}`,
seriesIndex: i,
strokeWidth: mBorderWidth,
size: mSize,
})
const SVGMarker = window.SVG().addTo(elMarker).size('100%', '100%')
const marker = new Graphics(this.ctx).drawMarker(0, 0, {
...markerConfig,
pointFillColor: Array.isArray(fillcolor)
? fillcolor[i]
: markerConfig.pointFillColor,
shape,
})
const shapesEls = w.globals.dom.Paper.find(
'.apexcharts-legend-marker.apexcharts-marker'
)
shapesEls.forEach((shapeEl) => {
if (shapeEl.node.classList.contains('apexcharts-marker-triangle')) {
shapeEl.node.style.transform = 'translate(50%, 45%)'
} else {
shapeEl.node.style.transform = 'translate(50%, 50%)'
}
})
SVGMarker.add(marker)
}
return elMarker
}
drawLegends() {
let me = this
let w = this.w
let fontFamily = w.config.legend.fontFamily
let legendNames = w.globals.seriesNames
let fillcolor = w.config.legend.markers.fillColors
? w.config.legend.markers.fillColors.slice()
: w.globals.colors.slice()
if (w.config.chart.type === 'heatmap') {
const ranges = w.config.plotOptions.heatmap.colorScale.ranges
legendNames = ranges.map((colorScale) => {
return colorScale.name
? colorScale.name
: colorScale.from + ' - ' + colorScale.to
})
fillcolor = ranges.map((color) => color.color)
} else if (this.isBarsDistributed) {
legendNames = w.globals.labels.slice()
}
if (w.config.legend.customLegendItems.length) {
legendNames = w.config.legend.customLegendItems
}
let legendFormatter = w.globals.legendFormatter
let isLegendInversed = w.config.legend.inverseOrder
let legendGroups = []
if (
w.globals.seriesGroups.length > 1 &&
w.config.legend.clusterGroupedSeries
) {
w.globals.seriesGroups.forEach((_, gi) => {
legendGroups[gi] = document.createElement('div')
legendGroups[gi].classList.add(
'apexcharts-legend-group',
`apexcharts-legend-group-${gi}`
)
if (w.config.legend.clusterGroupedSeriesOrientation === 'horizontal') {
w.globals.dom.elLegendWrap.classList.add(
'apexcharts-legend-group-horizontal'
)
} else {
legendGroups[gi].classList.add('apexcharts-legend-group-vertical')
}
})
}
for (
let i = isLegendInversed ? legendNames.length - 1 : 0;
isLegendInversed ? i >= 0 : i <= legendNames.length - 1;
isLegendInversed ? i-- : i++
) {
let text = legendFormatter(legendNames[i], { seriesIndex: i, w })
let collapsedSeries = false
let ancillaryCollapsedSeries = false
if (w.globals.collapsedSeries.length > 0) {
for (let c = 0; c < w.globals.collapsedSeries.length; c++) {
if (w.globals.collapsedSeries[c].index === i) {
collapsedSeries = true
}
}
}
if (w.globals.ancillaryCollapsedSeriesIndices.length > 0) {
for (
let c = 0;
c < w.globals.ancillaryCollapsedSeriesIndices.length;
c++
) {
if (w.globals.ancillaryCollapsedSeriesIndices[c] === i) {
ancillaryCollapsedSeries = true
}
}
}
let elMarker = this.createLegendMarker({ i, fillcolor })
Graphics.setAttrs(elMarker, {
rel: i + 1,
'data:collapsed': collapsedSeries || ancillaryCollapsedSeries,
})
if (collapsedSeries || ancillaryCollapsedSeries) {
elMarker.classList.add('apexcharts-inactive-legend')
}
let elLegend = document.createElement('div')
let elLegendText = document.createElement('span')
elLegendText.classList.add('apexcharts-legend-text')
elLegendText.innerHTML = Array.isArray(text) ? text.join(' ') : text
let textColor = w.config.legend.labels.useSeriesColors
? w.globals.colors[i]
: Array.isArray(w.config.legend.labels.colors)
? w.config.legend.labels.colors?.[i]
: w.config.legend.labels.colors
if (!textColor) {
textColor = w.config.chart.foreColor
}
elLegendText.style.color = textColor
elLegendText.style.fontSize = parseFloat(w.config.legend.fontSize) + 'px'
elLegendText.style.fontWeight = w.config.legend.fontWeight
elLegendText.style.fontFamily = fontFamily || w.config.chart.fontFamily
Graphics.setAttrs(elLegendText, {
rel: i + 1,
i,
'data:default-text': encodeURIComponent(text),
'data:collapsed': collapsedSeries || ancillaryCollapsedSeries,
})
elLegend.appendChild(elMarker)
elLegend.appendChild(elLegendText)
const coreUtils = new CoreUtils(this.ctx)
if (!w.config.legend.showForZeroSeries) {
const total = coreUtils.getSeriesTotalByIndex(i)
if (
total === 0 &&
coreUtils.seriesHaveSameValues(i) &&
!coreUtils.isSeriesNull(i) &&
w.globals.collapsedSeriesIndices.indexOf(i) === -1 &&
w.globals.ancillaryCollapsedSeriesIndices.indexOf(i) === -1
) {
elLegend.classList.add('apexcharts-hidden-zero-series')
}
}
if (!w.config.legend.showForNullSeries) {
if (
coreUtils.isSeriesNull(i) &&
w.globals.collapsedSeriesIndices.indexOf(i) === -1 &&
w.globals.ancillaryCollapsedSeriesIndices.indexOf(i) === -1
) {
elLegend.classList.add('apexcharts-hidden-null-series')
}
}
if (legendGroups.length) {
w.globals.seriesGroups.forEach((group, gi) => {
if (group.includes(w.config.series[i]?.name)) {
w.globals.dom.elLegendWrap.appendChild(legendGroups[gi])
legendGroups[gi].appendChild(elLegend)
}
})
} else {
w.globals.dom.elLegendWrap.appendChild(elLegend)
}
w.globals.dom.elLegendWrap.classList.add(
`apexcharts-align-${w.config.legend.horizontalAlign}`
)
w.globals.dom.elLegendWrap.classList.add(
'apx-legend-position-' + w.config.legend.position
)
elLegend.classList.add('apexcharts-legend-series')
elLegend.style.margin = `${w.config.legend.itemMargin.vertical}px ${w.config.legend.itemMargin.horizontal}px`
w.globals.dom.elLegendWrap.style.width = w.config.legend.width
? w.config.legend.width + 'px'
: ''
w.globals.dom.elLegendWrap.style.height = w.config.legend.height
? w.config.legend.height + 'px'
: ''
Graphics.setAttrs(elLegend, {
rel: i + 1,
seriesName: Utils.escapeString(legendNames[i]),
'data:collapsed': collapsedSeries || ancillaryCollapsedSeries,
})
if (collapsedSeries || ancillaryCollapsedSeries) {
elLegend.classList.add('apexcharts-inactive-legend')
}
if (!w.config.legend.onItemClick.toggleDataSeries) {
elLegend.classList.add('apexcharts-no-click')
}
}
w.globals.dom.elWrap.addEventListener('click', me.onLegendClick, true)
if (
w.config.legend.onItemHover.highlightDataSeries &&
w.config.legend.customLegendItems.length === 0
) {
w.globals.dom.elWrap.addEventListener(
'mousemove',
me.onLegendHovered,
true
)
w.globals.dom.elWrap.addEventListener(
'mouseout',
me.onLegendHovered,
true
)
}
}
setLegendWrapXY(offsetX, offsetY) {
let w = this.w
let elLegendWrap = w.globals.dom.elLegendWrap
const legendHeight = elLegendWrap.clientHeight
let x = 0
let y = 0
if (w.config.legend.position === 'bottom') {
y =
w.globals.svgHeight -
Math.min(legendHeight, w.globals.svgHeight / 2) -
5
} else if (w.config.legend.position === 'top') {
const dim = new Dimensions(this.ctx)
const titleH = dim.dimHelpers.getTitleSubtitleCoords('title').height
const subtitleH = dim.dimHelpers.getTitleSubtitleCoords('subtitle').height
y = (titleH > 0 ? titleH - 10 : 0) + (subtitleH > 0 ? subtitleH - 10 : 0)
}
elLegendWrap.style.position = 'absolute'
x = x + offsetX + w.config.legend.offsetX
y = y + offsetY + w.config.legend.offsetY
elLegendWrap.style.left = x + 'px'
elLegendWrap.style.top = y + 'px'
if (w.config.legend.position === 'right') {
elLegendWrap.style.left = 'auto'
elLegendWrap.style.right = 25 + w.config.legend.offsetX + 'px'
}
const fixedHeigthWidth = ['width', 'height']
fixedHeigthWidth.forEach((hw) => {
if (elLegendWrap.style[hw]) {
elLegendWrap.style[hw] = parseInt(w.config.legend[hw], 10) + 'px'
}
})
}
legendAlignHorizontal() {
let w = this.w
let elLegendWrap = w.globals.dom.elLegendWrap
elLegendWrap.style.right = 0
let dimensions = new Dimensions(this.ctx)
let titleRect = dimensions.dimHelpers.getTitleSubtitleCoords('title')
let subtitleRect = dimensions.dimHelpers.getTitleSubtitleCoords('subtitle')
let offsetX = 20
let offsetY = 0
if (w.config.legend.position === 'top') {
offsetY =
titleRect.height +
subtitleRect.height +
w.config.title.margin +
w.config.subtitle.margin -
10
}
this.setLegendWrapXY(offsetX, offsetY)
}
legendAlignVertical() {
let w = this.w
let lRect = this.legendHelpers.getLegendDimensions()
let offsetY = 20
let offsetX = 0
if (w.config.legend.position === 'left') {
offsetX = 20
}
if (w.config.legend.position === 'right') {
offsetX = w.globals.svgWidth - lRect.clww - 10
}
this.setLegendWrapXY(offsetX, offsetY)
}
onLegendHovered(e) {
const w = this.w
const hoverOverLegend =
e.target.classList.contains('apexcharts-legend-series') ||
e.target.classList.contains('apexcharts-legend-text') ||
e.target.classList.contains('apexcharts-legend-marker')
if (w.config.chart.type !== 'heatmap' && !this.isBarsDistributed) {
if (
!e.target.classList.contains('apexcharts-inactive-legend') &&
hoverOverLegend
) {
let series = new Series(this.ctx)
series.toggleSeriesOnHover(e, e.target)
}
} else {
// for heatmap handling
if (hoverOverLegend) {
let seriesCnt = parseInt(e.target.getAttribute('rel'), 10) - 1
this.ctx.events.fireEvent('legendHover', [this.ctx, seriesCnt, this.w])
let series = new Series(this.ctx)
series.highlightRangeInSeries(e, e.target)
}
}
}
onLegendClick(e) {
const w = this.w
if (w.config.legend.customLegendItems.length) return
if (
e.target.classList.contains('apexcharts-legend-series') ||
e.target.classList.contains('apexcharts-legend-text') ||
e.target.classList.contains('apexcharts-legend-marker')
) {
let seriesCnt = parseInt(e.target.getAttribute('rel'), 10) - 1
let isHidden = e.target.getAttribute('data:collapsed') === 'true'
const legendClick = this.w.config.chart.events.legendClick
if (typeof legendClick === 'function') {
legendClick(this.ctx, seriesCnt, this.w)
}
this.ctx.events.fireEvent('legendClick', [this.ctx, seriesCnt, this.w])
const markerClick = this.w.config.legend.markers.onClick
if (
typeof markerClick === 'function' &&
e.target.classList.contains('apexcharts-legend-marker')
) {
markerClick(this.ctx, seriesCnt, this.w)
this.ctx.events.fireEvent('legendMarkerClick', [
this.ctx,
seriesCnt,
this.w,
])
}
// for now - just prevent click on heatmap legend - and allow hover only
const clickAllowed =
w.config.chart.type !== 'treemap' &&
w.config.chart.type !== 'heatmap' &&
!this.isBarsDistributed
if (clickAllowed && w.config.legend.onItemClick.toggleDataSeries) {
this.legendHelpers.toggleDataSeries(seriesCnt, isHidden)
}
}
}
}
export default Legend

View File

@@ -0,0 +1,338 @@
import Defaults from './Defaults'
import Utils from './../../utils/Utils'
import Options from './Options'
/**
* ApexCharts Config Class for extending user options with pre-defined ApexCharts config.
*
* @module Config
**/
export default class Config {
constructor(opts) {
this.opts = opts
}
init({ responsiveOverride }) {
let opts = this.opts
let options = new Options()
let defaults = new Defaults(opts)
this.chartType = opts.chart.type
opts = this.extendYAxis(opts)
opts = this.extendAnnotations(opts)
let config = options.init()
let newDefaults = {}
if (opts && typeof opts === 'object') {
let chartDefaults = {}
const chartTypes = [
'line',
'area',
'bar',
'candlestick',
'boxPlot',
'rangeBar',
'rangeArea',
'bubble',
'scatter',
'heatmap',
'treemap',
'pie',
'polarArea',
'donut',
'radar',
'radialBar',
]
if (chartTypes.indexOf(opts.chart.type) !== -1) {
chartDefaults = defaults[opts.chart.type]()
} else {
chartDefaults = defaults.line()
}
if (opts.plotOptions?.bar?.isFunnel) {
chartDefaults = defaults.funnel()
}
if (opts.chart.stacked && opts.chart.type === 'bar') {
chartDefaults = defaults.stackedBars()
}
if (opts.chart.brush?.enabled) {
chartDefaults = defaults.brush(chartDefaults)
}
if (opts.plotOptions?.line?.isSlopeChart) {
chartDefaults = defaults.slope()
}
if (opts.chart.stacked && opts.chart.stackType === '100%') {
opts = defaults.stacked100(opts)
}
if (opts.plotOptions?.bar?.isDumbbell) {
opts = defaults.dumbbell(opts)
}
// If user has specified a dark theme, make the tooltip dark too
this.checkForDarkTheme(window.Apex) // check global window Apex options
this.checkForDarkTheme(opts) // check locally passed options
opts.xaxis = opts.xaxis || window.Apex.xaxis || {}
// an important boolean needs to be set here
// otherwise all the charts will have this flag set to true window.Apex.xaxis is set globally
if (!responsiveOverride) {
opts.xaxis.convertedCatToNumeric = false
}
opts = this.checkForCatToNumericXAxis(this.chartType, chartDefaults, opts)
if (
opts.chart.sparkline?.enabled ||
window.Apex.chart?.sparkline?.enabled
) {
chartDefaults = defaults.sparkline(chartDefaults)
}
newDefaults = Utils.extend(config, chartDefaults)
}
// config should cascade in this fashion
// default-config < global-apex-variable-config < user-defined-config
// get GLOBALLY defined options and merge with the default config
let mergedWithDefaultConfig = Utils.extend(newDefaults, window.Apex)
// get the merged config and extend with user defined config
config = Utils.extend(mergedWithDefaultConfig, opts)
// some features are not supported. those mismatches should be handled
config = this.handleUserInputErrors(config)
return config
}
checkForCatToNumericXAxis(chartType, chartDefaults, opts) {
let defaults = new Defaults(opts)
const isBarHorizontal =
(chartType === 'bar' || chartType === 'boxPlot') &&
opts.plotOptions?.bar?.horizontal
const unsupportedZoom =
chartType === 'pie' ||
chartType === 'polarArea' ||
chartType === 'donut' ||
chartType === 'radar' ||
chartType === 'radialBar' ||
chartType === 'heatmap'
const notNumericXAxis =
opts.xaxis.type !== 'datetime' && opts.xaxis.type !== 'numeric'
let tickPlacement = opts.xaxis.tickPlacement
? opts.xaxis.tickPlacement
: chartDefaults.xaxis && chartDefaults.xaxis.tickPlacement
if (
!isBarHorizontal &&
!unsupportedZoom &&
notNumericXAxis &&
tickPlacement !== 'between'
) {
opts = defaults.convertCatToNumeric(opts)
}
return opts
}
extendYAxis(opts, w) {
let options = new Options()
if (
typeof opts.yaxis === 'undefined' ||
!opts.yaxis ||
(Array.isArray(opts.yaxis) && opts.yaxis.length === 0)
) {
opts.yaxis = {}
}
// extend global yaxis config (only if object is provided / not an array)
if (
opts.yaxis.constructor !== Array &&
window.Apex.yaxis &&
window.Apex.yaxis.constructor !== Array
) {
opts.yaxis = Utils.extend(opts.yaxis, window.Apex.yaxis)
}
// as we can't extend nested object's array with extend, we need to do it first
// user can provide either an array or object in yaxis config
if (opts.yaxis.constructor !== Array) {
// convert the yaxis to array if user supplied object
opts.yaxis = [Utils.extend(options.yAxis, opts.yaxis)]
} else {
opts.yaxis = Utils.extendArray(opts.yaxis, options.yAxis)
}
let isLogY = false
opts.yaxis.forEach((y) => {
if (y.logarithmic) {
isLogY = true
}
})
let series = opts.series
if (w && !series) {
series = w.config.series
}
// A logarithmic chart works correctly when each series has a corresponding y-axis
// If this is not the case, we manually create yaxis for multi-series log chart
if (isLogY && series.length !== opts.yaxis.length && series.length) {
opts.yaxis = series.map((s, i) => {
if (!s.name) {
series[i].name = `series-${i + 1}`
}
if (opts.yaxis[i]) {
opts.yaxis[i].seriesName = series[i].name
return opts.yaxis[i]
} else {
const newYaxis = Utils.extend(options.yAxis, opts.yaxis[0])
newYaxis.show = false
return newYaxis
}
})
}
if (isLogY && series.length > 1 && series.length !== opts.yaxis.length) {
console.warn(
'A multi-series logarithmic chart should have equal number of series and y-axes'
)
}
return opts
}
// annotations also accepts array, so we need to extend them manually
extendAnnotations(opts) {
if (typeof opts.annotations === 'undefined') {
opts.annotations = {}
opts.annotations.yaxis = []
opts.annotations.xaxis = []
opts.annotations.points = []
}
opts = this.extendYAxisAnnotations(opts)
opts = this.extendXAxisAnnotations(opts)
opts = this.extendPointAnnotations(opts)
return opts
}
extendYAxisAnnotations(opts) {
let options = new Options()
opts.annotations.yaxis = Utils.extendArray(
typeof opts.annotations.yaxis !== 'undefined'
? opts.annotations.yaxis
: [],
options.yAxisAnnotation
)
return opts
}
extendXAxisAnnotations(opts) {
let options = new Options()
opts.annotations.xaxis = Utils.extendArray(
typeof opts.annotations.xaxis !== 'undefined'
? opts.annotations.xaxis
: [],
options.xAxisAnnotation
)
return opts
}
extendPointAnnotations(opts) {
let options = new Options()
opts.annotations.points = Utils.extendArray(
typeof opts.annotations.points !== 'undefined'
? opts.annotations.points
: [],
options.pointAnnotation
)
return opts
}
checkForDarkTheme(opts) {
if (opts.theme && opts.theme.mode === 'dark') {
if (!opts.tooltip) {
opts.tooltip = {}
}
if (opts.tooltip.theme !== 'light') {
opts.tooltip.theme = 'dark'
}
if (!opts.chart.foreColor) {
opts.chart.foreColor = '#f6f7f8'
}
if (!opts.theme.palette) {
opts.theme.palette = 'palette4'
}
}
}
handleUserInputErrors(opts) {
let config = opts
// conflicting tooltip option. intersect makes sure to focus on 1 point at a time. Shared cannot be used along with it
if (config.tooltip.shared && config.tooltip.intersect) {
throw new Error(
'tooltip.shared cannot be enabled when tooltip.intersect is true. Turn off any other option by setting it to false.'
)
}
if (config.chart.type === 'bar' && config.plotOptions.bar.horizontal) {
// No multiple yaxis for bars
if (config.yaxis.length > 1) {
throw new Error(
'Multiple Y Axis for bars are not supported. Switch to column chart by setting plotOptions.bar.horizontal=false'
)
}
// if yaxis is reversed in horizontal bar chart, you should draw the y-axis on right side
if (config.yaxis[0].reversed) {
config.yaxis[0].opposite = true
}
config.xaxis.tooltip.enabled = false // no xaxis tooltip for horizontal bar
config.yaxis[0].tooltip.enabled = false // no xaxis tooltip for horizontal bar
config.chart.zoom.enabled = false // no zooming for horz bars
}
if (config.chart.type === 'bar' || config.chart.type === 'rangeBar') {
if (config.tooltip.shared) {
if (
config.xaxis.crosshairs.width === 'barWidth' &&
config.series.length > 1
) {
config.xaxis.crosshairs.width = 'tickWidth'
}
}
}
if (
config.chart.type === 'candlestick' ||
config.chart.type === 'boxPlot'
) {
if (config.yaxis[0].reversed) {
console.warn(
`Reversed y-axis in ${config.chart.type} chart is not supported.`
)
config.yaxis[0].reversed = false
}
}
return config
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,261 @@
import Utils from './../../utils/Utils'
export default class Globals {
initGlobalVars(gl) {
gl.series = [] // the MAIN series array (y values)
gl.seriesCandleO = []
gl.seriesCandleH = []
gl.seriesCandleM = []
gl.seriesCandleL = []
gl.seriesCandleC = []
gl.seriesRangeStart = []
gl.seriesRangeEnd = []
gl.seriesRange = []
gl.seriesPercent = []
gl.seriesGoals = []
gl.seriesX = []
gl.seriesZ = []
gl.seriesNames = []
gl.seriesTotals = []
gl.seriesLog = []
gl.seriesColors = []
gl.stackedSeriesTotals = []
gl.seriesXvalues = [] // we will need this in tooltip (it's x position)
// when we will have unequal x values, we will need
// some way to get x value depending on mouse pointer
gl.seriesYvalues = [] // we will need this when deciding which series
// user hovered on
gl.labels = []
gl.hasXaxisGroups = false
gl.groups = []
gl.barGroups = []
gl.lineGroups = []
gl.areaGroups = []
gl.hasSeriesGroups = false
gl.seriesGroups = []
gl.categoryLabels = []
gl.timescaleLabels = []
gl.noLabelsProvided = false
gl.resizeTimer = null
gl.selectionResizeTimer = null
gl.lastWheelExecution = 0
gl.delayedElements = []
gl.pointsArray = []
gl.dataLabelsRects = []
gl.isXNumeric = false
gl.skipLastTimelinelabel = false
gl.skipFirstTimelinelabel = false
gl.isDataXYZ = false
gl.isMultiLineX = false
gl.isMultipleYAxis = false
gl.maxY = -Number.MAX_VALUE
gl.minY = Number.MIN_VALUE
gl.minYArr = []
gl.maxYArr = []
gl.maxX = -Number.MAX_VALUE
gl.minX = Number.MAX_VALUE
gl.initialMaxX = -Number.MAX_VALUE
gl.initialMinX = Number.MAX_VALUE
gl.maxDate = 0
gl.minDate = Number.MAX_VALUE
gl.minZ = Number.MAX_VALUE
gl.maxZ = -Number.MAX_VALUE
gl.minXDiff = Number.MAX_VALUE
gl.yAxisScale = []
gl.xAxisScale = null
gl.xAxisTicksPositions = []
gl.yLabelsCoords = []
gl.yTitleCoords = []
gl.barPadForNumericAxis = 0
gl.padHorizontal = 0
gl.xRange = 0
gl.yRange = []
gl.zRange = 0
gl.dataPoints = 0
gl.xTickAmount = 0
gl.multiAxisTickAmount = 0
}
globalVars(config) {
return {
chartID: null, // chart ID - apexcharts-cuid
cuid: null, // chart ID - random numbers excluding "apexcharts" part
events: {
beforeMount: [],
mounted: [],
updated: [],
clicked: [],
selection: [],
dataPointSelection: [],
zoomed: [],
scrolled: [],
},
colors: [],
clientX: null,
clientY: null,
fill: {
colors: [],
},
stroke: {
colors: [],
},
dataLabels: {
style: {
colors: [],
},
},
radarPolygons: {
fill: {
colors: [],
},
},
markers: {
colors: [],
size: config.markers.size,
largestSize: 0,
},
animationEnded: false,
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints,
isDirty: false, // chart has been updated after the initial render. This is different than dataChanged property. isDirty means user manually called some method to update
isExecCalled: false, // whether user updated the chart through the exec method
initialConfig: null, // we will store the first config user has set to go back when user finishes interactions like zooming and come out of it
initialSeries: [],
lastXAxis: [],
lastYAxis: [],
columnSeries: null,
labels: [], // store the text to draw on x axis
// Don't mutate the labels, many things including tooltips depends on it!
timescaleLabels: [], // store the timescaleLabels Labels in another variable
noLabelsProvided: false, // if user didn't provide any categories/labels or x values, fallback to 1,2,3,4...
allSeriesCollapsed: false,
collapsedSeries: [], // when user collapses a series, it goes into this array
collapsedSeriesIndices: [], // this stores the index of the collapsedSeries instead of whole object for quick access
ancillaryCollapsedSeries: [], // when user collapses an "alwaysVisible" series, it goes into this array
ancillaryCollapsedSeriesIndices: [], // this stores the index of the ancillaryCollapsedSeries whose y-axis is always visible
risingSeries: [], // when user re-opens a collapsed series, it goes here
dataFormatXNumeric: false, // boolean value to indicate user has passed numeric x values
capturedSeriesIndex: -1,
capturedDataPointIndex: -1,
selectedDataPoints: [],
invalidLogScale: false, // if a user enabled log scale but the data provided is not valid to generate a log scale, turn on this flag
ignoreYAxisIndexes: [], // when series are being collapsed in multiple y axes, ignore certain index
maxValsInArrayIndex: 0,
radialSize: 0,
selection: undefined,
zoomEnabled:
config.chart.toolbar.autoSelected === 'zoom' &&
config.chart.toolbar.tools.zoom &&
config.chart.zoom.enabled,
panEnabled:
config.chart.toolbar.autoSelected === 'pan' &&
config.chart.toolbar.tools.pan,
selectionEnabled:
config.chart.toolbar.autoSelected === 'selection' &&
config.chart.toolbar.tools.selection,
yaxis: null,
mousedown: false,
lastClientPosition: {}, // don't reset this variable this the chart is destroyed. It is used to detect right or left mousemove in panning
visibleXRange: undefined,
yValueDecimal: 0, // are there floating numbers in the series. If yes, this represent the len of the decimals
total: 0,
SVGNS: 'http://www.w3.org/2000/svg', // svg namespace
svgWidth: 0, // the whole svg width
svgHeight: 0, // the whole svg height
noData: false, // whether there is any data to display or not
locale: {}, // the current locale values will be preserved here for global access
dom: {}, // for storing all dom nodes in this particular property
memory: {
methodsToExec: [],
},
shouldAnimate: true,
skipLastTimelinelabel: false, // when last label is cropped, skip drawing it
skipFirstTimelinelabel: false, // when first label is cropped, skip drawing it
delayedElements: [], // element which appear after animation has finished
axisCharts: true, // chart type = line or area or bar
// (refer them also as plot charts in the code)
isDataXYZ: false, // bool: data was provided in a {[x,y,z]} pattern
isSlopeChart: config.plotOptions.line.isSlopeChart,
resized: false, // bool: user has resized
resizeTimer: null, // timeout function to make a small delay before
// drawing when user resized
comboCharts: false, // bool: whether it's a combination of line/column
dataChanged: false, // bool: has data changed dynamically
previousPaths: [], // array: when data is changed, it will animate from
// previous paths
allSeriesHasEqualX: true,
pointsArray: [], // store the points positions here to draw later on hover
// format is - [[x,y],[x,y]... [x,y]]
dataLabelsRects: [], // store the positions of datalabels to prevent collision
lastDrawnDataLabelsIndexes: [],
hasNullValues: false, // bool: whether series contains null values
zoomed: false, // whether user has zoomed or not
gridWidth: 0, // drawable width of actual graphs (series paths)
gridHeight: 0, // drawable height of actual graphs (series paths)
rotateXLabels: false,
defaultLabels: false,
xLabelFormatter: undefined, // formatter for x axis labels
yLabelFormatters: [],
xaxisTooltipFormatter: undefined, // formatter for x axis tooltip
ttKeyFormatter: undefined,
ttVal: undefined,
ttZFormatter: undefined,
LINE_HEIGHT_RATIO: 1.618,
xAxisLabelsHeight: 0,
xAxisGroupLabelsHeight: 0,
xAxisLabelsWidth: 0,
yAxisLabelsWidth: 0,
scaleX: 1,
scaleY: 1,
translateX: 0,
translateY: 0,
translateYAxisX: [],
yAxisWidths: [],
translateXAxisY: 0,
translateXAxisX: 0,
tooltip: null,
// Rules for niceScaleAllowedMagMsd:
// 1) An array of two arrays only ([[],[]]):
// * array[0][]: influences labelling of data series that contain only integers
// - must contain only integers (or expect ugly ticks)
// * array[1][]: influences labelling of data series that contain at least one float
// - may contain floats
// * both arrays:
// - each array[][i] ideally satisfy: 10 mod array[][i] == 0 (or expect ugly ticks)
// - to avoid clipping data point keep each array[][i] >= i
// 2) each array[i][] contains 11 values, for all possible index values 0..10.
// array[][0] should not be needed (not proven) but ensures non-zero is returned.
//
// Users can effectively force their preferred "magMsd" through stepSize and
// forceNiceScale. With forceNiceScale: true, stepSize becomes normalizable to the
// axis's min..max range, which allows users to set stepSize to an integer 1..10, for
// example, stepSize: 3. This value will be preferred to the value determined through
// this array. The range-normalized value is checked for consistency with other
// user defined options and will be ignored if inconsistent.
niceScaleAllowedMagMsd: [
[1, 1, 2, 5, 5, 5, 10, 10, 10, 10, 10],
[1, 1, 2, 5, 5, 5, 10, 10, 10, 10, 10],
],
// Default ticks based on SVG size. These values have high numbers
// of divisors. The array is indexed using a calculated maxTicks value
// divided by 2 simply to halve the array size. See Scales.niceScale().
niceScaleDefaultTicks: [
1, 2, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 12, 12, 12, 12,
12, 12, 12, 12, 12, 24,
],
seriesYAxisMap: [], // Given yAxis index, return all series indices belonging to it. Multiple series can be referenced to each yAxis.
seriesYAxisReverseMap: [], // Given a Series index, return its yAxis index.
}
}
init(config) {
let globals = this.globalVars(config)
this.initGlobalVars(globals)
globals.initialConfig = Utils.extend({}, config)
globals.initialSeries = Utils.clone(config.series)
globals.lastXAxis = Utils.clone(globals.initialConfig.xaxis)
globals.lastYAxis = Utils.clone(globals.initialConfig.yaxis)
return globals
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
/**
* ApexCharts Tooltip.AxesTooltip Class.
* This file deals with the x-axis and y-axis tooltips.
*
* @module Tooltip.AxesTooltip
**/
class AxesTooltip {
constructor(tooltipContext) {
this.w = tooltipContext.w
this.ttCtx = tooltipContext
}
/**
* This method adds the secondary tooltip which appears below x axis
* @memberof Tooltip
**/
drawXaxisTooltip() {
let w = this.w
const ttCtx = this.ttCtx
const isBottom = w.config.xaxis.position === 'bottom'
ttCtx.xaxisOffY = isBottom
? w.globals.gridHeight + 1
: -w.globals.xAxisHeight - w.config.xaxis.axisTicks.height + 3
const tooltipCssClass = isBottom
? 'apexcharts-xaxistooltip apexcharts-xaxistooltip-bottom'
: 'apexcharts-xaxistooltip apexcharts-xaxistooltip-top'
let renderTo = w.globals.dom.elWrap
if (ttCtx.isXAxisTooltipEnabled) {
let xaxisTooltip = w.globals.dom.baseEl.querySelector(
'.apexcharts-xaxistooltip'
)
if (xaxisTooltip === null) {
ttCtx.xaxisTooltip = document.createElement('div')
ttCtx.xaxisTooltip.setAttribute(
'class',
tooltipCssClass + ' apexcharts-theme-' + w.config.tooltip.theme
)
renderTo.appendChild(ttCtx.xaxisTooltip)
ttCtx.xaxisTooltipText = document.createElement('div')
ttCtx.xaxisTooltipText.classList.add('apexcharts-xaxistooltip-text')
ttCtx.xaxisTooltipText.style.fontFamily =
w.config.xaxis.tooltip.style.fontFamily || w.config.chart.fontFamily
ttCtx.xaxisTooltipText.style.fontSize =
w.config.xaxis.tooltip.style.fontSize
ttCtx.xaxisTooltip.appendChild(ttCtx.xaxisTooltipText)
}
}
}
/**
* This method adds the secondary tooltip which appears below x axis
* @memberof Tooltip
**/
drawYaxisTooltip() {
let w = this.w
const ttCtx = this.ttCtx
for (let i = 0; i < w.config.yaxis.length; i++) {
const isRight =
w.config.yaxis[i].opposite || w.config.yaxis[i].crosshairs.opposite
ttCtx.yaxisOffX = isRight ? w.globals.gridWidth + 1 : 1
let tooltipCssClass = isRight
? `apexcharts-yaxistooltip apexcharts-yaxistooltip-${i} apexcharts-yaxistooltip-right`
: `apexcharts-yaxistooltip apexcharts-yaxistooltip-${i} apexcharts-yaxistooltip-left`
let renderTo = w.globals.dom.elWrap
let yaxisTooltip = w.globals.dom.baseEl.querySelector(
`.apexcharts-yaxistooltip apexcharts-yaxistooltip-${i}`
)
if (yaxisTooltip === null) {
ttCtx.yaxisTooltip = document.createElement('div')
ttCtx.yaxisTooltip.setAttribute(
'class',
tooltipCssClass + ' apexcharts-theme-' + w.config.tooltip.theme
)
renderTo.appendChild(ttCtx.yaxisTooltip)
if (i === 0) ttCtx.yaxisTooltipText = []
ttCtx.yaxisTooltipText[i] = document.createElement('div')
ttCtx.yaxisTooltipText[i].classList.add('apexcharts-yaxistooltip-text')
ttCtx.yaxisTooltip.appendChild(ttCtx.yaxisTooltipText[i])
}
}
}
/**
* @memberof Tooltip
**/
setXCrosshairWidth() {
let w = this.w
const ttCtx = this.ttCtx
// set xcrosshairs width
const xcrosshairs = ttCtx.getElXCrosshairs()
ttCtx.xcrosshairsWidth = parseInt(w.config.xaxis.crosshairs.width, 10)
if (!w.globals.comboCharts) {
if (w.config.xaxis.crosshairs.width === 'tickWidth') {
let count = w.globals.labels.length
ttCtx.xcrosshairsWidth = w.globals.gridWidth / count
} else if (w.config.xaxis.crosshairs.width === 'barWidth') {
let bar = w.globals.dom.baseEl.querySelector('.apexcharts-bar-area')
if (bar !== null) {
let barWidth = parseFloat(bar.getAttribute('barWidth'))
ttCtx.xcrosshairsWidth = barWidth
} else {
ttCtx.xcrosshairsWidth = 1
}
}
} else {
let bar = w.globals.dom.baseEl.querySelector('.apexcharts-bar-area')
if (bar !== null && w.config.xaxis.crosshairs.width === 'barWidth') {
let barWidth = parseFloat(bar.getAttribute('barWidth'))
ttCtx.xcrosshairsWidth = barWidth
} else {
if (w.config.xaxis.crosshairs.width === 'tickWidth') {
let count = w.globals.labels.length
ttCtx.xcrosshairsWidth = w.globals.gridWidth / count
}
}
}
if (w.globals.isBarHorizontal) {
ttCtx.xcrosshairsWidth = 0
}
if (xcrosshairs !== null && ttCtx.xcrosshairsWidth > 0) {
xcrosshairs.setAttribute('width', ttCtx.xcrosshairsWidth)
}
}
handleYCrosshair() {
let w = this.w
const ttCtx = this.ttCtx
// set ycrosshairs height
ttCtx.ycrosshairs = w.globals.dom.baseEl.querySelector(
'.apexcharts-ycrosshairs'
)
ttCtx.ycrosshairsHidden = w.globals.dom.baseEl.querySelector(
'.apexcharts-ycrosshairs-hidden'
)
}
drawYaxisTooltipText(index, clientY, xyRatios) {
const ttCtx = this.ttCtx
const w = this.w
const gl = w.globals
const yAxisSeriesArr = gl.seriesYAxisMap[index]
if (ttCtx.yaxisTooltips[index] && yAxisSeriesArr.length > 0) {
const lbFormatter = gl.yLabelFormatters[index]
const elGrid = ttCtx.getElGrid()
const seriesBound = elGrid.getBoundingClientRect()
// We can use the index of any series referenced by the Yaxis
// because they will all return the same value.
const seriesIndex = yAxisSeriesArr[0]
let translationsIndex = 0
if (xyRatios.yRatio.length > 1) {
translationsIndex = seriesIndex
}
const hoverY =
(clientY - seriesBound.top) * xyRatios.yRatio[translationsIndex]
const height = gl.maxYArr[seriesIndex] - gl.minYArr[seriesIndex]
let val = gl.minYArr[seriesIndex] + (height - hoverY)
if (w.config.yaxis[index].reversed) {
val = gl.maxYArr[seriesIndex] - (height - hoverY)
}
ttCtx.tooltipPosition.moveYCrosshairs(clientY - seriesBound.top)
ttCtx.yaxisTooltipText[index].innerHTML = lbFormatter(val)
ttCtx.tooltipPosition.moveYAxisTooltip(index)
}
}
}
export default AxesTooltip

View File

@@ -0,0 +1,344 @@
import Utils from '../../utils/Utils'
/**
* ApexCharts Tooltip.Intersect Class.
* This file deals with functions related to intersecting tooltips
* (tooltips that appear when user hovers directly over a data-point whether)
*
* @module Tooltip.Intersect
**/
class Intersect {
constructor(tooltipContext) {
this.w = tooltipContext.w
const w = this.w
this.ttCtx = tooltipContext
this.isVerticalGroupedRangeBar =
!w.globals.isBarHorizontal &&
w.config.chart.type === 'rangeBar' &&
w.config.plotOptions.bar.rangeBarGroupRows
}
// a helper function to get an element's attribute value
getAttr(e, attr) {
return parseFloat(e.target.getAttribute(attr))
}
// handle tooltip for heatmaps and treemaps
handleHeatTreeTooltip({ e, opt, x, y, type }) {
const ttCtx = this.ttCtx
const w = this.w
if (e.target.classList.contains(`apexcharts-${type}-rect`)) {
let i = this.getAttr(e, 'i')
let j = this.getAttr(e, 'j')
let cx = this.getAttr(e, 'cx')
let cy = this.getAttr(e, 'cy')
let width = this.getAttr(e, 'width')
let height = this.getAttr(e, 'height')
ttCtx.tooltipLabels.drawSeriesTexts({
ttItems: opt.ttItems,
i,
j,
shared: false,
e,
})
w.globals.capturedSeriesIndex = i
w.globals.capturedDataPointIndex = j
x = cx + ttCtx.tooltipRect.ttWidth / 2 + width
y = cy + ttCtx.tooltipRect.ttHeight / 2 - height / 2
ttCtx.tooltipPosition.moveXCrosshairs(cx + width / 2)
if (x > w.globals.gridWidth / 2) {
x = cx - ttCtx.tooltipRect.ttWidth / 2 + width
}
if (ttCtx.w.config.tooltip.followCursor) {
let seriesBound = w.globals.dom.elWrap.getBoundingClientRect()
x =
w.globals.clientX -
seriesBound.left -
(x > w.globals.gridWidth / 2 ? ttCtx.tooltipRect.ttWidth : 0)
y =
w.globals.clientY -
seriesBound.top -
(y > w.globals.gridHeight / 2 ? ttCtx.tooltipRect.ttHeight : 0)
}
}
return {
x,
y,
}
}
/**
* handle tooltips for line/area/scatter charts where tooltip.intersect is true
* when user hovers over the marker directly, this function is executed
*/
handleMarkerTooltip({ e, opt, x, y }) {
let w = this.w
const ttCtx = this.ttCtx
let i
let j
if (e.target.classList.contains('apexcharts-marker')) {
let cx = parseInt(opt.paths.getAttribute('cx'), 10)
let cy = parseInt(opt.paths.getAttribute('cy'), 10)
let val = parseFloat(opt.paths.getAttribute('val'))
j = parseInt(opt.paths.getAttribute('rel'), 10)
i =
parseInt(
opt.paths.parentNode.parentNode.parentNode.getAttribute('rel'),
10
) - 1
if (ttCtx.intersect) {
const el = Utils.findAncestor(opt.paths, 'apexcharts-series')
if (el) {
i = parseInt(el.getAttribute('data:realIndex'), 10)
}
}
ttCtx.tooltipLabels.drawSeriesTexts({
ttItems: opt.ttItems,
i,
j,
shared: ttCtx.showOnIntersect ? false : w.config.tooltip.shared,
e,
})
if (e.type === 'mouseup') {
ttCtx.markerClick(e, i, j)
}
w.globals.capturedSeriesIndex = i
w.globals.capturedDataPointIndex = j
x = cx
y = cy + w.globals.translateY - ttCtx.tooltipRect.ttHeight * 1.4
if (ttCtx.w.config.tooltip.followCursor) {
const elGrid = ttCtx.getElGrid()
const seriesBound = elGrid.getBoundingClientRect()
y = ttCtx.e.clientY + w.globals.translateY - seriesBound.top
}
if (val < 0) {
y = cy
}
ttCtx.marker.enlargeCurrentPoint(j, opt.paths, x, y)
}
return {
x,
y,
}
}
/**
* handle tooltips for bar/column charts
*/
handleBarTooltip({ e, opt }) {
const w = this.w
const ttCtx = this.ttCtx
const tooltipEl = ttCtx.getElTooltip()
let bx = 0
let x = 0
let y = 0
let i = 0
let strokeWidth
let barXY = this.getBarTooltipXY({
e,
opt,
})
if (barXY.j === null && barXY.barHeight === 0 && barXY.barWidth === 0) {
return // bar was not hovered and didn't receive correct coords
}
i = barXY.i
let j = barXY.j
w.globals.capturedSeriesIndex = i
w.globals.capturedDataPointIndex = j
if (
(w.globals.isBarHorizontal && ttCtx.tooltipUtil.hasBars()) ||
!w.config.tooltip.shared
) {
x = barXY.x
y = barXY.y
strokeWidth = Array.isArray(w.config.stroke.width)
? w.config.stroke.width[i]
: w.config.stroke.width
bx = x
} else {
if (!w.globals.comboCharts && !w.config.tooltip.shared) {
// todo: re-check this condition as it's always 0
bx = bx / 2
}
}
// y is NaN, make it touch the bottom of grid area
if (isNaN(y)) {
y = w.globals.svgHeight - ttCtx.tooltipRect.ttHeight
}
const seriesIndex = parseInt(
opt.paths.parentNode.getAttribute('data:realIndex'),
10
)
if (x + ttCtx.tooltipRect.ttWidth > w.globals.gridWidth) {
x = x - ttCtx.tooltipRect.ttWidth
} else if (x < 0) {
x = 0
}
if (ttCtx.w.config.tooltip.followCursor) {
const elGrid = ttCtx.getElGrid()
const seriesBound = elGrid.getBoundingClientRect()
y = ttCtx.e.clientY - seriesBound.top
}
// if tooltip is still null, querySelector
if (ttCtx.tooltip === null) {
ttCtx.tooltip = w.globals.dom.baseEl.querySelector('.apexcharts-tooltip')
}
if (!w.config.tooltip.shared) {
if (w.globals.comboBarCount > 0) {
ttCtx.tooltipPosition.moveXCrosshairs(bx + strokeWidth / 2)
} else {
ttCtx.tooltipPosition.moveXCrosshairs(bx)
}
}
// move tooltip here
if (
!ttCtx.fixedTooltip &&
(!w.config.tooltip.shared ||
(w.globals.isBarHorizontal && ttCtx.tooltipUtil.hasBars()))
) {
y = y + w.globals.translateY - ttCtx.tooltipRect.ttHeight / 2
tooltipEl.style.left = x + w.globals.translateX + 'px'
tooltipEl.style.top = y + 'px'
}
}
getBarTooltipXY({ e, opt }) {
let w = this.w
let j = null
const ttCtx = this.ttCtx
let i = 0
let x = 0
let y = 0
let barWidth = 0
let barHeight = 0
const cl = e.target.classList
if (
cl.contains('apexcharts-bar-area') ||
cl.contains('apexcharts-candlestick-area') ||
cl.contains('apexcharts-boxPlot-area') ||
cl.contains('apexcharts-rangebar-area')
) {
let bar = e.target
let barRect = bar.getBoundingClientRect()
let seriesBound = opt.elGrid.getBoundingClientRect()
let bh = barRect.height
barHeight = barRect.height
let bw = barRect.width
let cx = parseInt(bar.getAttribute('cx'), 10)
let cy = parseInt(bar.getAttribute('cy'), 10)
barWidth = parseFloat(bar.getAttribute('barWidth'))
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX
j = parseInt(bar.getAttribute('j'), 10)
i = parseInt(bar.parentNode.getAttribute('rel'), 10) - 1
let y1 = bar.getAttribute('data-range-y1')
let y2 = bar.getAttribute('data-range-y2')
if (w.globals.comboCharts) {
i = parseInt(bar.parentNode.getAttribute('data:realIndex'), 10)
}
const handleXForColumns = (x) => {
if (w.globals.isXNumeric) {
x = cx - bw / 2
} else {
if (this.isVerticalGroupedRangeBar) {
x = cx + bw / 2
} else {
x = cx - ttCtx.dataPointsDividedWidth + bw / 2
}
}
return x
}
const handleYForBars = () => {
return (
cy -
ttCtx.dataPointsDividedHeight +
bh / 2 -
ttCtx.tooltipRect.ttHeight / 2
)
}
ttCtx.tooltipLabels.drawSeriesTexts({
ttItems: opt.ttItems,
i,
j,
y1: y1 ? parseInt(y1, 10) : null,
y2: y2 ? parseInt(y2, 10) : null,
shared: ttCtx.showOnIntersect ? false : w.config.tooltip.shared,
e,
})
if (w.config.tooltip.followCursor) {
if (w.globals.isBarHorizontal) {
x = clientX - seriesBound.left + 15
y = handleYForBars()
} else {
x = handleXForColumns(x)
y = e.clientY - seriesBound.top - ttCtx.tooltipRect.ttHeight / 2 - 15
}
} else {
if (w.globals.isBarHorizontal) {
x = cx
if (x < ttCtx.xyRatios.baseLineInvertedY) {
x = cx - ttCtx.tooltipRect.ttWidth
}
y = handleYForBars()
} else {
x = handleXForColumns(x)
y = cy // - ttCtx.tooltipRect.ttHeight / 2 + 10
}
}
}
return {
x,
y,
barHeight,
barWidth,
i,
j,
}
}
}
export default Intersect

View File

@@ -0,0 +1,546 @@
import Formatters from '../Formatters'
import DateTime from '../../utils/DateTime'
import Utils from './Utils'
import Data from '../Data'
/**
* ApexCharts Tooltip.Labels Class to draw texts on the tooltip.
* This file deals with printing actual text on the tooltip.
*
* @module Tooltip.Labels
**/
export default class Labels {
constructor(tooltipContext) {
this.w = tooltipContext.w
this.ctx = tooltipContext.ctx
this.ttCtx = tooltipContext
this.tooltipUtil = new Utils(tooltipContext)
}
drawSeriesTexts({ shared = true, ttItems, i = 0, j = null, y1, y2, e }) {
let w = this.w
if (w.config.tooltip.custom !== undefined) {
this.handleCustomTooltip({ i, j, y1, y2, w })
} else {
this.toggleActiveInactiveSeries(shared, i)
}
let values = this.getValuesToPrint({
i,
j,
})
this.printLabels({
i,
j,
values,
ttItems,
shared,
e,
})
// Re-calculate tooltip dimensions now that we have drawn the text
const tooltipEl = this.ttCtx.getElTooltip()
this.ttCtx.tooltipRect.ttWidth = tooltipEl.getBoundingClientRect().width
this.ttCtx.tooltipRect.ttHeight = tooltipEl.getBoundingClientRect().height
}
printLabels({ i, j, values, ttItems, shared, e }) {
const w = this.w
let val
let goalVals = []
const hasGoalValues = (gi) => {
return (
w.globals.seriesGoals[gi] &&
w.globals.seriesGoals[gi][j] &&
Array.isArray(w.globals.seriesGoals[gi][j])
)
}
const { xVal, zVal, xAxisTTVal } = values
let seriesName = ''
let pColor = w.globals.colors[i] // The pColor here is for the markers inside tooltip
if (j !== null && w.config.plotOptions.bar.distributed) {
pColor = w.globals.colors[j]
}
for (
let t = 0, inverset = w.globals.series.length - 1;
t < w.globals.series.length;
t++, inverset--
) {
let f = this.getFormatters(i)
seriesName = this.getSeriesName({
fn: f.yLbTitleFormatter,
index: i,
seriesIndex: i,
j,
})
if (w.config.chart.type === 'treemap') {
seriesName = f.yLbTitleFormatter(String(w.config.series[i].data[j].x), {
series: w.globals.series,
seriesIndex: i,
dataPointIndex: j,
w,
})
}
const tIndex = w.config.tooltip.inverseOrder ? inverset : t
if (w.globals.axisCharts) {
const getValBySeriesIndex = (index) => {
if (w.globals.isRangeData) {
return (
f.yLbFormatter(w.globals.seriesRangeStart?.[index]?.[j], {
series: w.globals.seriesRangeStart,
seriesIndex: index,
dataPointIndex: j,
w,
}) +
' - ' +
f.yLbFormatter(w.globals.seriesRangeEnd?.[index]?.[j], {
series: w.globals.seriesRangeEnd,
seriesIndex: index,
dataPointIndex: j,
w,
})
)
}
return f.yLbFormatter(w.globals.series[index][j], {
series: w.globals.series,
seriesIndex: index,
dataPointIndex: j,
w,
})
}
if (shared) {
f = this.getFormatters(tIndex)
seriesName = this.getSeriesName({
fn: f.yLbTitleFormatter,
index: tIndex,
seriesIndex: i,
j,
})
pColor = w.globals.colors[tIndex]
val = getValBySeriesIndex(tIndex)
if (hasGoalValues(tIndex)) {
goalVals = w.globals.seriesGoals[tIndex][j].map((goal) => {
return {
attrs: goal,
val: f.yLbFormatter(goal.value, {
seriesIndex: tIndex,
dataPointIndex: j,
w,
}),
}
})
}
} else {
// get a color from a hover area (if it's a line pattern then get from a first line)
const targetFill = e?.target?.getAttribute('fill')
if (targetFill) {
if (targetFill.indexOf('url') !== -1) {
// pattern fill
if (targetFill.indexOf('Pattern') !== -1) {
pColor = w.globals.dom.baseEl
.querySelector(targetFill.substr(4).slice(0, -1))
.childNodes[0].getAttribute('stroke')
}
} else {
pColor = targetFill
}
}
val = getValBySeriesIndex(i)
if (hasGoalValues(i) && Array.isArray(w.globals.seriesGoals[i][j])) {
goalVals = w.globals.seriesGoals[i][j].map((goal) => {
return {
attrs: goal,
val: f.yLbFormatter(goal.value, {
seriesIndex: i,
dataPointIndex: j,
w,
}),
}
})
}
}
}
// for pie / donuts
if (j === null) {
val = f.yLbFormatter(w.globals.series[i], {
...w,
seriesIndex: i,
dataPointIndex: i,
})
}
this.DOMHandling({
i,
t: tIndex,
j,
ttItems,
values: {
val,
goalVals,
xVal,
xAxisTTVal,
zVal,
},
seriesName,
shared,
pColor,
})
}
}
getFormatters(i) {
const w = this.w
let yLbFormatter = w.globals.yLabelFormatters[i]
let yLbTitleFormatter
if (w.globals.ttVal !== undefined) {
if (Array.isArray(w.globals.ttVal)) {
yLbFormatter = w.globals.ttVal[i] && w.globals.ttVal[i].formatter
yLbTitleFormatter =
w.globals.ttVal[i] &&
w.globals.ttVal[i].title &&
w.globals.ttVal[i].title.formatter
} else {
yLbFormatter = w.globals.ttVal.formatter
if (typeof w.globals.ttVal.title.formatter === 'function') {
yLbTitleFormatter = w.globals.ttVal.title.formatter
}
}
} else {
yLbTitleFormatter = w.config.tooltip.y.title.formatter
}
if (typeof yLbFormatter !== 'function') {
if (w.globals.yLabelFormatters[0]) {
yLbFormatter = w.globals.yLabelFormatters[0]
} else {
yLbFormatter = function (label) {
return label
}
}
}
if (typeof yLbTitleFormatter !== 'function') {
yLbTitleFormatter = function (label) {
// refrence used from line: 966 in Options.js
return label ? label + ': ' : ''
}
}
return {
yLbFormatter,
yLbTitleFormatter,
}
}
getSeriesName({ fn, index, seriesIndex, j }) {
const w = this.w
return fn(String(w.globals.seriesNames[index]), {
series: w.globals.series,
seriesIndex,
dataPointIndex: j,
w,
})
}
DOMHandling({ i, t, j, ttItems, values, seriesName, shared, pColor }) {
const w = this.w
const ttCtx = this.ttCtx
const { val, goalVals, xVal, xAxisTTVal, zVal } = values
let ttItemsChildren = null
ttItemsChildren = ttItems[t].children
if (w.config.tooltip.fillSeriesColor) {
ttItems[t].style.color = pColor
ttItemsChildren[0].style.display = 'none'
}
if (ttCtx.showTooltipTitle) {
if (ttCtx.tooltipTitle === null) {
// get it once if null, and store it in class property
ttCtx.tooltipTitle = w.globals.dom.baseEl.querySelector(
'.apexcharts-tooltip-title'
)
}
ttCtx.tooltipTitle.innerHTML = xVal
}
// if xaxis tooltip is constructed, we need to replace the innerHTML
if (ttCtx.isXAxisTooltipEnabled) {
ttCtx.xaxisTooltipText.innerHTML = xAxisTTVal !== '' ? xAxisTTVal : xVal
}
const ttYLabel = ttItems[t].querySelector(
'.apexcharts-tooltip-text-y-label'
)
if (ttYLabel) {
ttYLabel.innerHTML = seriesName ? seriesName : ''
}
const ttYVal = ttItems[t].querySelector('.apexcharts-tooltip-text-y-value')
if (ttYVal) {
ttYVal.innerHTML = typeof val !== 'undefined' ? val : ''
}
if (
ttItemsChildren[0] &&
ttItemsChildren[0].classList.contains('apexcharts-tooltip-marker')
) {
if (
w.config.tooltip.marker.fillColors &&
Array.isArray(w.config.tooltip.marker.fillColors)
) {
pColor = w.config.tooltip.marker.fillColors[t]
}
ttItemsChildren[0].style.color = pColor
}
if (!w.config.tooltip.marker.show) {
ttItemsChildren[0].style.display = 'none'
}
const ttGLabel = ttItems[t].querySelector(
'.apexcharts-tooltip-text-goals-label'
)
const ttGVal = ttItems[t].querySelector(
'.apexcharts-tooltip-text-goals-value'
)
if (goalVals.length && w.globals.seriesGoals[t]) {
const createGoalsHtml = () => {
let gLabels = '<div>'
let gVals = '<div>'
goalVals.forEach((goal, gi) => {
gLabels += ` <div style="display: flex"><span class="apexcharts-tooltip-marker" style="background-color: ${goal.attrs.strokeColor}; height: 3px; border-radius: 0; top: 5px;"></span> ${goal.attrs.name}</div>`
gVals += `<div>${goal.val}</div>`
})
ttGLabel.innerHTML = gLabels + `</div>`
ttGVal.innerHTML = gVals + `</div>`
}
if (shared) {
if (
w.globals.seriesGoals[t][j] &&
Array.isArray(w.globals.seriesGoals[t][j])
) {
createGoalsHtml()
} else {
ttGLabel.innerHTML = ''
ttGVal.innerHTML = ''
}
} else {
createGoalsHtml()
}
} else {
ttGLabel.innerHTML = ''
ttGVal.innerHTML = ''
}
if (zVal !== null) {
const ttZLabel = ttItems[t].querySelector(
'.apexcharts-tooltip-text-z-label'
)
ttZLabel.innerHTML = w.config.tooltip.z.title
const ttZVal = ttItems[t].querySelector(
'.apexcharts-tooltip-text-z-value'
)
ttZVal.innerHTML = typeof zVal !== 'undefined' ? zVal : ''
}
if (shared && ttItemsChildren[0]) {
// hide when no Val or series collapsed
if (w.config.tooltip.hideEmptySeries) {
let ttItemMarker = ttItems[t].querySelector(
'.apexcharts-tooltip-marker'
)
let ttItemText = ttItems[t].querySelector('.apexcharts-tooltip-text')
if (parseFloat(val) == 0) {
ttItemMarker.style.display = 'none'
ttItemText.style.display = 'none'
} else {
ttItemMarker.style.display = 'block'
ttItemText.style.display = 'block'
}
}
if (
typeof val === 'undefined' ||
val === null ||
w.globals.ancillaryCollapsedSeriesIndices.indexOf(t) > -1 ||
w.globals.collapsedSeriesIndices.indexOf(t) > -1 ||
(Array.isArray(ttCtx.tConfig.enabledOnSeries) &&
ttCtx.tConfig.enabledOnSeries.indexOf(t) === -1)
) {
ttItemsChildren[0].parentNode.style.display = 'none'
} else {
ttItemsChildren[0].parentNode.style.display =
w.config.tooltip.items.display
}
} else {
if (
Array.isArray(ttCtx.tConfig.enabledOnSeries) &&
ttCtx.tConfig.enabledOnSeries.indexOf(t) === -1
) {
ttItemsChildren[0].parentNode.style.display = 'none'
}
}
}
toggleActiveInactiveSeries(shared, i) {
const w = this.w
if (shared) {
// make all tooltips active
this.tooltipUtil.toggleAllTooltipSeriesGroups('enable')
} else {
// disable all tooltip text groups
this.tooltipUtil.toggleAllTooltipSeriesGroups('disable')
// enable the first tooltip text group
let firstTooltipSeriesGroup = w.globals.dom.baseEl.querySelector(
`.apexcharts-tooltip-series-group-${i}`
)
if (firstTooltipSeriesGroup) {
firstTooltipSeriesGroup.classList.add('apexcharts-active')
firstTooltipSeriesGroup.style.display = w.config.tooltip.items.display
}
}
}
getValuesToPrint({ i, j }) {
const w = this.w
const filteredSeriesX = this.ctx.series.filteredSeriesX()
let xVal = ''
let xAxisTTVal = ''
let zVal = null
let val = null
const customFormatterOpts = {
series: w.globals.series,
seriesIndex: i,
dataPointIndex: j,
w,
}
let zFormatter = w.globals.ttZFormatter
if (j === null) {
val = w.globals.series[i]
} else {
if (w.globals.isXNumeric && w.config.chart.type !== 'treemap') {
xVal = filteredSeriesX[i][j]
if (filteredSeriesX[i].length === 0) {
// a series (possibly the first one) might be collapsed, so get the next active index
const firstActiveSeriesIndex =
this.tooltipUtil.getFirstActiveXArray(filteredSeriesX)
xVal = filteredSeriesX[firstActiveSeriesIndex][j]
}
} else {
const dataFormat = new Data(this.ctx)
if (dataFormat.isFormatXY()) {
xVal =
typeof w.config.series[i].data[j] !== 'undefined'
? w.config.series[i].data[j].x
: ''
} else {
xVal =
typeof w.globals.labels[j] !== 'undefined'
? w.globals.labels[j]
: ''
}
}
}
let bufferXVal = xVal
if (w.globals.isXNumeric && w.config.xaxis.type === 'datetime') {
let xFormat = new Formatters(this.ctx)
xVal = xFormat.xLabelFormat(
w.globals.ttKeyFormatter,
bufferXVal,
bufferXVal,
{
i: undefined,
dateFormatter: new DateTime(this.ctx).formatDate,
w: this.w,
}
)
} else {
if (w.globals.isBarHorizontal) {
xVal = w.globals.yLabelFormatters[0](bufferXVal, customFormatterOpts)
} else {
xVal = w.globals.xLabelFormatter(bufferXVal, customFormatterOpts)
}
}
// override default x-axis formatter with tooltip formatter
if (w.config.tooltip.x.formatter !== undefined) {
xVal = w.globals.ttKeyFormatter(bufferXVal, customFormatterOpts)
}
if (w.globals.seriesZ.length > 0 && w.globals.seriesZ[i].length > 0) {
zVal = zFormatter(w.globals.seriesZ[i][j], w)
}
if (typeof w.config.xaxis.tooltip.formatter === 'function') {
xAxisTTVal = w.globals.xaxisTooltipFormatter(
bufferXVal,
customFormatterOpts
)
} else {
xAxisTTVal = xVal
}
return {
val: Array.isArray(val) ? val.join(' ') : val,
xVal: Array.isArray(xVal) ? xVal.join(' ') : xVal,
xAxisTTVal: Array.isArray(xAxisTTVal) ? xAxisTTVal.join(' ') : xAxisTTVal,
zVal,
}
}
handleCustomTooltip({ i, j, y1, y2, w }) {
const tooltipEl = this.ttCtx.getElTooltip()
let fn = w.config.tooltip.custom
if (Array.isArray(fn) && fn[i]) {
fn = fn[i]
}
const customTooltip = fn({
ctx: this.ctx,
series: w.globals.series,
seriesIndex: i,
dataPointIndex: j,
y1,
y2,
w,
})
if (typeof customTooltip === 'string') {
tooltipEl.innerHTML = customTooltip
} else if (
customTooltip instanceof Element ||
typeof customTooltip.nodeName === 'string'
) {
tooltipEl.innerHTML = ''
tooltipEl.appendChild(customTooltip.cloneNode(true))
}
}
}

View File

@@ -0,0 +1,195 @@
import Graphics from '../Graphics'
import Position from './Position'
import Markers from '../../modules/Markers'
import Utils from '../../utils/Utils'
/**
* ApexCharts Tooltip.Marker Class to draw texts on the tooltip.
* This file deals with the markers that appear near tooltip in line/area charts.
* These markers helps the user to associate the data-points and the values
* that are shown in the tooltip
*
* @module Tooltip.Marker
**/
export default class Marker {
constructor(tooltipContext) {
this.w = tooltipContext.w
this.ttCtx = tooltipContext
this.ctx = tooltipContext.ctx
this.tooltipPosition = new Position(tooltipContext)
}
drawDynamicPoints() {
let w = this.w
let graphics = new Graphics(this.ctx)
let marker = new Markers(this.ctx)
let elsSeries = w.globals.dom.baseEl.querySelectorAll('.apexcharts-series')
elsSeries = [...elsSeries]
if (w.config.chart.stacked) {
elsSeries.sort((a, b) => {
return (
parseFloat(a.getAttribute('data:realIndex')) -
parseFloat(b.getAttribute('data:realIndex'))
)
})
}
for (let i = 0; i < elsSeries.length; i++) {
let pointsMain = elsSeries[i].querySelector(
`.apexcharts-series-markers-wrap`
)
if (pointsMain !== null) {
// it can be null as we have tooltips in donut/bar charts
let point
let PointClasses = `apexcharts-marker w${(Math.random() + 1)
.toString(36)
.substring(4)}`
if (
(w.config.chart.type === 'line' || w.config.chart.type === 'area') &&
!w.globals.comboCharts &&
!w.config.tooltip.intersect
) {
PointClasses += ' no-pointer-events'
}
let elPointOptions = marker.getMarkerConfig({
cssClass: PointClasses,
seriesIndex: Number(pointsMain.getAttribute('data:realIndex')), // fixes apexcharts/apexcharts.js #1427
})
point = graphics.drawMarker(0, 0, elPointOptions)
point.node.setAttribute('default-marker-size', 0)
let elPointsG = document.createElementNS(w.globals.SVGNS, 'g')
elPointsG.classList.add('apexcharts-series-markers')
elPointsG.appendChild(point.node)
pointsMain.appendChild(elPointsG)
}
}
}
enlargeCurrentPoint(rel, point, x = null, y = null) {
let w = this.w
if (w.config.chart.type !== 'bubble') {
this.newPointSize(rel, point)
}
let cx = point.getAttribute('cx')
let cy = point.getAttribute('cy')
if (x !== null && y !== null) {
cx = x
cy = y
}
this.tooltipPosition.moveXCrosshairs(cx)
if (!this.fixedTooltip) {
if (w.config.chart.type === 'radar') {
const elGrid = this.ttCtx.getElGrid()
const seriesBound = elGrid.getBoundingClientRect()
cx = this.ttCtx.e.clientX - seriesBound.left
}
this.tooltipPosition.moveTooltip(cx, cy, w.config.markers.hover.size)
}
}
enlargePoints(j) {
let w = this.w
let me = this
const ttCtx = this.ttCtx
let col = j
let points = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-series:not(.apexcharts-series-collapsed) .apexcharts-marker'
)
let newSize = w.config.markers.hover.size
for (let p = 0; p < points.length; p++) {
let rel = points[p].getAttribute('rel')
let index = points[p].getAttribute('index')
if (newSize === undefined) {
newSize =
w.globals.markers.size[index] + w.config.markers.hover.sizeOffset
}
if (col === parseInt(rel, 10)) {
me.newPointSize(col, points[p])
let cx = points[p].getAttribute('cx')
let cy = points[p].getAttribute('cy')
me.tooltipPosition.moveXCrosshairs(cx)
if (!ttCtx.fixedTooltip) {
me.tooltipPosition.moveTooltip(cx, cy, newSize)
}
} else {
me.oldPointSize(points[p])
}
}
}
newPointSize(rel, point) {
let w = this.w
let newSize = w.config.markers.hover.size
let elPoint =
rel === 0 ? point.parentNode.firstChild : point.parentNode.lastChild
if (elPoint.getAttribute('default-marker-size') !== '0') {
const index = parseInt(elPoint.getAttribute('index'), 10)
if (newSize === undefined) {
newSize =
w.globals.markers.size[index] + w.config.markers.hover.sizeOffset
}
if (newSize < 0) {
newSize = 0
}
const path = this.ttCtx.tooltipUtil.getPathFromPoint(point, newSize)
point.setAttribute('d', path)
}
}
oldPointSize(point) {
const size = parseFloat(point.getAttribute('default-marker-size'))
const path = this.ttCtx.tooltipUtil.getPathFromPoint(point, size)
point.setAttribute('d', path)
}
resetPointsSize() {
let w = this.w
let points = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-series:not(.apexcharts-series-collapsed) .apexcharts-marker'
)
for (let p = 0; p < points.length; p++) {
const size = parseFloat(points[p].getAttribute('default-marker-size'))
if (Utils.isNumber(size) && size > 0) {
const path = this.ttCtx.tooltipUtil.getPathFromPoint(points[p], size)
points[p].setAttribute('d', path)
} else {
points[p].setAttribute('d', 'M0,0')
}
}
}
}

View File

@@ -0,0 +1,451 @@
import Graphics from '../Graphics'
import Series from '../Series'
/**
* ApexCharts Tooltip.Position Class to move the tooltip based on x and y position.
*
* @module Tooltip.Position
**/
export default class Position {
constructor(tooltipContext) {
this.ttCtx = tooltipContext
this.ctx = tooltipContext.ctx
this.w = tooltipContext.w
}
/**
* This will move the crosshair (the vertical/horz line that moves along with mouse)
* Along with this, this function also calls the xaxisMove function
* @memberof Position
* @param {int} - cx = point's x position, wherever point's x is, you need to move crosshair
*/
moveXCrosshairs(cx, j = null) {
const ttCtx = this.ttCtx
let w = this.w
const xcrosshairs = ttCtx.getElXCrosshairs()
let x = cx - ttCtx.xcrosshairsWidth / 2
let tickAmount = w.globals.labels.slice().length
if (j !== null) {
x = (w.globals.gridWidth / tickAmount) * j
}
if (xcrosshairs !== null && !w.globals.isBarHorizontal) {
xcrosshairs.setAttribute('x', x)
xcrosshairs.setAttribute('x1', x)
xcrosshairs.setAttribute('x2', x)
xcrosshairs.setAttribute('y2', w.globals.gridHeight)
xcrosshairs.classList.add('apexcharts-active')
}
if (x < 0) {
x = 0
}
if (x > w.globals.gridWidth) {
x = w.globals.gridWidth
}
if (ttCtx.isXAxisTooltipEnabled) {
let tx = x
if (
w.config.xaxis.crosshairs.width === 'tickWidth' ||
w.config.xaxis.crosshairs.width === 'barWidth'
) {
tx = x + ttCtx.xcrosshairsWidth / 2
}
this.moveXAxisTooltip(tx)
}
}
/**
* This will move the crosshair (the vertical/horz line that moves along with mouse)
* Along with this, this function also calls the xaxisMove function
* @memberof Position
* @param {int} - cx = point's x position, wherever point's x is, you need to move crosshair
*/
moveYCrosshairs(cy) {
const ttCtx = this.ttCtx
if (ttCtx.ycrosshairs !== null) {
Graphics.setAttrs(ttCtx.ycrosshairs, {
y1: cy,
y2: cy,
})
}
if (ttCtx.ycrosshairsHidden !== null) {
Graphics.setAttrs(ttCtx.ycrosshairsHidden, {
y1: cy,
y2: cy,
})
}
}
/**
** AxisTooltip is the small rectangle which appears on x axis with x value, when user moves
* @memberof Position
* @param {int} - cx = point's x position, wherever point's x is, you need to move
*/
moveXAxisTooltip(cx) {
let w = this.w
const ttCtx = this.ttCtx
if (ttCtx.xaxisTooltip !== null && ttCtx.xcrosshairsWidth !== 0) {
ttCtx.xaxisTooltip.classList.add('apexcharts-active')
let cy =
ttCtx.xaxisOffY +
w.config.xaxis.tooltip.offsetY +
w.globals.translateY +
1 +
w.config.xaxis.offsetY
let xaxisTTText = ttCtx.xaxisTooltip.getBoundingClientRect()
let xaxisTTTextWidth = xaxisTTText.width
cx = cx - xaxisTTTextWidth / 2
if (!isNaN(cx)) {
cx = cx + w.globals.translateX
let textRect = 0
const graphics = new Graphics(this.ctx)
textRect = graphics.getTextRects(ttCtx.xaxisTooltipText.innerHTML)
ttCtx.xaxisTooltipText.style.minWidth = textRect.width + 'px'
ttCtx.xaxisTooltip.style.left = cx + 'px'
ttCtx.xaxisTooltip.style.top = cy + 'px'
}
}
}
moveYAxisTooltip(index) {
const w = this.w
const ttCtx = this.ttCtx
if (ttCtx.yaxisTTEls === null) {
ttCtx.yaxisTTEls = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-yaxistooltip'
)
}
const ycrosshairsHiddenRectY1 = parseInt(
ttCtx.ycrosshairsHidden.getAttribute('y1'),
10
)
let cy = w.globals.translateY + ycrosshairsHiddenRectY1
const yAxisTTRect = ttCtx.yaxisTTEls[index].getBoundingClientRect()
const yAxisTTHeight = yAxisTTRect.height
let cx = w.globals.translateYAxisX[index] - 2
if (w.config.yaxis[index].opposite) {
cx = cx - 26
}
cy = cy - yAxisTTHeight / 2
if (w.globals.ignoreYAxisIndexes.indexOf(index) === -1) {
ttCtx.yaxisTTEls[index].classList.add('apexcharts-active')
ttCtx.yaxisTTEls[index].style.top = cy + 'px'
ttCtx.yaxisTTEls[index].style.left =
cx + w.config.yaxis[index].tooltip.offsetX + 'px'
} else {
ttCtx.yaxisTTEls[index].classList.remove('apexcharts-active')
}
}
/**
** moves the whole tooltip by changing x, y attrs
* @memberof Position
* @param {int} - cx = point's x position, wherever point's x is, you need to move tooltip
* @param {int} - cy = point's y position, wherever point's y is, you need to move tooltip
* @param {int} - markerSize = point's size
*/
moveTooltip(cx, cy, markerSize = null) {
let w = this.w
let ttCtx = this.ttCtx
const tooltipEl = ttCtx.getElTooltip()
let tooltipRect = ttCtx.tooltipRect
let pointSize = markerSize !== null ? parseFloat(markerSize) : 1
let x = parseFloat(cx) + pointSize + 5
let y = parseFloat(cy) + pointSize / 2 // - tooltipRect.ttHeight / 2
if (x > w.globals.gridWidth / 2) {
x = x - tooltipRect.ttWidth - pointSize - 10
}
if (x > w.globals.gridWidth - tooltipRect.ttWidth - 10) {
x = w.globals.gridWidth - tooltipRect.ttWidth
}
if (x < -20) {
x = -20
}
if (w.config.tooltip.followCursor) {
const elGrid = ttCtx.getElGrid()
const seriesBound = elGrid.getBoundingClientRect()
x = ttCtx.e.clientX - seriesBound.left
if (x > w.globals.gridWidth / 2) {
x = x - ttCtx.tooltipRect.ttWidth
}
y = ttCtx.e.clientY + w.globals.translateY - seriesBound.top
if (y > w.globals.gridHeight / 2) {
y = y - ttCtx.tooltipRect.ttHeight
}
} else {
if (!w.globals.isBarHorizontal) {
if (tooltipRect.ttHeight / 2 + y > w.globals.gridHeight) {
y = w.globals.gridHeight - tooltipRect.ttHeight + w.globals.translateY
}
}
}
if (!isNaN(x)) {
x = x + w.globals.translateX
tooltipEl.style.left = x + 'px'
tooltipEl.style.top = y + 'px'
}
}
moveMarkers(i, j) {
let w = this.w
let ttCtx = this.ttCtx
if (w.globals.markers.size[i] > 0) {
let allPoints = w.globals.dom.baseEl.querySelectorAll(
` .apexcharts-series[data\\:realIndex='${i}'] .apexcharts-marker`
)
for (let p = 0; p < allPoints.length; p++) {
if (parseInt(allPoints[p].getAttribute('rel'), 10) === j) {
ttCtx.marker.resetPointsSize()
ttCtx.marker.enlargeCurrentPoint(j, allPoints[p])
}
}
} else {
ttCtx.marker.resetPointsSize()
this.moveDynamicPointOnHover(j, i)
}
}
// This function is used when you need to show markers/points only on hover -
// DIFFERENT X VALUES in multiple series
moveDynamicPointOnHover(j, capturedSeries) {
let w = this.w
let ttCtx = this.ttCtx
let cx = 0
let cy = 0
const graphics = new Graphics(this.ctx)
let pointsArr = w.globals.pointsArray
let hoverSize = ttCtx.tooltipUtil.getHoverMarkerSize(capturedSeries)
const serType = w.config.series[capturedSeries].type
if (
serType &&
(serType === 'column' ||
serType === 'candlestick' ||
serType === 'boxPlot')
) {
// fix error mentioned in #811
return
}
cx = pointsArr[capturedSeries][j]?.[0]
cy = pointsArr[capturedSeries][j]?.[1] || 0
let point = w.globals.dom.baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${capturedSeries}'] .apexcharts-series-markers path`
)
if (point && cy < w.globals.gridHeight && cy > 0) {
const shape = point.getAttribute('shape')
const path = graphics.getMarkerPath(cx, cy, shape, hoverSize * 1.5)
point.setAttribute('d', path)
}
this.moveXCrosshairs(cx)
if (!ttCtx.fixedTooltip) {
this.moveTooltip(cx, cy, hoverSize)
}
}
// This function is used when you need to show markers/points only on hover -
// SAME X VALUES in multiple series
moveDynamicPointsOnHover(j) {
const ttCtx = this.ttCtx
let w = ttCtx.w
let cx = 0
let cy = 0
let activeSeries = 0
let pointsArr = w.globals.pointsArray
let series = new Series(this.ctx)
const graphics = new Graphics(this.ctx)
activeSeries = series.getActiveConfigSeriesIndex('asc', [
'line',
'area',
'scatter',
'bubble',
])
let hoverSize = ttCtx.tooltipUtil.getHoverMarkerSize(activeSeries)
if (pointsArr[activeSeries]) {
cx = pointsArr[activeSeries][j][0]
cy = pointsArr[activeSeries][j][1]
}
if (isNaN(cx)) {
return
}
let points = ttCtx.tooltipUtil.getAllMarkers()
if (points.length) {
for (let p = 0; p < w.globals.series.length; p++) {
let pointArr = pointsArr[p]
if (w.globals.comboCharts) {
// in a combo chart, if column charts are present, markers will not match with the number of series, hence this patch to push a null value in points array
if (typeof pointArr === 'undefined') {
// nodelist to array
points.splice(p, 0, null)
}
}
if (pointArr && pointArr.length) {
let pcy = pointsArr[p][j][1]
let pcy2
points[p].setAttribute('cx', cx)
const shape = points[p].getAttribute('shape')
if (w.config.chart.type === 'rangeArea' && !w.globals.comboCharts) {
const rangeStartIndex = j + w.globals.series[p].length
pcy2 = pointsArr[p][rangeStartIndex][1]
const pcyDiff = Math.abs(pcy - pcy2) / 2
pcy = pcy - pcyDiff
}
if (
pcy !== null &&
!isNaN(pcy) &&
pcy < w.globals.gridHeight + hoverSize &&
pcy + hoverSize > 0
) {
const path = graphics.getMarkerPath(cx, pcy, shape, hoverSize)
points[p].setAttribute('d', path)
} else {
points[p].setAttribute('d', '')
}
}
}
}
this.moveXCrosshairs(cx)
if (!ttCtx.fixedTooltip) {
this.moveTooltip(cx, cy || w.globals.gridHeight, hoverSize)
}
}
moveStickyTooltipOverBars(j, capturedSeries) {
const w = this.w
const ttCtx = this.ttCtx
let barLen = w.globals.columnSeries
? w.globals.columnSeries.length
: w.globals.series.length
if (w.config.chart.stacked) {
barLen = w.globals.barGroups.length
}
let i =
barLen >= 2 && barLen % 2 === 0
? Math.floor(barLen / 2)
: Math.floor(barLen / 2) + 1
if (w.globals.isBarHorizontal) {
let series = new Series(this.ctx)
i = series.getActiveConfigSeriesIndex('desc') + 1
}
let jBar = w.globals.dom.baseEl.querySelector(
`.apexcharts-bar-series .apexcharts-series[rel='${i}'] path[j='${j}'], .apexcharts-candlestick-series .apexcharts-series[rel='${i}'] path[j='${j}'], .apexcharts-boxPlot-series .apexcharts-series[rel='${i}'] path[j='${j}'], .apexcharts-rangebar-series .apexcharts-series[rel='${i}'] path[j='${j}']`
)
if (!jBar && typeof capturedSeries === 'number') {
// Try with captured series index
jBar = w.globals.dom.baseEl.querySelector(
`.apexcharts-bar-series .apexcharts-series[data\\:realIndex='${capturedSeries}'] path[j='${j}'],
.apexcharts-candlestick-series .apexcharts-series[data\\:realIndex='${capturedSeries}'] path[j='${j}'],
.apexcharts-boxPlot-series .apexcharts-series[data\\:realIndex='${capturedSeries}'] path[j='${j}'],
.apexcharts-rangebar-series .apexcharts-series[data\\:realIndex='${capturedSeries}'] path[j='${j}']`
)
}
let bcx = jBar ? parseFloat(jBar.getAttribute('cx')) : 0
let bcy = jBar ? parseFloat(jBar.getAttribute('cy')) : 0
let bw = jBar ? parseFloat(jBar.getAttribute('barWidth')) : 0
const elGrid = ttCtx.getElGrid()
let seriesBound = elGrid.getBoundingClientRect()
const isBoxOrCandle =
jBar &&
(jBar.classList.contains('apexcharts-candlestick-area') ||
jBar.classList.contains('apexcharts-boxPlot-area'))
if (w.globals.isXNumeric) {
if (jBar && !isBoxOrCandle) {
bcx = bcx - (barLen % 2 !== 0 ? bw / 2 : 0)
}
if (
jBar && // fixes apexcharts.js#2354
isBoxOrCandle
) {
bcx = bcx - bw / 2
}
} else {
if (!w.globals.isBarHorizontal) {
bcx =
ttCtx.xAxisTicksPositions[j - 1] + ttCtx.dataPointsDividedWidth / 2
if (isNaN(bcx)) {
bcx = ttCtx.xAxisTicksPositions[j] - ttCtx.dataPointsDividedWidth / 2
}
}
}
if (!w.globals.isBarHorizontal) {
if (w.config.tooltip.followCursor) {
bcy = ttCtx.e.clientY - seriesBound.top - ttCtx.tooltipRect.ttHeight / 2
} else {
if (bcy + ttCtx.tooltipRect.ttHeight + 15 > w.globals.gridHeight) {
bcy = w.globals.gridHeight
}
}
} else {
bcy = bcy - ttCtx.tooltipRect.ttHeight
}
if (!w.globals.isBarHorizontal) {
this.moveXCrosshairs(bcx)
}
if (!ttCtx.fixedTooltip) {
this.moveTooltip(bcx, bcy || w.globals.gridHeight)
}
}
}

View File

@@ -0,0 +1,20 @@
### AxesTooltip.js
This file deals with the x-axis and y-axis tooltips.
### Intersect.js
This file deals with functions related to intersecting tooltips (tooltips that appear when user hovers directly over a data-point whether).
### Labels.js
This file deals with printing actual text on the tooltip.
### Marker.js
This file deals with the markers that appear near tooltip in line/area charts. These markers helps the user to associate the data-points and the values that are shown in the tooltip
### Position.js
This file deals with positioning of the tooltip.
### Tooltip.js
This is the primary file which is an entry point for all tooltip related functionality.
### Utils.js
Helper functions related to tooltips.

View File

@@ -0,0 +1,923 @@
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.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
this.xaxisTooltip = null
this.yaxisTTEls = null
this.isBarShared = !w.globals.isBarHorizontal && this.tConfig.shared
this.lastHoverTime = Date.now()
}
getElTooltip(ctx) {
if (!ctx) ctx = this
if (!ctx.w.globals.dom.baseEl) return null
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.isXAxisTooltipEnabled =
w.config.xaxis.tooltip.enabled && w.globals.axisCharts
this.yaxisTooltips = w.config.yaxis.map((y, i) => {
return y.show && y.tooltip.enabled && w.globals.axisCharts ? true : false
})
this.allTooltipSeriesGroups = []
if (!w.globals.axisCharts) {
this.showTooltipTitle = false
}
const tooltipEl = document.createElement('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}`)
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 ||
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(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 =
this.tConfig.style.fontFamily || w.config.chart.fontFamily
this.tooltipTitle.style.fontSize = this.tConfig.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) && this.tConfig.shared) {
if (!this.showOnIntersect) {
ttItemsCnt = w.globals.series.length
} else {
ttItemsCnt = 1
}
}
this.legendLabels = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-legend-text'
)
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',
`apexcharts-tooltip-series-group-${i}`
)
gTxt.style.order = w.config.tooltip.inverseOrder ? ttItemsCnt - i : i + 1
let point = document.createElement('span')
point.classList.add('apexcharts-tooltip-marker')
point.style.color = w.globals.colors[i]
let mShape = w.config.markers.shape
let shape = mShape
if (Array.isArray(mShape)) {
shape = mShape[i]
}
point.setAttribute('shape', shape)
gTxt.appendChild(point)
const gYZ = document.createElement('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 = document.createElement('div')
gValText.classList.add(`apexcharts-tooltip-${g}-group`)
let txtLabel = document.createElement('span')
txtLabel.classList.add(`apexcharts-tooltip-text-${g}-label`)
gValText.appendChild(txtLabel)
let txtValue = document.createElement('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
let type = w.config.chart.type
const tooltipEl = this.getElTooltip()
const commonBar = !!(
type === 'bar' ||
type === 'candlestick' ||
type === 'boxPlot' ||
type === 'rangeBar'
)
const chartWithmarkers =
type === 'area' ||
type === 'line' ||
type === 'scatter' ||
type === 'bubble' ||
type === 'radar'
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,
tooltipEl,
tooltipY,
tooltipX,
ttItems: this.ttItems,
}
let points
if (w.globals.axisCharts) {
if (chartWithmarkers) {
points = w.globals.dom.baseEl.querySelectorAll(
".apexcharts-series[data\\:longestSeries='true'] .apexcharts-marker"
)
} else if (commonBar) {
points = w.globals.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.globals.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'
) {
let seriesAll =
w.globals.dom.baseEl.querySelectorAll('.apexcharts-series')
this.addPathsEventListeners(seriesAll, seriesHoverParams)
}
if (this.showOnIntersect) {
let lineAreaPoints = w.globals.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() {
let w = this.w
const tooltipEl = this.getElTooltip()
let tooltipRect = tooltipEl.getBoundingClientRect()
let ttWidth = tooltipRect.width + 10
let 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,
}
}
addDatapointEventsListeners(seriesHoverParams) {
let w = this.w
let points = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-series-markers .apexcharts-marker, .apexcharts-bar-area, .apexcharts-candlestick-area, .apexcharts-boxPlot-area, .apexcharts-rangebar-area'
)
this.addPathsEventListeners(points, 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,
}
let 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
*/
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
*/
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) {
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,
})
}
}
seriesHoverByContext({ chartCtx, ttCtx, opt, e }) {
let w = chartCtx.w
const tooltipEl = this.getElTooltip(chartCtx)
if (!tooltipEl) return
// 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.tooltipUtil.hasBars() &&
!w.globals.comboCharts &&
!ttCtx.isBarShared
) {
if (this.tConfig.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 x, y
let 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.globals.capturedSeriesIndex = -1
w.globals.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()
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()
}
let 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.globals.series.length
) {
return
}
if (xcrosshairs !== null) {
xcrosshairs.classList.add('apexcharts-active')
}
const hasYAxisTooltip = this.yaxisTooltips.filter((b) => {
return b === true
})
if (this.ycrosshairs !== null && hasYAxisTooltip.length) {
this.ycrosshairs.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'
) {
let 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()) {
// intersect - line/area/scatter/bubble
this.intersect.handleMarkerTooltip({
e,
opt,
x,
y,
})
}
}
}
if (this.yaxisTooltips.length) {
for (let yt = 0; yt < w.config.yaxis.length; yt++) {
this.axesTooltip.drawYaxisTooltipText(yt, clientY, this.xyRatios)
}
}
w.globals.dom.baseEl.classList.add('apexcharts-tooltip-active')
opt.tooltipEl.classList.add('apexcharts-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 seriesBound = w.globals.dom.elWrap.getBoundingClientRect()
if (e.type === 'mousemove' || e.type === 'touchmove') {
w.globals.dom.baseEl.classList.add('apexcharts-tooltip-active')
tooltipEl.classList.add('apexcharts-active')
this.tooltipLabels.drawSeriesTexts({
ttItems: opt.ttItems,
i: parseInt(rel, 10) - 1,
shared: false,
})
let x = w.globals.clientX - seriesBound.left - tooltipRect.ttWidth / 2
let y = w.globals.clientY - seriesBound.top - tooltipRect.ttHeight - 10
tooltipEl.style.left = x + 'px'
tooltipEl.style.top = y + 'px'
if (w.config.legend.tooltipHoverFormatter) {
let legendFormatter = w.config.legend.tooltipHoverFormatter
const i = rel - 1
const legendName =
this.legendLabels[i].getAttribute('data:default-text')
let text = legendFormatter(legendName, {
seriesIndex: i,
dataPointIndex: i,
w,
})
this.legendLabels[i].innerHTML = text
}
} else if (e.type === 'mouseout' || e.type === 'touchend') {
tooltipEl.classList.remove('apexcharts-active')
w.globals.dom.baseEl.classList.remove('apexcharts-tooltip-active')
if (w.config.legend.tooltipHoverFormatter) {
this.legendLabels.forEach((l) => {
const defaultText = l.getAttribute('data:default-text')
l.innerHTML = decodeURIComponent(defaultText)
})
}
}
}
handleStickyTooltip(e, clientX, clientY, opt) {
const w = this.w
let capj = this.tooltipUtil.getNearestValues({
context: this,
hoverArea: opt.hoverArea,
elGrid: opt.elGrid,
clientX,
clientY,
})
let j = capj.j
let capturedSeries = capj.capturedSeries
if (w.globals.collapsedSeriesIndices.includes(capturedSeries))
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, opt, j)
} else {
// couldn't capture any series. check if shared X is same,
// if yes, draw a grouped tooltip
if (this.tooltipUtil.isXoverlap(j) || w.globals.isBarHorizontal) {
const firstVisibleSeries = w.globals.series.findIndex(
(s, i) => !w.globals.collapsedSeriesIndices.includes(i)
)
this.create(e, this, firstVisibleSeries, j, opt.ttItems)
}
}
}
handleStickyCapturedSeries(e, capturedSeries, opt, j) {
const w = this.w
if (!this.tConfig.shared) {
let ignoreNull = w.globals.series[capturedSeries][j] === null
if (ignoreNull) {
this.handleMouseOut(opt)
return
}
}
if (typeof w.globals.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.globals.series.findIndex(
(s, i) => !w.globals.collapsedSeriesIndices.includes(i)
)
this.create(e, this, firstVisibleSeries, j, opt.ttItems)
}
}
}
deactivateHoverFilter() {
let w = this.w
let graphics = new Graphics(this.ctx)
let allPaths = w.globals.dom.Paper.find(`.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()
w.globals.dom.baseEl.classList.remove('apexcharts-tooltip-active')
opt.tooltipEl.classList.remove('apexcharts-active')
this.deactivateHoverFilter()
if (w.config.chart.type !== 'bubble') {
this.marker.resetPointsSize()
}
if (xcrosshairs !== null) {
xcrosshairs.classList.remove('apexcharts-active')
}
if (this.ycrosshairs !== null) {
this.ycrosshairs.classList.remove('apexcharts-active')
}
if (this.isXAxisTooltipEnabled) {
this.xaxisTooltip.classList.remove('apexcharts-active')
}
if (this.yaxisTooltips.length) {
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('apexcharts-active')
}
}
if (w.config.legend.tooltipHoverFormatter) {
this.legendLabels.forEach((l) => {
const defaultText = l.getAttribute('data:default-text')
l.innerHTML = decodeURIComponent(defaultText)
})
}
}
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 },
])
}
create(e, context, capturedSeries, j, ttItems, shared = null) {
let w = this.w
let 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) {
let legendFormatter = w.config.legend.tooltipHoverFormatter
let els = 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')
)
let 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 commonSeriesTextsParams = {
ttItems,
i: capturedSeries,
j,
...(typeof w.globals.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y1 !==
'undefined' && {
y1: w.globals.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y1,
}),
...(typeof w.globals.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y2 !==
'undefined' && {
y2: w.globals.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(bars)
if (this.barSeriesHeight > 0) {
// hover state, activate snap filter
let graphics = new Graphics(this.ctx)
let paths = w.globals.dom.Paper.find(`.apexcharts-bar-area[j='${j}']`)
// de-activate first
this.deactivateHoverFilter()
ttCtx.tooltipPosition.moveStickyTooltipOverBars(j, capturedSeries)
let points = ttCtx.tooltipUtil.getAllMarkers(true)
if (points.length) {
handlePoints()
}
for (let b = 0; b < paths.length; b++) {
graphics.pathMouseEnter(paths[b])
}
}
}
} else {
ttCtx.tooltipLabels.drawSeriesTexts({
shared: false,
...commonSeriesTextsParams,
})
if (this.tooltipUtil.hasBars()) {
ttCtx.tooltipPosition.moveStickyTooltipOverBars(j, capturedSeries)
}
if (hasMarkers) {
ttCtx.tooltipPosition.moveMarkers(capturedSeries, j)
}
}
}
}

View File

@@ -0,0 +1,375 @@
import Utilities from '../../utils/Utils'
import Graphics from '../Graphics'
/**
* ApexCharts Tooltip.Utils Class to support Tooltip functionality.
*
* @module Tooltip.Utils
**/
export default class Utils {
constructor(tooltipContext) {
this.w = tooltipContext.w
this.ttCtx = tooltipContext
this.ctx = tooltipContext.ctx
}
/**
** 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 {object}
* - hoverArea = the rect on which user hovers
* - elGrid = dimensions of the hover rect (it can be different than hoverarea)
*/
getNearestValues({ hoverArea, elGrid, clientX, clientY }) {
let w = this.w
const seriesBound = elGrid.getBoundingClientRect()
const hoverWidth = seriesBound.width
const hoverHeight = seriesBound.height
let xDivisor = hoverWidth / (w.globals.dataPoints - 1)
let yDivisor = hoverHeight / w.globals.dataPoints
const hasBars = this.hasBars()
if (
(w.globals.comboCharts || hasBars) &&
!w.config.xaxis.convertedCatToNumeric
) {
xDivisor = hoverWidth / w.globals.dataPoints
}
let hoverX = clientX - seriesBound.left - w.globals.barPadForNumericAxis
let 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.globals.zoomEnabled) {
hoverArea.classList.remove('hovering-pan')
hoverArea.classList.add('hovering-zoom')
} else if (w.globals.panEnabled) {
hoverArea.classList.remove('hovering-zoom')
hoverArea.classList.add('hovering-pan')
}
}
let j = Math.round(hoverX / xDivisor)
let 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
let seriesXValArr = w.globals.seriesXvalues.map((seriesXVal) => {
return seriesXVal.filter((s) => Utilities.isNumber(s))
})
let seriesYValArr = w.globals.seriesYvalues.map((seriesYVal) => {
return seriesYVal.filter((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.globals.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()
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
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
}
}
w.globals.capturedSeriesIndex =
capturedSeries === null ? -1 : capturedSeries
if (!j || j < 1) j = 0
if (w.globals.isBarHorizontal) {
w.globals.capturedDataPointIndex = jHorz
} else {
w.globals.capturedDataPointIndex = j
}
return {
capturedSeries,
j: w.globals.isBarHorizontal ? jHorz : j,
hoverX,
hoverY,
}
}
getFirstActiveXArray(Xarrays) {
const w = this.w
let activeIndex = 0
let firstActiveSeriesIndex = Xarrays.map((xarr, 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
}
closestInMultiArray(hoverX, hoverY, Xarrays, Yarrays) {
const w = this.w
// Determine which series are active (not collapsed)
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,
}
}
closestInArray(val, arr) {
let curr = arr[0]
let currIndex = null
let diff = Math.abs(val - curr)
for (let i = 0; i < arr.length; i++) {
let 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 {int}
* - j = is the inner index of series -> (series[i][j])
* @return {bool}
*/
isXoverlap(j) {
let w = this.w
let xSameForAllSeriesJArr = []
const seriesX = w.globals.seriesX.filter((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 = this.w.globals.initialSeries
for (let i = 0; i < initialSeries.length - 1; i++) {
if (initialSeries[i].data.length !== initialSeries[i + 1].data.length) {
sameLen = false
break
}
}
return sameLen
}
getBarsHeight(allbars) {
let bars = [...allbars]
const totalHeight = bars.reduce((acc, bar) => acc + bar.getBBox().height, 0)
return totalHeight
}
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.globals.dom.baseEl.querySelectorAll(
`.apexcharts-series[data\\:realIndex='${capturedSeries}'] .apexcharts-series-markers-wrap > *`
)
}
return this.w.globals.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 = this.w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-series-markers-wrap'
)
markersWraps = [...markersWraps]
if (filterCollapsed) {
markersWraps = markersWraps.filter((m) => {
const realIndex = Number(m.getAttribute('data:realIndex'))
return this.w.globals.collapsedSeriesIndices.indexOf(realIndex) === -1
})
}
markersWraps.sort((a, b) => {
var indexA = Number(a.getAttribute('data:realIndex'))
var indexB = Number(b.getAttribute('data:realIndex'))
return indexB < indexA ? 1 : indexB > indexA ? -1 : 0
})
let markers = []
markersWraps.forEach((m) => {
markers.push(m.querySelector('.apexcharts-marker'))
})
return markers
}
hasMarkers(capturedSeries) {
const markers = this.getElMarkers(capturedSeries)
return markers.length > 0
}
getPathFromPoint(point, size) {
let cx = Number(point.getAttribute('cx'))
let cy = Number(point.getAttribute('cy'))
let shape = point.getAttribute('shape')
return new Graphics(this.ctx).getMarkerPath(cx, cy, shape, size)
}
getElBars() {
return this.w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-bar-series, .apexcharts-candlestick-series, .apexcharts-boxPlot-series, .apexcharts-rangebar-series'
)
}
hasBars() {
const bars = this.getElBars()
return bars.length > 0
}
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
}
toggleAllTooltipSeriesGroups(state) {
let w = this.w
const ttCtx = this.ttCtx
if (ttCtx.allTooltipSeriesGroups.length === 0) {
ttCtx.allTooltipSeriesGroups = w.globals.dom.baseEl.querySelectorAll(
'.apexcharts-tooltip-series-group'
)
}
let 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'
}
}
}
}