UNPKG

apexcharts

Version:

A JavaScript Chart Library

1,515 lines (1,514 loc) 44 kB
/*! * ApexCharts v6.6.1 * (c) 2018-2026 ApexCharts */ import * as _core from "apexcharts/core"; import _core__default from "apexcharts/core"; import { default as default2 } from "apexcharts/core"; const Graphics = _core.__apex_Graphics; const STYLE_KEYS = { fill: "fill", stroke: "stroke", "stroke-width": "strokeWidth", "stroke-dasharray": "strokeDash", "stroke-linecap": "lineCap", "fill-opacity": "fillOpacity", "stroke-opacity": "strokeOpacity", "fill-rule": "fillRule" }; const NEVER = Symbol("never"); const SHAPE_ID = { circle: 0, square: 1, rect: 1, triangle: 2, diamond: 3, star: 4, sparkle: 5, cross: 6, plus: 7, line: 8 }; const SHAPE_NAME = [ "circle", "square", "triangle", "diamond", "star", "sparkle", "cross", "plus", "line" ]; const NOOP_RUNNER = { /** @returns {any} */ attr() { return NOOP_RUNNER; }, plot() { return NOOP_RUNNER; }, during() { return NOOP_RUNNER; }, after(fn) { if (typeof fn === "function") fn(); return NOOP_RUNNER; }, animate() { return NOOP_RUNNER; }, delay() { return NOOP_RUNNER; }, loop() { return NOOP_RUNNER; }, finish() { return NOOP_RUNNER; }, stop() { return NOOP_RUNNER; } }; const SHARED_MARKER_NODE = { nodeName: "path", style: {}, classList: { add() { }, remove() { }, toggle() { }, contains: () => false }, setAttribute() { }, getAttribute: () => null, removeAttribute() { }, hasAttribute: () => false, addEventListener() { }, removeEventListener() { }, appendChild() { }, getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }) }; const SHARED_GROUP = { __isCanvasMark: true, node: { nodeName: "g", instance: null, style: {}, classList: { add() { }, remove() { }, toggle() { }, contains: () => false }, setAttribute() { }, getAttribute: () => null, removeAttribute() { }, addEventListener() { }, removeEventListener() { }, appendChild() { }, getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }) }, /** @returns {any} */ attr() { return SHARED_GROUP; }, add() { return SHARED_GROUP; }, addTo() { return SHARED_GROUP; }, remove() { return SHARED_GROUP; }, clear() { return SHARED_GROUP; }, css() { return SHARED_GROUP; }, hide() { return SHARED_GROUP; }, show() { return SHARED_GROUP; }, removeClass() { return SHARED_GROUP; }, animate() { return NOOP_RUNNER; } }; class CanvasMarkerRef { /** * @param {CanvasGraphics} g * @param {number} i */ constructor(g, i) { this.__isCanvasMark = true; this._g = g; this._i = i; } get node() { return SHARED_MARKER_NODE; } /** * @param {any} a * @param {any} [v] * @returns {any} */ attr(a, v) { if (typeof a === "string") { if (a === "fill" && v !== void 0) this._g._setMarkerFill(this._i, v); return v === void 0 ? null : this; } if (a && a.fill !== void 0) this._g._setMarkerFill(this._i, a.fill); return this; } /** @param {any} _c */ add(_c) { return this; } /** @param {any} _p */ addTo(_p) { return this; } remove() { return this; } /** @param {any} _s */ css(_s) { return this; } /** @param {any} _v */ fill(_v) { if (_v !== void 0) this._g._setMarkerFill(this._i, _v); return this; } /** @param {any} _v */ stroke(_v) { return this; } hide() { return this; } show() { return this; } /** @param {string} _c */ removeClass(_c) { return this; } animate() { return NOOP_RUNNER; } } const SHARED_RECT_REF = { __isCanvasMark: true, node: SHARED_MARKER_NODE, /** @returns {any} */ attr() { return SHARED_RECT_REF; }, add() { return SHARED_RECT_REF; }, addTo() { return SHARED_RECT_REF; }, remove() { return SHARED_RECT_REF; }, /** @returns {any} */ css() { return SHARED_RECT_REF; }, animate() { return NOOP_RUNNER; } }; class CanvasMark { /** @param {any} cmd */ constructor(cmd) { this.__isCanvasMark = true; this._cmd = cmd; const self = this; this.node = { nodeName: cmd ? cmd.tag : "g", instance: this, style: {}, classList: { add() { }, remove() { }, toggle() { }, contains: () => false }, /** @param {string} k @param {any} v */ setAttribute(k, v) { self._applyAttr(k, v); }, getAttribute: () => null, removeAttribute() { }, hasAttribute: () => false, addEventListener() { }, removeEventListener() { }, appendChild() { }, getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }) }; } /** * @param {string} k * @param {any} v */ _applyAttr(k, v) { const cmd = this._cmd; if (!cmd) return; const sk = STYLE_KEYS[k]; if (sk !== void 0) cmd[sk] = v; } /** * @param {any} a * @param {any} [v] * @returns {any} */ attr(a, v) { if (typeof a === "string") { if (v === void 0) return null; this._applyAttr(a, v); return this; } for (const k in a) { if (a[k] !== void 0) this._applyAttr(k, a[k]); } return this; } /** @param {any} _c */ add(_c) { return this; } /** @param {any} _p */ addTo(_p) { return this; } remove() { return this; } clear() { return this; } /** @param {any} _s */ css(_s) { return this; } /** @param {any} v */ fill(v) { if (typeof v === "object") return this.attr(v); return this.attr("fill", v); } /** @param {any} v */ stroke(v) { if (typeof v === "object") { if (v.color !== void 0) this.attr("stroke", v.color); if (v.width !== void 0) this.attr("stroke-width", v.width); return this; } return this.attr("stroke", v); } /** @param {string} d */ plot(d) { if (typeof d === "string" && this._cmd && this._cmd.tag === "path") { this._cmd.d = d; } return this; } hide() { return this; } show() { return this; } /** @param {string} _c */ removeClass(_c) { return this; } bbox() { return { x: 0, y: 0, width: 0, height: 0 }; } animate() { return NOOP_RUNNER; } } class CanvasGraphics { /** @param {any} w */ constructor(w) { this.w = w; this._g = new Graphics(w); this._list = []; this._mx = new Float64Array(16); this._my = new Float64Array(16); this._msize = new Float64Array(16); this._mshape = new Int16Array(16); this._mstyle = new Int32Array(16); this._msi = new Int32Array(16); this._mn = 0; this._mcap = 16; this._crx = new Float64Array(16); this._cry = new Float64Array(16); this._crw = new Float64Array(16); this._crh = new Float64Array(16); this._crstyle = new Int32Array(16); this._crsi = new Int32Array(16); this._crdi = new Int32Array(16); this._crn = 0; this._crcap = 16; this._cellRadius = 0; this._styles = []; this._styleMap = /* @__PURE__ */ new Map(); this._lf = NEVER; this._ls = NEVER; this._lsw = NEVER; this._ld = NEVER; this._lfo = NEVER; this._lso = NEVER; this._lid = -1; this._lofFill = NEVER; this._lofBase = -1; this._lofId = -1; this._rlf = NEVER; this._rls = NEVER; this._rlsw = NEVER; this._rlfo = NEVER; this._rlso = NEVER; this._rlid = -1; } _resetStyleCache() { this._lf = NEVER; this._ls = NEVER; this._lsw = NEVER; this._ld = NEVER; this._lfo = NEVER; this._lso = NEVER; this._lid = -1; this._lofFill = NEVER; this._lofBase = -1; this._lofId = -1; this._rlf = NEVER; this._rls = NEVER; this._rlsw = NEVER; this._rlfo = NEVER; this._rlso = NEVER; this._rlid = -1; } /** Start a fresh scene (columnar marker store + object-command list). */ reset() { this._list = []; this._mn = 0; this._styles = []; this._styleMap = /* @__PURE__ */ new Map(); this._resetStyleCache(); const series = this.w.config.series || []; let cap = 16; for (let i = 0; i < series.length; i++) { const d = series[i] && series[i].data; if (Array.isArray(d)) cap += d.length; } cap = Math.ceil(cap * 1.15) + 16; if (cap > this._mcap) this._allocMarkers(cap); this._crn = 0; this._cellRadius = 0; if (cap > this._crcap) this._allocRects(cap); } /** @param {number} cap */ _allocRects(cap) { this._crcap = cap; this._crx = new Float64Array(cap); this._cry = new Float64Array(cap); this._crw = new Float64Array(cap); this._crh = new Float64Array(cap); this._crstyle = new Int32Array(cap); this._crsi = new Int32Array(cap); this._crdi = new Int32Array(cap); } /** Grow the rect columns (rare: capacity estimate was low). */ _growRects() { const cap = this._crcap * 2; const nx = new Float64Array(cap); nx.set(this._crx); this._crx = nx; const ny = new Float64Array(cap); ny.set(this._cry); this._cry = ny; const nw = new Float64Array(cap); nw.set(this._crw); this._crw = nw; const nh = new Float64Array(cap); nh.set(this._crh); this._crh = nh; const nst = new Int32Array(cap); nst.set(this._crstyle); this._crstyle = nst; const nsi = new Int32Array(cap); nsi.set(this._crsi); this._crsi = nsi; const ndi = new Int32Array(cap); ndi.set(this._crdi); this._crdi = ndi; this._crcap = cap; } /** @param {number} cap */ _allocMarkers(cap) { this._mcap = cap; this._mx = new Float64Array(cap); this._my = new Float64Array(cap); this._msize = new Float64Array(cap); this._mshape = new Int16Array(cap); this._mstyle = new Int32Array(cap); this._msi = new Int32Array(cap); } /** Grow the marker columns (rare: capacity estimate was low). */ _growMarkers() { const cap = this._mcap * 2; const nx = new Float64Array(cap); nx.set(this._mx); this._mx = nx; const ny = new Float64Array(cap); ny.set(this._my); this._my = ny; const ns = new Float64Array(cap); ns.set(this._msize); this._msize = ns; const nsh = new Int16Array(cap); nsh.set(this._mshape); this._mshape = nsh; const nst = new Int32Array(cap); nst.set(this._mstyle); this._mstyle = nst; const nsi = new Int32Array(cap); nsi.set(this._msi); this._msi = nsi; this._mcap = cap; } displayList() { return this._list; } markerCount() { return this._mn; } /** * Intern a marker style; returns its palette id. Keeps the per-point columns * numeric (no retained per-point object). * @param {any} fill @param {any} stroke @param {any} sw @param {any} dash * @param {any} fo @param {any} so * @returns {number} */ _internStyle(fill, stroke, sw, dash, fo, so) { const key = `${fill}|${stroke}|${sw}|${dash}|${fo}|${so}`; const cached = this._styleMap.get(key); if (cached !== void 0) return cached; const id = this._styles.length; this._styles.push({ fill, stroke, strokeWidth: sw, strokeDash: dash, fillOpacity: fo, strokeOpacity: so }); this._styleMap.set(key, id); return id; } /** * Override a recorded marker's fill (scatter sets a per-point fill via `attr` * right after drawMarker). Like the draw path, the string-key intern is cached * on (base style, fill) so the Map/string work stays OFF the per-point path * (it otherwise mixes with the `_mstyle[i]` typed-array write → the ~80× slow * path). For a scatter series the base + fill are uniform, so it interns once. * @param {number} i @param {any} fill */ _setMarkerFill(i, fill) { const base = this._mstyle[i]; if (fill === this._lofFill && base === this._lofBase) { this._mstyle[i] = this._lofId; return; } const s = this._styles[base]; if (!s || s.fill === fill) { this._lofFill = fill; this._lofBase = base; this._lofId = base; return; } const id = this._internStyle( fill, s.stroke, s.strokeWidth, s.strokeDash, s.fillOpacity, s.strokeOpacity ); this._mstyle[i] = id; this._lofFill = fill; this._lofBase = base; this._lofId = id; } /** @param {number} i @returns {any} the style object for a marker index */ markerStyle(i) { return this._styles[this._mstyle[i]]; } /** @param {number} i @returns {number} series (realIndex) of a marker, -1 if none */ markerSeries(i) { return this._msi[i]; } /** @param {number} id @returns {string} */ shapeName(id) { return SHAPE_NAME[id] || "circle"; } // ── columnar rect cell (heatmap): parallel unboxed arrays, no per-cell object ── /** * Record a heatmap-style cell (a filled, optionally stroked rect). Geometry * and style are captured up front into the columns; the returned handle is a * shared no-op (the emit site sets nothing back on it in canvas mode). * @param {number} x @param {number} y @param {number} w @param {number} h * @param {any} opts {fill, fillOpacity, stroke, strokeWidth, radius, seriesIndex, dataPointIndex} * @returns {any} */ drawRectCell(x, y, w, h, opts = {}) { const styleId = this._rectStyleId(opts); if (this._crn >= this._crcap) this._growRects(); const i = this._crn++; this._crx[i] = x || 0; this._cry[i] = y || 0; this._crw[i] = w > 0 ? w : 0; this._crh[i] = h > 0 ? h : 0; this._crstyle[i] = styleId; this._crsi[i] = opts.seriesIndex == null ? -1 : opts.seriesIndex; this._crdi[i] = opts.dataPointIndex == null ? -1 : opts.dataPointIndex; if (opts.radius) this._cellRadius = opts.radius; return SHARED_RECT_REF; } /** * Resolve (and dedupe) a rect-cell style → shared-palette id. A last-style * cache keeps the Map/string work off the path for runs of same-style cells. * @param {any} opts * @returns {number} */ _rectStyleId(opts) { const fill = opts.fill; const stroke = opts.stroke; const sw = opts.strokeWidth; const fo = opts.fillOpacity; const so = opts.strokeOpacity; if (fill === this._rlf && stroke === this._rls && sw === this._rlsw && fo === this._rlfo && so === this._rlso) { return this._rlid; } const id = this._internStyle(fill, stroke, sw, 0, fo, so); this._rlf = fill; this._rls = stroke; this._rlsw = sw; this._rlfo = fo; this._rlso = so; this._rlid = id; return id; } /** @returns {number} number of recorded rect cells */ rectCount() { return this._crn; } /** @param {number} i @returns {any} the style object for a rect cell */ rectStyle(i) { return this._styles[this._crstyle[i]]; } /** @param {number} i @returns {number} series (realIndex) of a cell, -1 if none */ rectSeries(i) { return this._crsi[i]; } /** * @param {string} tag * @param {number} z * @returns {any} */ _cmd(tag, z) { const cmd = { tag, z: z || 0, fill: void 0, stroke: void 0, strokeWidth: void 0, strokeDash: void 0, lineCap: void 0, fillOpacity: void 0, strokeOpacity: void 0, fillRule: void 0 }; this._list.push(cmd); return cmd; } // ── organizational (groups don't paint or record) ── // Series draw() creates a wrap group PER POINT (scatter.draw / plotChartMarkers // run per point), so allocating a handle per group is ~50k heavy allocations // at scale: enough transient churn to tip V8 into a GC blow-up. Groups carry // no paint state in canvas mode (attr/add are no-ops), so every group shares // one singleton: zero per-point allocation. /** @param {any} _attrs */ group(_attrs) { return SHARED_GROUP; } // ── per-point marker (line/area markers, scatter, bubble): COLUMNAR ── /** * @param {number} x * @param {number} y * @param {any} opts */ drawMarker(x, y, opts = {}) { var _a; const styleId = this._markerStyleId(opts); if (this._mn >= this._mcap) this._growMarkers(); const i = this._mn++; this._mx[i] = x || 0; this._my[i] = typeof y === "number" ? y : NaN; this._msize[i] = opts.pSize || 0; this._mshape[i] = (_a = SHAPE_ID[opts.shape || "circle"]) != null ? _a : 0; this._mstyle[i] = styleId; this._msi[i] = opts.seriesIndex == null ? -1 : opts.seriesIndex; return new CanvasMarkerRef(this, i); } /** * Resolve (and dedupe) a marker style → palette id. A last-style cache keeps * the Map/string work off the path when consecutive markers share a style * (the common case), so intern runs once per style run. * @param {any} opts * @returns {number} */ _markerStyleId(opts) { const shape = opts.shape || "circle"; const strokeTinted = shape === "line" || shape === "plus" || shape === "cross"; const fill = strokeTinted ? "none" : opts.pointFillColor; const stroke = strokeTinted ? opts.pointFillColor : opts.pointStrokeColor; const sw = opts.pointStrokeWidth; const dash = opts.pointStrokeDashArray; const fo = opts.pointFillOpacity; const so = strokeTinted ? opts.pointFillOpacity : opts.pointStrokeOpacity; if (fill === this._lf && stroke === this._ls && sw === this._lsw && dash === this._ld && fo === this._lfo && so === this._lso) { return this._lid; } const id = this._internStyle(fill, stroke, sw, dash, fo, so); this._lf = fill; this._ls = stroke; this._lsw = sw; this._ld = dash; this._lfo = fo; this._lso = so; this._lid = id; return id; } // ── series body path (line/area/bar): object command ── /** @param {any} opts */ renderPaths(opts) { const cmd = this._cmd("path", opts.realIndex); cmd.d = opts.pathTo; if (opts.pathToNumeric) { cmd.nxs = opts.pathToNumeric.xs; cmd.nys = opts.pathToNumeric.ys; cmd.ncloseY = opts.pathToNumeric.closeY; } cmd.stroke = opts.stroke; cmd.strokeWidth = opts.strokeWidth; cmd.fill = opts.fill; cmd.lineCap = opts.strokeLinecap; cmd.si = opts.realIndex; this.w.globals.animationEnded = true; return new CanvasMark(cmd); } /** @param {any} opts */ drawPath(opts) { const cmd = this._cmd("path", 0); cmd.d = opts.d; cmd.stroke = opts.stroke; cmd.strokeWidth = opts.strokeWidth; cmd.fill = opts.fill; cmd.fillOpacity = opts.fillOpacity; cmd.strokeOpacity = opts.strokeOpacity; cmd.lineCap = opts.strokeLinecap; cmd.strokeDash = opts.strokeDashArray; return new CanvasMark(cmd); } // ── remaining primitives (contract completeness / Marks #11 forward-compat) ── /** * @param {number} x1 @param {number} y1 @param {number} x2 @param {number} y2 * @param {string} lineColor @param {any} dashArray @param {number} strokeWidth */ drawLine(x1, y1, x2, y2, lineColor = "#a8a8a8", dashArray = 0, strokeWidth = 1) { const cmd = this._cmd("line", 0); cmd.lx1 = x1; cmd.ly1 = y1; cmd.lx2 = x2; cmd.ly2 = y2; cmd.stroke = lineColor; cmd.strokeDash = dashArray; cmd.strokeWidth = strokeWidth; return new CanvasMark(cmd); } /** * Mirrors Graphics.drawRect's full signature (including stroke), so callers * that stroke rects (Marks api.rect, chart code) paint the same on canvas. * @param {number} x1 @param {number} y1 @param {number} x2 @param {number} y2 * @param {number} radius @param {string} color @param {number} opacity * @param {number|null} [strokeWidth] @param {string|null} [strokeColor] * @param {any} [strokeDashArray] */ drawRect(x1 = 0, y1 = 0, x2 = 0, y2 = 0, radius = 0, color = "#fefefe", opacity = 1, strokeWidth = null, strokeColor = null, strokeDashArray = 0) { const cmd = this._cmd("rect", 0); cmd.x1 = x1; cmd.y1 = y1; cmd.rw = x2 > 0 ? x2 : 0; cmd.rh = y2 > 0 ? y2 : 0; cmd.radius = radius; cmd.fill = color; cmd.fillOpacity = opacity; if (strokeColor != null) { cmd.stroke = strokeColor; cmd.strokeWidth = strokeWidth == null ? 1 : strokeWidth; cmd.strokeDash = strokeDashArray; } return new CanvasMark(cmd); } /** * @param {number} radius * @param {any} attrs */ drawCircle(radius, attrs = null) { const cmd = this._cmd("circle", 0); cmd.r = radius < 0 ? 0 : radius; if (attrs) { cmd.cx = attrs.cx; cmd.cy = attrs.cy; if (attrs.fill !== void 0) cmd.fill = attrs.fill; if (attrs.stroke !== void 0) cmd.stroke = attrs.stroke; } return new CanvasMark(cmd); } /** @param {any} opts */ drawText(opts) { const cmd = this._cmd("text", 0); cmd.text = Array.isArray(opts.text) ? opts.text.join(" ") : opts.text; cmd.tx = opts.x; cmd.ty = opts.y; cmd.textAnchor = opts.textAnchor || "start"; cmd.fontSize = opts.fontSize; cmd.fontFamily = opts.fontFamily; cmd.fill = opts.foreColor; return new CanvasMark(cmd); } /** * Resolve a marker's SVG path `d` (non-circle shapes) lazily at paint time. * @param {number} x @param {number} y @param {number} shapeId @param {number} size * @returns {string} */ markerPath(x, y, shapeId, size) { return this._g.getMarkerPath(x, y, SHAPE_NAME[shapeId] || "circle", size); } } const SVGElement = _core.__apex_SVGElement; const SVGNS = _core.__apex_math_SVGNS; const BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs; const TWO_PI = Math.PI * 2; const DPR_CAP = 2; class CanvasCompositor { /** @param {any} w */ constructor(w) { this.w = w; this._host = null; this._canvas = null; this._c2d = null; this._margin = 0; this._dpr = 1; this._dim = null; this._alpha = 1; this._unitPaths = /* @__PURE__ */ new Map(); this._markerBatches = 0; } /** Marker style-batches applied during the last paint() (dev/test hook). */ markerBatchCount() { return this._markerBatches; } /** * Opacity multiplier for a series index under the active dim spec: 1 for the * highlighted series (or when not dimming, or for unidentified marks), else * the inactive opacity. * @param {number} si * @returns {number} */ _seriesAlpha(si) { const d = this._dim; if (!d || d.active == null || d.active < 0 || si == null || si < 0) return 1; return si === d.active ? 1 : d.opacity == null ? 0.2 : d.opacity; } _plotDims() { var _a; const gw = Math.max(0, Math.ceil(this.w.layout.gridWidth || 0)); const gh = Math.max(0, Math.ceil(this.w.layout.gridHeight || 0)); const largest = ((_a = this.w.globals.markers) == null ? void 0 : _a.largestSize) || 0; const margin = Math.ceil(largest + 8); return { gw, gh, margin }; } /** * Create (or recreate) the foreignObject + canvas sized to the plot rect and * return the SVGElement host that `plotChartType` inserts into the tree. * @returns {any} */ createHost() { const win = BrowserAPIs.getWindow(); this._dpr = Math.min(DPR_CAP, win && win.devicePixelRatio || 1); const { gw, gh, margin } = this._plotDims(); this._margin = margin; const w = gw + margin * 2; const h = gh + margin * 2; const fo = BrowserAPIs.createElementNS(SVGNS, "foreignObject"); fo.setAttribute("x", String(-margin)); fo.setAttribute("y", String(-margin)); fo.setAttribute("width", String(w)); fo.setAttribute("height", String(h)); fo.setAttribute("class", "apexcharts-canvas-series"); fo.style.overflow = "visible"; const canvas = ( /** @type {any} */ BrowserAPIs.createElement("canvas") ); canvas.setAttribute("class", "apexcharts-series-canvas"); canvas.width = Math.max(1, Math.round(w * this._dpr)); canvas.height = Math.max(1, Math.round(h * this._dpr)); canvas.style.width = w + "px"; canvas.style.height = h + "px"; canvas.style.pointerEvents = "none"; fo.appendChild(canvas); this._canvas = canvas; this._c2d = canvas.getContext("2d"); this._host = new SVGElement(fo); return this._host; } getHost() { return this._host; } clear() { if (!this._c2d || !this._canvas) return; this._c2d.setTransform(1, 0, 0, 1, 0, 0); this._c2d.clearRect(0, 0, this._canvas.width, this._canvas.height); } /** * Re-check devicePixelRatio before painting: a restyle() repaint after the * window moved between monitors would otherwise keep the stale backing-store * scale (blurry or over-sized) until the next full render rebuilds the host. * The canvas CSS size is unchanged; only the backing store is resized. */ _syncDpr() { if (!this._canvas) return; const win = BrowserAPIs.getWindow(); const dpr = Math.min(DPR_CAP, win && win.devicePixelRatio || 1); if (dpr === this._dpr) return; this._dpr = dpr; const wCss = parseFloat(this._canvas.style.width) || 0; const hCss = parseFloat(this._canvas.style.height) || 0; this._canvas.width = Math.max(1, Math.round(wCss * dpr)); this._canvas.height = Math.max(1, Math.round(hCss * dpr)); } /** * Paint the recorded scene: object commands (series bodies / rects / lines / * text) first, then the columnar markers on top (matching SVG z-order where * markers sit above the series path). `shim` supplies the columnar marker * arrays + lazy non-circle marker geometry. * @param {any[]} list * @param {any} shim * @param {{active:number, opacity:number}|null} [dim] per-series dim spec * (hover / legend restyle); null repaints at full opacity. */ paint(list, shim, dim = null) { const ctx = this._c2d; if (!ctx) return; this._dim = dim || null; this._syncDpr(); this.clear(); const dpr = this._dpr; const m = this._margin; ctx.setTransform(dpr, 0, 0, dpr, m * dpr, m * dpr); if (list.length) { const ordered = list.length > 1 ? list.map((c, i) => [c, i]).sort( (a, b) => a[0].z === b[0].z ? a[1] - b[1] : a[0].z - b[0].z ).map((pair) => pair[0]) : list; for (let i = 0; i < ordered.length; i++) { const c = ordered[i]; this._alpha = this._dim ? this._seriesAlpha(c.si) : 1; this._paintOne(ctx, c); } } this._paintRects(ctx, shim); this._paintMarkers(ctx, shim); this._alpha = 1; } /** * Paint the columnar rect cells (heatmap) as STYLE BATCHES: one fill/stroke * state application per run of consecutive same-style cells, then a fast * fillRect (or a roundRect path when the shared corner radius is non-zero) * per cell. Clipped to the plot rect so cells never bleed into the canvas * margin (mirrors the SVG gridRectMask). Per-cell globalAlpha carries the * hover/legend dim multiplier when a dim spec is active. * @param {any} ctx * @param {any} shim */ _paintRects(ctx, shim) { const n = shim.rectCount ? shim.rectCount() : 0; if (!n) return; const rx = shim._crx; const ry = shim._cry; const rw = shim._crw; const rh = shim._crh; const rstyle = shim._crstyle; const radius = shim._cellRadius || 0; const cx = ( /** @type {any} */ ctx ); const useRound = radius > 0 && typeof cx.roundRect === "function"; const dimming = !!this._dim; const gw = Math.max(0, this.w.layout.gridWidth || 0); const gh = Math.max(0, this.w.layout.gridHeight || 0); ctx.save(); ctx.beginPath(); ctx.rect(0, 0, gw, gh); ctx.clip(); let i = 0; while (i < n) { const styleId = rstyle[i]; const style = shim.rectStyle(i); if (!style) { i++; continue; } const fill = style.fill; const doFill = fill && fill !== "none" && !(typeof fill === "string" && fill.indexOf("url(") === 0); const stroke = style.stroke; const sw = style.strokeWidth == null ? 0 : Number(style.strokeWidth); const doStroke = stroke && stroke !== "none" && sw > 0 && !(typeof stroke === "string" && stroke.indexOf("url(") === 0); if (doFill) ctx.fillStyle = fill; if (doStroke) { ctx.strokeStyle = stroke; ctx.lineWidth = sw; ctx.setLineDash([]); } const baseFillA = style.fillOpacity == null ? 1 : Number(style.fillOpacity); const baseStrokeA = style.strokeOpacity == null ? 1 : Number(style.strokeOpacity); let j = i; while (j < n && rstyle[j] === styleId) { const w = rw[j]; const h = rh[j]; if (w > 0 && h > 0) { const f = dimming ? this._seriesAlpha(shim.rectSeries(j)) : 1; if (useRound) { ctx.beginPath(); cx.roundRect(rx[j], ry[j], w, h, radius); if (doFill) { ctx.globalAlpha = baseFillA * f; ctx.fill(); } if (doStroke) { ctx.globalAlpha = baseStrokeA * f; ctx.stroke(); } } else { if (doFill) { ctx.globalAlpha = baseFillA * f; ctx.fillRect(rx[j], ry[j], w, h); } if (doStroke) { ctx.globalAlpha = baseStrokeA * f; ctx.strokeRect(rx[j], ry[j], w, h); } } } j++; } i = j; } ctx.globalAlpha = 1; ctx.restore(); } /** * Reusable unit Path2D for a (shape, size): the shape's geometry built at the * origin once, then translated per marker via setTransform. Returns null when * the geometry string cannot be parsed. * @param {any} shim * @param {number} shapeId * @param {number} size * @returns {any} */ _unitPath(shim, shapeId, size) { const key = shapeId + "|" + size; let p = this._unitPaths.get(key); if (p === void 0) { try { p = new Path2D(shim.markerPath(0, 0, shapeId, size)); } catch (e) { p = null; } this._unitPaths.set(key, p); } return p; } /** * Markers paint as STYLE BATCHES: one fill/stroke state application per run * of consecutive same-style markers (a uniform single-series scatter is * exactly one batch), then per-marker geometry inside the run. Per-marker * geometry stays painter's-ordered (fill+stroke per marker) so overlapping * semi-transparent markers composite exactly as SVG does. * @param {any} ctx * @param {any} shim */ _paintMarkers(ctx, shim) { this._markerBatches = 0; const n = shim.markerCount(); if (!n) return; const mx = shim._mx; const my = shim._my; const msize = shim._msize; const mshape = shim._mshape; const mstyle = shim._mstyle; const dimming = !!this._dim; if (!dimming) this._alpha = 1; let i = 0; while (i < n) { const styleId = mstyle[i]; const shapeId = mshape[i]; const style = shim.markerStyle(i); if (!style) { i++; continue; } const doFill = this._applyFill(ctx, style); const doStroke = this._applyStroke(ctx, style); this._markerBatches++; const baseFillA = style.fillOpacity == null ? 1 : Number(style.fillOpacity); const baseStrokeA = style.strokeOpacity == null ? 1 : Number(style.strokeOpacity); if (shapeId === 0) { let j = i; while (j < n && mshape[j] === 0 && mstyle[j] === styleId) { const r = msize[j] || 0; const y = my[j]; if (r > 0 && y === y) { ctx.beginPath(); ctx.arc(mx[j], y, r, 0, TWO_PI); if (dimming) { const f = this._seriesAlpha(shim.markerSeries(j)); if (doFill) { ctx.globalAlpha = baseFillA * f; ctx.fill(); } if (doStroke) { ctx.globalAlpha = baseStrokeA * f; ctx.stroke(); } } else { if (doFill) ctx.fill(); if (doStroke) ctx.stroke(); } } j++; } ctx.globalAlpha = 1; i = j; } else { const dpr = this._dpr; const m = this._margin; let j = i; while (j < n && mshape[j] === shapeId && mstyle[j] === styleId) { const y = my[j]; const size = msize[j]; if (y === y && size > 0) { const p = this._unitPath(shim, shapeId, size); if (p) { ctx.setTransform(dpr, 0, 0, dpr, (m + mx[j]) * dpr, (m + y) * dpr); const f = dimming ? this._seriesAlpha(shim.markerSeries(j)) : 1; if (doFill) { ctx.globalAlpha = baseFillA * f; ctx.fill(p); } if (doStroke) { ctx.globalAlpha = baseStrokeA * f; ctx.stroke(p); } } } j++; } ctx.setTransform(dpr, 0, 0, dpr, m * dpr, m * dpr); ctx.globalAlpha = 1; i = j; } } } /** * Paint a series path from its numeric fast-path coords: a direct * moveTo/lineTo loop over the typed arrays, no Path2D and no d-string * parse. `ncloseY` (areas) closes the polygon down to the baseline exactly * like the string form's `L xLast bottom L x0 bottom z` tail. * @param {any} ctx * @param {any} cmd */ _paintNumericPath(ctx, cmd) { const xs = cmd.nxs; const ys = cmd.nys; const n = xs.length; if (!n) return; ctx.beginPath(); ctx.moveTo(xs[0], ys[0]); for (let k = 1; k < n; k++) { ctx.lineTo(xs[k], ys[k]); } if (cmd.ncloseY != null) { ctx.lineTo(xs[n - 1], cmd.ncloseY); ctx.lineTo(xs[0], cmd.ncloseY); ctx.closePath(); } if (this._applyFill(ctx, cmd)) { ctx.fill(cmd.fillRule === "evenodd" ? "evenodd" : "nonzero"); } if (this._applyStroke(ctx, cmd)) { ctx.stroke(); } ctx.globalAlpha = 1; } /** * @param {any} ctx * @param {any} cmd style-bearing flat command */ _paintOne(ctx, cmd) { switch (cmd.tag) { case "path": { if (cmd.nxs) { this._paintNumericPath(ctx, cmd); break; } if (!cmd.d) return; if (!cmd.path2d) { try { cmd.path2d = new Path2D(cmd.d); } catch (e) { return; } } this._fillStrokePath(ctx, cmd, cmd.path2d); break; } case "rect": { const p = new Path2D(); if (cmd.radius && typeof /** @type {any} */ p.roundRect === "function") { p.roundRect(cmd.x1, cmd.y1, cmd.rw, cmd.rh, cmd.radius); } else { p.rect(cmd.x1, cmd.y1, cmd.rw, cmd.rh); } this._fillStrokePath(ctx, cmd, p); break; } case "circle": { if (!(cmd.r > 0)) return; ctx.beginPath(); ctx.arc(cmd.cx, cmd.cy, cmd.r, 0, TWO_PI); this._fillStroke(ctx, cmd); break; } case "line": { ctx.beginPath(); ctx.moveTo(cmd.lx1, cmd.ly1); ctx.lineTo(cmd.lx2, cmd.ly2); this._strokeOnly(ctx, cmd); break; } case "text": { if (cmd.text == null) return; ctx.save(); ctx.globalAlpha = this._alpha; ctx.fillStyle = cmd.fill || "#000"; const size = cmd.fontSize || "11px"; ctx.font = `${typeof size === "number" ? size + "px" : size} ${cmd.fontFamily || "Helvetica, Arial, sans-serif"}`; ctx.textAlign = cmd.textAnchor === "middle" ? "center" : cmd.textAnchor === "end" ? "right" : "left"; ctx.fillText(String(cmd.text), cmd.tx, cmd.ty); ctx.restore(); break; } } } /** * @param {any} ctx * @param {any} style * @param {any} path2d */ _fillStrokePath(ctx, style, path2d) { if (this._applyFill(ctx, style)) { ctx.fill(path2d, style.fillRule === "evenodd" ? "evenodd" : "nonzero"); } if (this._applyStroke(ctx, style)) { ctx.stroke(path2d); } ctx.globalAlpha = 1; } /** * @param {any} ctx * @param {any} style */ _fillStroke(ctx, style) { if (this._applyFill(ctx, style)) ctx.fill(); if (this._applyStroke(ctx, style)) ctx.stroke(); ctx.globalAlpha = 1; } /** * @param {any} ctx * @param {any} style */ _strokeOnly(ctx, style) { if (this._applyStroke(ctx, style)) ctx.stroke(); ctx.globalAlpha = 1; } /** * Set fill state. Returns false when there's nothing to fill. * @param {any} ctx * @param {any} style */ _applyFill(ctx, style) { const fill = style.fill; if (!fill || fill === "none") return false; if (typeof fill === "string" && fill.indexOf("url(") === 0) return false; ctx.globalAlpha = (style.fillOpacity == null ? 1 : Number(style.fillOpacity)) * this._alpha; ctx.fillStyle = fill; return true; } /** * Set stroke state. Returns false when there's nothing to stroke. * @param {any} ctx * @param {any} style */ _applyStroke(ctx, style) { const stroke = style.stroke; const sw = style.strokeWidth == null ? 1 : Number(style.strokeWidth); if (!stroke || stroke === "none" || !(sw > 0)) return false; if (typeof stroke === "string" && stroke.indexOf("url(") === 0) return false; ctx.globalAlpha = (style.strokeOpacity == null ? 1 : Number(style.strokeOpacity)) * this._alpha; ctx.strokeStyle = stroke; ctx.lineWidth = sw; ctx.lineCap = style.lineCap || "butt"; const dash = style.strokeDash; if (dash && dash !== 0) { ctx.setLineDash(Array.isArray(dash) ? dash : [Number(dash)]); } else { ctx.setLineDash([]); } return true; } /** Series bitmap for the export composite bridge (P4). @returns {string|null} */ toDataURL() { return this._canvas ? this._canvas.toDataURL() : null; } destroy() { this._host = null; this._canvas = null; this._c2d = null; this._unitPaths.clear(); } } class CanvasRenderer { /** * @param {any} w * @param {any} ctx */ constructor(w, ctx) { this.w = w; this.ctx = ctx; this.kind = "canvas"; this._g = new CanvasGraphics(w); this._compositor = new CanvasCompositor(w); } // ── lifecycle ── /** Start a fresh series display list for this render pass. */ beginSeries() { this._g.reset(); } /** * Finalize the pass: paint the recorded display list and return the SVG host * (a `<foreignObject><canvas>`) for `plotChartType` to composite into the tree. * @returns {any} */ present() { const host = this._compositor.createHost(); this._compositor.paint(this._g.displayList(), this._g); return host; } /** Fast-path wipe of the series layer. */ clear() { this._compositor.clear(); } /** * Whether the existing <canvas> host can be repainted in place (it exists * and is still mounted). Used by the data-only fast update path to skip * recreating the foreignObject + backing store on every tick. * @returns {boolean} */ canRepaintInPlace() { const host = this._compositor.getHost(); return !!(host && host.node && host.node.isConnected); } /** Repaint the freshly recorded display list into the EXISTING canvas. */ repaintInPlace() { this._compositor.paint(this._g.displayList(), this._g); } // ── emit primitives (delegate to the display-list shim) ── /** @param {any} attrs */ group(attrs) { return this._g.group(attrs); } /** @param {any} opts */ drawPath(opts) { return this._g.drawPath(opts); } /** @param {any[]} args */ drawLine(...args) { return ( /** @type {any} */ this._g.drawLine(...args) ); } /** @param {any[]} args */ drawRect(...args) { return ( /** @type {any} */ this._g.drawRect(...args) ); } /** * Columnar heatmap-cell rect (dense same-shape rects): recorded into typed * arrays, painted as style batches. Distinct from drawRect (object command) * so 100k cells don't allocate 100k retained commands. * @param {number} x @param {number} y @param {number} w @param {number} h * @param {any} opts */ drawRectCell(x, y, w, h, opts) { return this._g.drawRectCell(x, y, w, h, opts); } /** * @param {number} r * @param {any} attrs */ drawCircle(r, attrs) { return this._g.drawCircle(r, attrs); } /** * @param {number} x * @param {number} y * @param {any} opts */ drawMarker(x, y, opts) { return this._g.drawMarker(x, y, opts); } /** @param {any} opts */ renderPaths(opts) { return this._g.renderPaths(opts); } /** @param {any} opts */ drawText(opts) { return this._g.drawText(opts); } // ── capabilities ── /** @param {string} feature */ supports(feature) { return feature === "solidFill" || feature === "dashArray"; } // ── interaction ── // Line/area/bar/scatter tooltips resolve via coordinate lookup (pointsArray), // so those need no per-mark query. Heatmap cells, however, are hovered by // point (the SVG path hit-tests the <rect> under the cursor); with cells on // canvas there is no node, so hitTest resolves the columnar rect store. /** * Find the cell under a plot-local point (0,0 = plot origin, the same space * as the recorded cell geometry). Reverse scan so a later-painted cell wins * when cells overlap (continuous-x edges). A linear scan stays well under a * frame even at 100k cells (~100k integer compares). Returns the cell's * series/dataPoint index plus its geometry for tooltip positioning, or null * when the point is off every cell. * @param {number} px * @param {number} py * @returns {({seriesIndex:number,dataPointIndex:number,x:number,y:number,width:number,height:number})|null} */ hitTest(px, py) { const g = this._g; const n = g.rectCount ? g.rectCount() : 0; if (!n) return null; const rx = g._crx; const ry = g._cry; const rw = g._crw; const rh = g._crh; for (let k = n - 1; k >= 0; k--) { const w = rw[k]; const h = rh[k]; if (w <= 0 || h <= 0) continue; if (px >= rx[k] && px < rx[k] + w && py >= ry[k] && py < ry[k] + h) { return { seriesIndex: g._crsi[k], dataPointIndex: g._crdi[k], x: rx[k], y: ry[k], width: w, height: h }; } } return null; } /** * Repaint the retained series scene with a per-series dim spec (hover / * legend restyle). No geometry recompute: reuses the display list + marker * columns recorded at render time. Pass null to repaint at full opacity. * @param {{active:number, opacity:number}|null} [dim] */ restyle(dim) { this._compositor.paint(this._g.displayList(), this._g, dim || null); } // ── export ── toBitmap() and the compositor's toDataURL() back // Exports.inlineCanvasLayers, which inlines the series bitmap as an SVG // <image> so PNG/SVG export includes the canvas layer in correct z-order. /** @returns {{dataURL:string,x:number,y:number,w:number,h:number}|null} */ toBitmap() { const url = this._compositor.toDataURL(); if (!url) return null; const gl = this.w.globals; const cfg = this.w.config.chart; const margin = this._compositor._margin; return { dataURL: url, x: (gl.translateX || 0) + (cfg.offsetX || 0) - margin, y: (gl.translateY || 0) + (cfg.offsetY || 0) - margin, w: (this.w.layout.gridWidth || 0) + margin * 2, h: (this.w.layout.gridHeight || 0) + margin * 2 }; } destroy() { this._compositor.destroy(); } } _core__default.registerRenderer( "canvas", /** * @param {any} w * @param {any} ctx */ (w, ctx) => new CanvasRenderer(w, ctx) ); export { default2 as default };