apexcharts
Version:
A JavaScript Chart Library
448 lines (447 loc) • 16.2 kB
JavaScript
/*!
* ApexCharts v5.15.0
* (c) 2018-2026 ApexCharts
*/
import * as _core from "apexcharts/core";
import _core__default from "apexcharts/core";
import { default as default2 } from "apexcharts/core";
const Environment = _core.__apex_Environment_Environment;
const BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs;
const prefersReducedMotion = _core.__apex_Animations_prefersReducedMotion;
const parsePath = _core.__apex_PathMorphing_parsePath;
const BAR_FAMILY = /* @__PURE__ */ new Set(["bar", "funnel", "pyramid"]);
const RADIAL_FAMILY = /* @__PURE__ */ new Set(["pie", "donut", "polarArea", "radialBar", "gauge"]);
function familyOf(type) {
if (BAR_FAMILY.has(type)) return "bar";
if (RADIAL_FAMILY.has(type)) return "radial";
return null;
}
class MorphTypeChange {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w, ctx) {
this.w = w;
this.ctx = ctx;
this._snapshot = null;
}
/**
* @param {string} fromType
* @param {string} toType
* @returns {boolean}
*/
canMorphTypes(fromType, toType) {
if (fromType === toType) return false;
const ff = familyOf(fromType);
const tf = familyOf(toType);
if (!ff || !tf) return false;
return true;
}
/**
* @param {string} fromType
* @param {string} toType
* @param {any} newSeries
* @returns {boolean}
*/
isCompatibleSeriesShape(fromType, toType, newSeries) {
if (!Array.isArray(newSeries) || newSeries.length === 0) return false;
const ff = familyOf(fromType);
const tf = familyOf(toType);
if (tf === "radial") {
return newSeries.every((v) => typeof v === "number");
}
if (tf === "bar") {
return newSeries.every(
(s) => s && typeof s === "object" && Array.isArray(s.data)
);
}
return ff !== null && tf !== null;
}
/**
* Capture the live DOM of the *current* (outgoing) chart and stash it on
* this module. Called from `apexcharts._updateOptions` before the config
* merge that flips `chart.type`.
*
* Returns true if a morph is queued — caller doesn't need the value, but
* tests use it.
*
* @param {{ fromType: string, toType: string, newSeries: any }} args
* @returns {boolean}
*/
captureBeforeDestroy({ fromType, toType, newSeries }) {
this._snapshot = null;
if (!Environment.isBrowser()) return false;
const animCfg = this.w.config.chart.animations;
if (!animCfg || animCfg.enabled === false) return false;
if (animCfg.chartTypeMorph && animCfg.chartTypeMorph.enabled === false)
return false;
if (animCfg.respectReducedMotion && prefersReducedMotion()) return false;
if (!this.canMorphTypes(fromType, toType)) return false;
if (!this.isCompatibleSeriesShape(fromType, toType, newSeries)) return false;
const captured = this._captureFromDOM(fromType);
if (!captured.length) return false;
const mapping = this._buildMapping(captured, fromType, toType, newSeries);
if (mapping.size === 0) return false;
this._snapshot = {
fromType,
toType,
mapping,
oldLayout: {
translateX: this.w.layout.translateX || 0,
translateY: this.w.layout.translateY || 0
}
};
this.w.globals.previousPaths = [];
return true;
}
/**
* Walk the outgoing chart's DOM and collect path `d` strings keyed by
* (realIndex, j). The selectors are scoped to the chart family — bar
* elements have `pathTo` set; pie/radial elements use their final `d`.
*
* @param {string} fromType
* @returns {Array<{ realIndex: number, j: number, d: string, fill: string|null }>}
*/
_captureFromDOM(fromType) {
var _a;
const baseEl = (_a = this.w.globals.dom) == null ? void 0 : _a.baseEl;
if (!baseEl) return [];
const captured = [];
const fam = familyOf(fromType);
if (fam === "bar") {
const seriesNodes = baseEl.querySelectorAll(
".apexcharts-bar-series .apexcharts-series"
);
seriesNodes.forEach((seriesNode) => {
var _a2;
const realIndex = parseInt(
(_a2 = seriesNode.getAttribute("data:realIndex")) != null ? _a2 : "0",
10
);
const paths = seriesNode.querySelectorAll("path[pathTo]");
paths.forEach((p, j) => {
const d = p.getAttribute("pathTo") || p.getAttribute("d");
if (!d) return;
captured.push({
realIndex,
j,
d,
fill: p.getAttribute("fill")
});
});
});
} else if (fam === "radial") {
if (fromType === "radialBar" || fromType === "gauge") {
const centerX = this.w.layout.gridWidth / 2;
const centerY = Math.min(this.w.layout.gridWidth, this.w.layout.gridHeight) / 2;
const rings = baseEl.querySelectorAll(
".apexcharts-radial-series .apexcharts-radialbar-area"
);
rings.forEach((p) => {
var _a2;
const parent = (
/** @type {Element|null} */
p.parentElement
);
const realIndex = parseInt(
(_a2 = parent == null ? void 0 : parent.getAttribute("data:realIndex")) != null ? _a2 : "0",
10
);
const rawD = p.getAttribute("d");
if (!rawD) return;
const strokeWidth = parseFloat(p.getAttribute("stroke-width") || "0");
const d = strokeWidth > 1 ? this._radialArcToFilledSegment(
rawD,
strokeWidth,
centerX,
centerY
) || rawD : rawD;
captured.push({
realIndex,
j: 0,
d,
fill: p.getAttribute("stroke")
});
});
} else {
const slices = baseEl.querySelectorAll(
".apexcharts-pie-series .apexcharts-pie-area"
);
slices.forEach(
(p, i) => {
const d = p.getAttribute("d");
if (!d) return;
captured.push({
realIndex: i,
j: 0,
d,
fill: p.getAttribute("fill")
});
}
);
}
}
return captured;
}
/**
* Convert a radialBar's stroked open-arc `d` ("M x1 y1 A r r 0 large sweep
* x2 y2") into a closed donut-segment polygon whose FILLED rendering
* visually matches the original stroked arc — needed because the morph
* target (pie/donut/polarArea) renders by fill, not stroke. Returns null
* if the input doesn't match the expected M-then-A shape.
*
* @param {string} rawD
* @param {number} strokeWidth
* @param {number} centerX
* @param {number} centerY
* @returns {string | null}
*/
_radialArcToFilledSegment(rawD, strokeWidth, centerX, centerY) {
const m = rawD.match(
/M\s*(-?[\d.]+)\s+(-?[\d.]+)\s+A\s*(-?[\d.]+)\s+(?:-?[\d.]+)\s+(?:-?[\d.]+)\s+(\d)\s+(\d)\s+(-?[\d.]+)\s+(-?[\d.]+)/
);
if (!m) return null;
const x1 = parseFloat(m[1]);
const y1 = parseFloat(m[2]);
const r = parseFloat(m[3]);
const large = parseInt(m[4], 10);
const sweep = parseInt(m[5], 10);
const x2 = parseFloat(m[6]);
const y2 = parseFloat(m[7]);
if (!isFinite(r) || r <= 0) return null;
const half = strokeWidth / 2;
const rOuter = r + half;
const rInner = Math.max(0, r - half);
const proj = (px, py, newR) => {
const dx = px - centerX;
const dy = py - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return { x: centerX, y: centerY };
const k = newR / dist;
return { x: centerX + dx * k, y: centerY + dy * k };
};
const o1 = proj(x1, y1, rOuter);
const o2 = proj(x2, y2, rOuter);
const i1 = proj(x1, y1, rInner);
const i2 = proj(x2, y2, rInner);
const sweepBack = sweep ? 0 : 1;
return `M ${o1.x} ${o1.y} A ${rOuter} ${rOuter} 0 ${large} ${sweep} ${o2.x} ${o2.y} L ${i2.x} ${i2.y} A ${rInner} ${rInner} 0 ${large} ${sweepBack} ${i1.x} ${i1.y} Z`;
}
/**
* Build a closed donut-segment path for the given polar arc geometry. Used
* by Radial.drawArcs when morphing FROM a filled wedge (pie/donut/polarArea)
* TO a radialBar arc: the final radialBar is rendered as a stroked open arc,
* but during the morph we tween d toward this closed-segment form (which
* looks identical to the stroked arc when filled with the same color) so
* the in-between frames remain visually consistent filled shapes rather
* than a thick-outlined wedge.
*
* @param {number} centerX
* @param {number} centerY
* @param {number} ringRadius - centerline radius of the radialBar ring
* @param {number} strokeWidth - the ring's stroke thickness
* @param {number} startAngleDeg - in degrees, 0° = top (12 o'clock)
* @param {number} endAngleDeg
* @returns {string}
*/
buildRingSegmentPath(centerX, centerY, ringRadius, strokeWidth, startAngleDeg, endAngleDeg) {
const halfStroke = strokeWidth / 2;
const rOuter = ringRadius + halfStroke;
const rInner = Math.max(0, ringRadius - halfStroke);
const sRad = (startAngleDeg - 90) * Math.PI / 180;
const eRad = (endAngleDeg - 90) * Math.PI / 180;
const oStart = {
x: centerX + rOuter * Math.cos(sRad),
y: centerY + rOuter * Math.sin(sRad)
};
const oEnd = {
x: centerX + rOuter * Math.cos(eRad),
y: centerY + rOuter * Math.sin(eRad)
};
const iStart = {
x: centerX + rInner * Math.cos(sRad),
y: centerY + rInner * Math.sin(sRad)
};
const iEnd = {
x: centerX + rInner * Math.cos(eRad),
y: centerY + rInner * Math.sin(eRad)
};
const sweep = endAngleDeg > startAngleDeg ? 1 : 0;
const large = Math.abs(endAngleDeg - startAngleDeg) > 180 ? 1 : 0;
return `M ${oStart.x} ${oStart.y} A ${rOuter} ${rOuter} 0 ${large} ${sweep} ${oEnd.x} ${oEnd.y} L ${iEnd.x} ${iEnd.y} A ${rInner} ${rInner} 0 ${large} ${1 - sweep} ${iStart.x} ${iStart.y} Z`;
}
/**
* @returns {string | null} the chart-type the active snapshot was captured
* from, or null when no morph is in flight.
*/
getFromType() {
return this._snapshot ? this._snapshot.fromType : null;
}
/**
* Build a (targetKey → captured) map. The targetKey matches the lookup
* pattern each chart-type renderer uses when it asks
* `getInitialPathFor(realIndex, j)`.
*
* Strategy: flatten the captured items into a linear sequence (matching the
* source chart's natural DOM iteration order: series-then-point for bar,
* ring-by-ring for radial), then walk the target's iteration positions in
* the same order and pair them up 1:1. This handles every supported shape
* without per-pair branching:
*
* - bar (1 series, N pts) ↔ radial-family (N items) → linear[k] ↔ k
* - bar (M series, 1 pt) ↔ radial-family (M items) → linear[k] ↔ k
* - radial-family (N items) ↔ bar (any matching shape) → linear[k] ↔ flat target
* - radial-family ↔ radial-family → linear[k] ↔ k
*
* @param {Array<{ realIndex: number, j: number, d: string, fill: string|null }>} captured
* @param {string} _fromType
* @param {string} toType
* @param {any} newSeries - the series array being passed to the new chart;
* used only to derive the bar target's (realIndex, j) iteration positions.
*/
_buildMapping(captured, _fromType, toType, newSeries) {
const map = /* @__PURE__ */ new Map();
const tf = familyOf(toType);
const flat = captured.slice().sort((a, b) => a.realIndex - b.realIndex || a.j - b.j);
if (tf === "radial") {
flat.forEach((c, i) => {
map.set(`${i}:0`, { d: c.d, fill: c.fill });
});
return map;
}
if (tf === "bar") {
const positions = [];
const series = Array.isArray(newSeries) ? newSeries : [];
series.forEach((s, seriesIdx) => {
const data = s && Array.isArray(s.data) ? s.data : [];
for (let j = 0; j < data.length; j++) {
positions.push({ realIndex: seriesIdx, j });
}
});
flat.forEach((c, i) => {
const pos = positions[i];
if (pos) {
map.set(`${pos.realIndex}:${pos.j}`, { d: c.d, fill: c.fill });
}
});
return map;
}
return map;
}
isActive() {
return this._snapshot !== null;
}
/**
* @param {number} realIndex
* @param {number} j
* @returns {string | null}
*/
getInitialPathFor(realIndex, j) {
if (!this._snapshot) return null;
const entry = this._snapshot.mapping.get(`${realIndex}:${j}`);
if (!entry) return null;
const dx = this._snapshot.oldLayout.translateX - (this.w.layout.translateX || 0);
const dy = this._snapshot.oldLayout.translateY - (this.w.layout.translateY || 0);
return dx === 0 && dy === 0 ? entry.d : this._translatePathD(entry.d, dx, dy);
}
/**
* Offset every absolute coordinate in an SVG path `d` by (dx, dy).
*
* Assumes the path uses only uppercase (absolute) commands — every path
* ApexCharts generates does. Relative-command paths would pass through
* unchanged at the lowercase, which is also semantically correct (deltas
* don't shift under a parent translate).
*
* @param {string} d
* @param {number} dx
* @param {number} dy
* @returns {string}
*/
_translatePathD(d, dx, dy) {
if (dx === 0 && dy === 0) return d;
const commands = parsePath(d);
return commands.map(
/** @param {any[]} c */
(c) => {
const cmd = c[0];
if (cmd === "Z") return "Z";
if (cmd === "M" || cmd === "L" || cmd === "T") {
return `${cmd} ${c[1] + dx} ${c[2] + dy}`;
}
if (cmd === "H") return `${cmd} ${c[1] + dx}`;
if (cmd === "V") return `${cmd} ${c[1] + dy}`;
if (cmd === "C") {
return `${cmd} ${c[1] + dx} ${c[2] + dy} ${c[3] + dx} ${c[4] + dy} ${c[5] + dx} ${c[6] + dy}`;
}
if (cmd === "S" || cmd === "Q") {
return `${cmd} ${c[1] + dx} ${c[2] + dy} ${c[3] + dx} ${c[4] + dy}`;
}
if (cmd === "A") {
return `${cmd} ${c[1]} ${c[2]} ${c[3]} ${c[4]} ${c[5]} ${c[6] + dx} ${c[7] + dy}`;
}
return c.join(" ");
}
).join(" ");
}
/**
* @param {number} realIndex
* @param {number} j
* @returns {string | null}
*/
getInitialFillFor(realIndex, j) {
if (!this._snapshot) return null;
const entry = this._snapshot.mapping.get(`${realIndex}:${j}`);
return entry ? entry.fill : null;
}
/** @returns {number} */
getSpeed() {
const animCfg = this.w.config.chart.animations;
return animCfg.chartTypeMorph && animCfg.chartTypeMorph.speed || animCfg.speed || 600;
}
/**
* Fade newly-mounted axes / grid / legend / titles from opacity 0 → 1 in
* parallel with the morph. Without this the chart's chrome would pop in
* abruptly while the series elements are still mid-tween, which reads as a
* jarring layout shift.
*/
applyChromeFade() {
var _a;
if (!this._snapshot || !Environment.isBrowser()) return;
const baseEl = (_a = this.w.globals.dom) == null ? void 0 : _a.baseEl;
if (!baseEl) return;
const speed = this.getSpeed();
const chromeSelectors = [
".apexcharts-xaxis",
".apexcharts-yaxis",
".apexcharts-grid",
".apexcharts-gridlines-horizontal",
".apexcharts-gridlines-vertical",
".apexcharts-legend",
".apexcharts-title-text",
".apexcharts-subtitle-text"
];
chromeSelectors.forEach((sel) => {
baseEl.querySelectorAll(sel).forEach((el) => {
if (!el.style) return;
el.style.opacity = "0";
el.style.transition = `opacity ${speed}ms ease-out`;
BrowserAPIs.requestAnimationFrame(() => {
el.style.opacity = "1";
});
setTimeout(() => {
el.style.transition = "";
el.style.opacity = "";
}, speed + 80);
});
});
setTimeout(() => this.cleanup(), speed + 100);
}
cleanup() {
this._snapshot = null;
}
}
_core__default.registerFeatures({ morphTypeChange: MorphTypeChange });
export {
default2 as default
};