apexcharts
Version:
A JavaScript Chart Library
500 lines (463 loc) • 18 kB
JavaScript
// @ts-check
import Utils from './../../utils/Utils'
import {
LINE_HEIGHT_RATIO,
NICE_SCALE_ALLOWED_MAG_MSD,
NICE_SCALE_DEFAULT_TICKS,
} from '../../utils/Constants'
export default class Globals {
/**
* @param {any} gl
*/
initGlobalVars(gl) {
// ── Series data (ephemeral — fully recomputed from config.series each render) ──
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 = [] // x positions used by tooltip when x values are unequal
gl.seriesYvalues = [] // y positions used to identify which series the user hovered
gl.dataWasParsed = false
gl.originalSeries = null
gl.maxValsInArrayIndex = 0 // index of the series with the most data points
gl.yValueDecimal = 0 // decimal precision of y values (0 = integers only)
gl.allSeriesHasEqualX = true // false when series have differing x value sets
// ── Labels & categories (ephemeral — derived from parsed series/config) ──
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
// ── Axis bounds & scales (ephemeral — recomputed by Range/Scales each render) ──
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.xRange = 0
gl.yRange = []
gl.zRange = 0
gl.dataPoints = 0
gl.xTickAmount = 0
gl.multiAxisTickAmount = 0
// ── Zoom / timeline range state (ephemeral — recomputed by TimeScale) ──
gl.disableZoomIn = false
gl.disableZoomOut = false
// ── Layout & dimensions (ephemeral — recomputed by Dimensions each render) ──
gl.yLabelsCoords = []
gl.yTitleCoords = []
gl.barPadForNumericAxis = 0
gl.padHorizontal = 0
gl.rotateXLabels = false // true when x-axis labels are angled
gl.overlappingXLabels = false // true when x labels overlap and must be hidden
gl.radialSize = 0 // computed radius for radial/polar charts
// Note: formatter properties (xLabelFormatter, yLabelFormatters, etc.) live on
// w.formatters — not on gl. See Base.js for backward-compat shims.
// ── Bar chart sizing (ephemeral — set by bar/Helpers.initialPositions) ──
gl.barHeight = 0
gl.barWidth = 0
// ── Animation (ephemeral — reset at the start of each render pass) ──
gl.animationEnded = false
// ── Caches (ephemeral — cleared so stale DOM refs/measurements don't persist) ──
gl.resizeTimer = null
gl.selectionResizeTimer = null
gl.lastWheelExecution = 0
gl.delayedElements = []
gl.pointsArray = []
gl.dataLabelsRects = []
gl.lastDrawnDataLabelsIndexes = [] // tracks which data labels were drawn per series to prevent collisions
gl.textRectsCache = new Map()
gl.domCache = new Map()
gl.dimensionCache = {} // cache for getBoundingClientRect results
gl.cachedSelectors = {} // cache for querySelectorAll results
// Attach namespace views the first time only — live proxies into the flat
// globals above. initGlobalVars is called again on resetGlobals (each
// updateSeries), so we guard against re-attaching.
if (!gl.seriesNS) {
this._attachNamespaces(gl)
}
}
/**
* Attach domain-grouped namespace sub-objects onto gl.
* Each sub-object is a plain object whose properties are defined as
* getters/setters that read/write the canonical flat properties on gl.
* This means there is exactly ONE storage location per value — no copies,
* no sync needed.
*
* Namespaces:
* gl.series — parsed series data and chart-type-specific arrays
* gl.axes — axis bounds, scales, ranges, tick state
* gl.layout — SVG/grid dimensions, translations, label sizes
* gl.cache — DOM caches, timers, observers, drawing scratch space
*
* Note: interact state lives on w.interact (not gl) — see Base.js.
* @param {any} gl
*/
_attachNamespaces(gl) {
// Helper: define a property on `ns` that proxies to `gl[key]`
/**
* @param {any} ns
* @param {string} key
*/
const proxy = (ns, key, nsKey = key) => {
Object.defineProperty(ns, nsKey, {
get() {
return gl[key]
},
set(v) {
gl[key] = v
},
enumerable: true,
configurable: true,
})
}
// ── series namespace ─────────────────────────────────────────────────────
// All parsed/computed series data. Chart-type-specific arrays included so
// one namespace covers all chart types.
// Note: `gl.series` (the main y-values array) is exposed here as `seriesNS.data`
// to avoid a circular proxy. All other series-prefixed fields are proxied by name.
const seriesNS = {}
// Alias gl.series → seriesNS.data
proxy(seriesNS, 'series', 'data')
for (const key of [
'seriesNames',
'seriesX',
'seriesZ',
'seriesXvalues',
'seriesYvalues',
'seriesGoals',
'seriesLog',
'seriesColors',
'seriesPercent',
'seriesTotals',
'stackedSeriesTotals',
'seriesCandleO',
'seriesCandleH',
'seriesCandleM',
'seriesCandleL',
'seriesCandleC',
'seriesRangeStart',
'seriesRangeEnd',
'seriesRange',
'seriesYAxisMap',
'seriesYAxisReverseMap',
'seriesGroups',
'barGroups',
'lineGroups',
'areaGroups',
'originalSeries',
'collapsedSeries',
'collapsedSeriesIndices',
'ancillaryCollapsedSeries',
'ancillaryCollapsedSeriesIndices',
'allSeriesCollapsed',
'risingSeries',
'previousPaths',
'ignoreYAxisIndexes',
'labels',
'categoryLabels',
'timescaleLabels',
'groups',
]) {
proxy(seriesNS, key)
}
// Expose the namespace under a non-colliding key.
// gl.series still refers to the series data array (unchanged).
// gl.seriesNS groups all series-related fields.
Object.defineProperty(gl, 'seriesNS', {
value: seriesNS,
writable: false,
enumerable: false,
configurable: true,
})
// ── axes namespace ───────────────────────────────────────────────────────
// Axis bounds, scale objects, range/tick state updated each render.
const axesNS = {}
for (const key of [
'minX',
'maxX',
'initialMinX',
'initialMaxX',
'minY',
'maxY',
'minYArr',
'maxYArr',
'minZ',
'maxZ',
'minDate',
'maxDate',
'minXDiff',
'xRange',
'yRange',
'zRange',
'xAxisScale',
'yAxisScale',
'xAxisTicksPositions',
'xTickAmount',
'multiAxisTickAmount',
'dataPoints',
'maxValsInArrayIndex',
'isXNumeric',
'isMultipleYAxis',
'isMultiLineX',
'isDataXYZ',
'dataFormatXNumeric',
'allSeriesHasEqualX',
'hasNullValues',
'dataWasParsed',
'hasXaxisGroups',
'hasSeriesGroups',
'skipFirstTimelinelabel',
'skipLastTimelinelabel',
'yValueDecimal',
'invalidLogScale',
'noLabelsProvided',
]) {
proxy(axesNS, key)
}
Object.defineProperty(gl, 'axes', {
value: axesNS,
writable: false,
enumerable: false,
configurable: true,
})
// ── layout namespace ─────────────────────────────────────────────────────
// SVG and grid dimensions, translations, label/axis sizing.
const layoutNS = {}
for (const key of [
'svgWidth',
'svgHeight',
'gridWidth',
'gridHeight',
'translateX',
'translateY',
'translateXAxisX',
'translateXAxisY',
'translateYAxisX',
'xAxisLabelsHeight',
'xAxisGroupLabelsHeight',
'xAxisLabelsWidth',
'yAxisLabelsWidth',
'yAxisWidths',
'yLabelsCoords',
'yTitleCoords',
'padHorizontal',
'barPadForNumericAxis',
'rotateXLabels',
'scaleX',
'scaleY',
'radialSize',
'defaultLabels',
'overlappingXLabels',
]) {
proxy(layoutNS, key)
}
Object.defineProperty(gl, 'layout', {
value: layoutNS,
writable: false,
enumerable: false,
configurable: true,
})
// Note: interact namespace (zoom/pan/selection/mouse state) lives on w.interact
// — see Base.js. Backward-compat shims proxy gl.* → w.interact.*.
// ── cache namespace ──────────────────────────────────────────────────────
// Internal caches, timers, observers, drawing scratch arrays.
// These are never part of the public API surface.
const cacheNS = {}
for (const key of [
'domCache',
'dimensionCache',
'cachedSelectors',
'textRectsCache',
'pointsArray',
'dataLabelsRects',
'lastDrawnDataLabelsIndexes',
'delayedElements',
'resizeTimer',
'selectionResizeTimer',
'resizeObserver',
]) {
proxy(cacheNS, key)
}
Object.defineProperty(gl, 'cache', {
value: cacheNS,
writable: false,
enumerable: false,
configurable: true,
})
}
/**
* Persistent chart state — set ONCE at chart construction and intentionally NOT
* reset by initGlobalVars. These values must survive updateSeries / re-render.
*
* Rule: if a value is recalculated fresh on every render it belongs in
* initGlobalVars instead, not here.
* @returns {import('../../types/internal').ChartGlobals}
* @param {Record<string, any>} config
*/
globalVars(config) {
const globals = {
// ── Identity (set once, never changes) ───────────────────────────────────
chartID: null, // full chart ID: "apexcharts-<cuid>"
cuid: null, // random suffix only
// ── Event registry (accumulates listeners, never reset) ───────────────────
events: {
beforeMount: [],
mounted: [],
updated: [],
clicked: [],
selection: [],
dataPointSelection: [],
zoomed: [],
scrolled: [],
},
// ── Theme colors (set by Theme module after config merge) ─────────────────
colors: [],
fill: { colors: [] },
stroke: { colors: [] },
dataLabels: { style: { colors: [] } },
radarPolygons: { fill: { colors: [] } },
markers: {
colors: [],
size: config.markers.size,
largestSize: 0,
},
// ── Device / environment detected once at startup ─────────────────────────
// Note: isTouchDevice lives on w.interact — see Base.js. Shim installed there.
LINE_HEIGHT_RATIO,
// ── Chart-type flags (derived from config, set during Core.mount) ─────────
axisCharts: true, // false for pie/radial/treemap etc.
isSlopeChart: config.plotOptions.line.isSlopeChart,
comboCharts: false, // true when mixing line + column series
// ── Config snapshots (backups for zoom-reset / updateOptions) ────────────
initialConfig: null, // deep clone of the original user config
initialSeries: [],
lastXAxis: [],
lastYAxis: [],
// ── User interaction state (must survive re-renders) ──────────────────────
// Note: zoomEnabled, panEnabled, selectionEnabled, zoomed, selection,
// visibleXRange, selectedDataPoints, mousedown, clientX, clientY,
// lastClientPosition, lastWheelExecution, capturedSeriesIndex,
// capturedDataPointIndex, disableZoomIn, disableZoomOut, isTouchDevice
// live on w.interact — see Base.js. Backward-compat shims installed there.
// Series collapse state (user-driven, must persist across re-renders)
allSeriesCollapsed: false,
collapsedSeries: [],
collapsedSeriesIndices: [],
ancillaryCollapsedSeries: [],
ancillaryCollapsedSeriesIndices: [],
risingSeries: [], // series being re-shown after collapse
ignoreYAxisIndexes: [], // y-axis indices excluded during series collapse
// ── Lifecycle / update flags ──────────────────────────────────────────────
isDirty: false, // true when user called an update method manually
isExecCalled: false, // true when update came via exec()
dataChanged: false, // true when series data was changed dynamically
resized: false, // true after a container resize
// ── Data format flags (derived from config/series, stable between renders) ─
// Note: dataFormatXNumeric lives on w.axisFlags — see Base.js. Shim installed there.
invalidLogScale: false, // true when log scale requested but data is invalid
hasNullValues: false, // true when any series contains null values
// Persistent data tracking
columnSeries: null, // tracks which series are rendered as bars/columns
yaxis: null, // resolved yaxis config array
total: 0, // running total (used by pie/radial)
// ── Animation control ─────────────────────────────────────────────────────
shouldAnimate: true,
previousPaths: [], // paths from previous render — source for enter animation
// ── SVG viewport (set by Dimensions, but persistent as layout anchor) ─────
svgWidth: 0,
svgHeight: 0,
// Note: gridWidth, gridHeight, translateX, translateY, translateXAxisX,
// translateXAxisY, xAxisLabelsHeight, xAxisGroupLabelsHeight, xAxisLabelsWidth,
// rotateXLabels, xAxisHeight, yLabelsCoords, yTitleCoords live on w.layout —
// see Base.js. Backward-compat shims installed there.
defaultLabels: false,
// Note: formatter properties (xLabelFormatter, yLabelFormatters, etc.) live on
// w.formatters — see Base.js. Backward-compat shims installed there.
yAxisLabelsWidth: 0,
scaleX: 1,
scaleY: 1,
translateYAxisX: [],
yAxisWidths: [],
// ── Instances (created once, replaced only on full re-init) ──────────────
tooltip: null,
resizeObserver: null,
// ── Locale (loaded once; changes only via setLocale()) ───────────────────
locale: {},
// ── Method queue (deferred calls during async operations) ────────────────
memory: {
methodsToExec: [],
},
// ── Scale configuration constants — imported from utils/Constants.js ──────
niceScaleAllowedMagMsd: NICE_SCALE_ALLOWED_MAG_MSD,
niceScaleDefaultTicks: NICE_SCALE_DEFAULT_TICKS,
// ── Multi-axis series mapping ─────────────────────────────────────────────
seriesYAxisMap: [], // yAxis index → series indices[]
seriesYAxisReverseMap: [], // series index → yAxis index
noData: false, // true when there is nothing to render
}
// globalVars() returns the persistent subset; initGlobalVars() fills the
// ephemeral properties before first render. Cast to ChartGlobals since the
// combined object is structurally complete by the time it is first read.
return /** @type {import('../../types/internal').ChartGlobals} */ (
/** @type {unknown} */ (globals)
)
}
/**
* @param {Record<string, any>} config
*/
init(config) {
const globals = this.globalVars(config)
this.initGlobalVars(globals)
globals.initialConfig = Utils.extend({}, config)
globals.initialSeries = Utils.clone(config.series)
globals.lastXAxis = Utils.clone(
/** @type {NonNullable<typeof globals.initialConfig>} */ (
globals.initialConfig
).xaxis,
)
globals.lastYAxis = Utils.clone(
/** @type {NonNullable<typeof globals.initialConfig>} */ (
globals.initialConfig
).yaxis,
)
return globals
}
}