apexcharts
Version:
A JavaScript Chart Library
616 lines (615 loc) • 22.8 kB
JavaScript
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
/*!
* 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 Utils = _core.__apex_Utils;
const Environment = _core.__apex_Environment_Environment;
const BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs;
const XHTML = "http://www.w3.org/1999/xhtml";
class Breadcrumb {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
* @param {import('./Drilldown').default} drilldown
*/
constructor(w, ctx, drilldown) {
this.w = w;
this.ctx = ctx;
this.drilldown = drilldown;
}
/**
* @param {Array<string|number>} path - ['root', id, id, ...]
*/
render(path) {
if (!Environment.isBrowser()) return;
const w = this.w;
const elWrap = w.dom.elWrap;
if (!elWrap) return;
const existing = elWrap.querySelector(".apexcharts-breadcrumb");
if (existing && existing.parentNode) {
existing.parentNode.removeChild(existing);
}
const cfg = w.config.drilldown && w.config.drilldown.breadcrumb;
if (!cfg || cfg.show === false) return;
if (this.drilldown.depth === 0) return;
const nav = BrowserAPIs.createElementNS(XHTML, "nav");
nav.setAttribute("class", "apexcharts-breadcrumb");
nav.setAttribute("aria-label", "Drilldown breadcrumb");
this._position(nav, cfg);
const separator = cfg.separator != null ? cfg.separator : " / ";
path.forEach((id, i) => {
if (i > 0) {
const sep = BrowserAPIs.createElementNS(XHTML, "span");
sep.setAttribute("class", "apexcharts-breadcrumb-separator");
sep.setAttribute("aria-hidden", "true");
sep.textContent = separator;
nav.appendChild(sep);
}
const label = this._label(id, i);
const isCurrent = i === path.length - 1;
if (isCurrent) {
const cur = BrowserAPIs.createElementNS(XHTML, "span");
cur.setAttribute(
"class",
"apexcharts-breadcrumb-item apexcharts-breadcrumb-current"
);
cur.setAttribute("aria-current", "page");
cur.textContent = label;
nav.appendChild(cur);
} else {
const btn = (
/** @type {HTMLButtonElement} */
BrowserAPIs.createElementNS(XHTML, "button")
);
btn.setAttribute("type", "button");
btn.setAttribute("class", "apexcharts-breadcrumb-item");
if (i === 0) {
const arrow = BrowserAPIs.createElementNS(XHTML, "span");
arrow.setAttribute("class", "apexcharts-breadcrumb-arrow");
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "←";
btn.appendChild(arrow);
}
const text = BrowserAPIs.createElementNS(XHTML, "span");
text.setAttribute("class", "apexcharts-breadcrumb-label");
text.textContent = label;
btn.appendChild(text);
btn.addEventListener("click", () => this.drilldown.drillToLevel(i));
nav.appendChild(btn);
}
});
elWrap.appendChild(nav);
}
/**
* @param {string|number} id
* @param {number} index
* @returns {string}
*/
_label(id, index) {
const cfg = this.w.config.drilldown.breadcrumb;
let label;
if (index === 0) {
label = cfg.rootLabel != null ? cfg.rootLabel : "All";
} else {
const list = (this.w.config.drilldown.series || []).find(
(s) => s && s.id === id
);
label = list && list.name || String(id);
}
if (typeof cfg.formatter === "function") {
return cfg.formatter(label, { index, depth: this.drilldown.depth });
}
return label;
}
/**
* @param {HTMLElement} nav
* @param {Record<string, any>} cfg
*/
_position(nav, cfg) {
const ox = cfg.offsetX || 0;
const oy = cfg.offsetY || 0;
nav.style.position = "absolute";
nav.style.top = oy + "px";
if (cfg.position === "top-right") {
nav.style.right = -ox + 3 + "px";
} else {
nav.style.left = ox + "px";
}
}
}
const MAX_DEPTH = 32;
class Drilldown {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w, ctx) {
this.w = w;
this.ctx = ctx;
this.stack = [];
this.rootSnapshot = null;
this._wired = false;
this.breadcrumb = new Breadcrumb(w, ctx, this);
this._onPointSelect = this._onPointSelect.bind(this);
this._afterRender = this._afterRender.bind(this);
this.init();
}
init() {
const w = this.w;
if (!w.config.drilldown || !w.config.drilldown.enabled) return;
if (this._wired) return;
this._wired = true;
this.ctx.addEventListener("dataPointSelection", this._onPointSelect);
this.ctx.addEventListener("mounted", this._afterRender);
this.ctx.addEventListener("updated", this._afterRender);
}
// ─── Observable state ──────────────────────────────────────────────────────
/** @returns {Array<string|number>} e.g. ['root', '2024-quarters'] */
get path() {
return ["root", ...this.stack.map((f) => f.id)];
}
/** @returns {number} 0 at root */
get depth() {
return this.stack.length;
}
// ─── Navigation API ────────────────────────────────────────────────────────
/**
* Drill into the child level with the given id.
* @param {string|number} id
* @param {any} [triggerPoint] - the clicked data point (for events / async ctx)
* @param {{ seriesIndex?: number, dataPointIndex?: number }} [meta]
* @returns {Promise<any>}
*/
drillDown(id, triggerPoint, meta) {
const child = this._resolveChild(id);
if (child) return this._drillInto(child, triggerPoint, meta);
if (typeof this.w.config.drilldown.onDrillDown === "function") {
return this._drillDownAsync(id, triggerPoint, meta);
}
console.warn(
`ApexCharts: drilldown id "${id}" not found in chart.drilldown.series, and no onDrillDown resolver is set.`
);
return Promise.resolve(this.ctx);
}
/**
* Navigate back one level.
* @returns {Promise<any>}
*/
drillUp() {
return this.drillToLevel(this.stack.length - 1);
}
/**
* Navigate back to the root view.
* @returns {Promise<any>}
*/
drillToRoot() {
return this.drillToLevel(0);
}
/**
* Navigate to an arbitrary depth (0 = root). Used by breadcrumb clicks.
* @param {number} targetDepth
* @returns {Promise<any>}
*/
drillToLevel(targetDepth) {
const cur = this.stack.length;
if (targetDepth < 0 || targetDepth >= cur) return Promise.resolve(this.ctx);
const from = this.path[this.path.length - 1];
const restore = targetDepth === 0 ? this.rootSnapshot : this.stack[targetDepth].restore;
this.stack = this.stack.slice(0, targetDepth);
const to = this.path[this.path.length - 1];
return this._apply(this._viewFromSnapshot(restore), "up", { from, to });
}
// ─── Internals ─────────────────────────────────────────────────────────────
/**
* @param {string|number} id
* @returns {any|null}
*/
_resolveChild(id) {
const list = this.w.config.drilldown && this.w.config.drilldown.series;
if (!Array.isArray(list)) return null;
return list.find((s) => s && s.id === id) || null;
}
/**
* @param {any} child
* @param {any} [triggerPoint]
* @param {object} [meta]
* @returns {Promise<any>}
*/
_drillInto(child, triggerPoint, meta) {
if (this.stack.length >= MAX_DEPTH) {
console.warn(`ApexCharts: drilldown max depth (${MAX_DEPTH}) reached.`);
return Promise.resolve(this.ctx);
}
if (!this.rootSnapshot) this.rootSnapshot = this._snapshot();
const from = this.path[this.path.length - 1];
this.stack.push({ id: child.id, name: child.name, restore: this._snapshot() });
return this._apply(this._viewFromChild(child), "down", {
from,
to: child.id,
point: triggerPoint,
seriesIndex: meta && meta.seriesIndex,
dataPointIndex: meta && meta.dataPointIndex
});
}
/**
* Minimal async resolver (loading overlay lands in Phase 3).
* @param {string|number|null} id
* @param {any} point
* @param {object} [meta]
* @returns {Promise<any>}
*/
_drillDownAsync(id, point, meta) {
const fn = this.w.config.drilldown.onDrillDown;
let result;
try {
result = fn({
point,
seriesIndex: meta && meta.seriesIndex,
dataPointIndex: meta && meta.dataPointIndex
});
} catch (error) {
this._fire("drillDownError", { id, error });
return Promise.resolve(this.ctx);
}
return Promise.resolve(result).then(
(child) => {
if (!child || !child.data) return this.ctx;
return this._drillInto(child, point, meta);
},
(error) => {
this._fire("drillDownError", { id, error });
return this.ctx;
}
);
}
/**
* Capture the overridable surface of the current view so it can be restored.
* Only fields that some drilldown.series entry can change are cloned; series
* and chart.type/stacked are always captured.
* @returns {object}
*/
_snapshot() {
const c = this.w.config;
const fields = this._overrideFields();
const snap = { series: Utils.clone(c.series) };
if (Array.isArray(c.labels) && c.labels.length) {
snap.labels = Utils.clone(c.labels);
}
snap.chart = { type: c.chart.type, stacked: c.chart.stacked };
if (fields.has("xaxis")) snap.xaxis = Utils.clone(c.xaxis);
if (fields.has("yaxis")) snap.yaxis = Utils.clone(c.yaxis);
if (fields.has("colors")) snap.colors = c.colors ? Utils.clone(c.colors) : void 0;
if (fields.has("plotOptions")) snap.plotOptions = Utils.clone(c.plotOptions);
if (fields.has("fill")) snap.fill = Utils.clone(c.fill);
if (fields.has("legend")) snap.legend = Utils.clone(c.legend);
return snap;
}
/**
* Union of overridable fields across all declared drilldown levels. Ensures a
* deep drillToRoot restores everything any intermediate level may have changed.
* @returns {Set<string>}
*/
_overrideFields() {
const fields = /* @__PURE__ */ new Set();
const list = this.w.config.drilldown && this.w.config.drilldown.series || [];
for (const s of list) {
if (!s) continue;
if (s.xaxis) fields.add("xaxis");
if (s.yaxis) fields.add("yaxis");
if (s.colors) fields.add("colors");
if (s.plotOptions) fields.add("plotOptions");
if (s.fill) fields.add("fill");
if (s.legend) fields.add("legend");
}
return fields;
}
/**
* Build an updateOptions/updateSeries payload for drilling INTO a child level.
* Works for axis charts and pie/donut alike: both accept series objects with a
* `data` array of `{ x, y }` points (pie derives slice labels from `x`).
* @param {any} child
* @returns {Record<string, any>}
*/
_viewFromChild(child) {
const view = {};
if (Array.isArray(child.series)) {
view.series = child.series;
} else {
view.series = [{ name: child.name || "", data: child.data }];
}
const chart = {};
if (child.chart && child.chart.type) chart.type = child.chart.type;
if (child.chart && child.chart.stacked != null) chart.stacked = child.chart.stacked;
if (Object.keys(chart).length) view.chart = chart;
if (child.xaxis) view.xaxis = child.xaxis;
if (child.yaxis) view.yaxis = child.yaxis;
if (child.colors) view.colors = child.colors;
if (child.plotOptions) view.plotOptions = child.plotOptions;
if (child.fill) view.fill = child.fill;
if (child.legend) view.legend = child.legend;
return view;
}
/**
* Build an updateOptions payload from a restore-snapshot.
* @param {Record<string, any>} snap
* @returns {Record<string, any>}
*/
_viewFromSnapshot(snap) {
const view = { series: snap.series, chart: snap.chart };
if (snap.labels && snap.labels.length) view.labels = snap.labels;
if (snap.xaxis) view.xaxis = snap.xaxis;
if (snap.yaxis) view.yaxis = snap.yaxis;
if (snap.colors) view.colors = snap.colors;
if (snap.plotOptions) view.plotOptions = snap.plotOptions;
if (snap.fill) view.fill = snap.fill;
if (snap.legend) view.legend = snap.legend;
return view;
}
/**
* Apply a view by delegating to the right update path, firing drill events
* around it.
* @param {Record<string, any>} view
* @param {'down'|'up'} direction
* @param {object} meta
* @returns {Promise<any>}
*/
_apply(view, direction, meta) {
const w = this.w;
w.interact.selectedDataPoints = [];
w.globals.collapsedSeries = [];
w.globals.collapsedSeriesIndices = [];
w.globals.ancillaryCollapsedSeries = [];
w.globals.ancillaryCollapsedSeriesIndices = [];
w.globals.allSeriesCollapsed = false;
w.globals.risingSeries = [];
const animate = (!w.config.drilldown.animation || w.config.drilldown.animation.enabled !== false) && w.config.chart.animations.enabled !== false;
if (direction === "down") this._fire("drillDownStart", meta);
const runUpdate = (anim) => this.ctx.updateOptions(view, false, anim, false, false);
const done = () => {
this._fire(direction === "down" ? "drillDownEnd" : "drillUp", meta);
return this.ctx;
};
if (animate && this._zoomEnabled()) {
const origin = this._triggerOrigin(meta);
if (origin) {
return this._zoomDrill(origin, direction, () => runUpdate(false)).then(done);
}
}
return runUpdate(animate).then(done);
}
/** @returns {boolean} whether trigger-point zoom is configured on. */
_zoomEnabled() {
const a = this.w.config.drilldown && this.w.config.drilldown.animation;
return !!(a && a.zoomFromPoint);
}
/** @returns {SVGSVGElement|null} the chart's root <svg> node, if present. */
_svgNode() {
const paper = this.w.dom && this.w.dom.Paper;
return paper && paper.node ? paper.node : null;
}
/**
* The group wrapping ONLY the data marks (bars/cells/tiles) — not the axes,
* grid, or titles. Animating this keeps the chart frame still while the marks
* move. Covers bar/line/area (`.apexcharts-plot-series`), heatmap, and treemap.
* @returns {SVGElement|null}
*/
_markGroup() {
const svg = this._svgNode();
if (!svg || typeof svg.querySelector !== "function") return null;
return svg.querySelector(
".apexcharts-plot-series, .apexcharts-heatmap, .apexcharts-treemap"
);
}
/**
* Centre of the clicked point in the SVG's view-box pixel space, used as the
* transform-origin for the mark-group scale (which uses `transform-box:
* view-box`, so the origin is resolved in SVG coordinates and stays stable
* across the parent and child renders). Falls back to the mark group's centre
* when there is no trigger point (e.g. drillUp / imperative drill). Returns
* null when the marks / SVG / WAAPI are unavailable (SSR / old browsers).
* @param {object} meta
* @returns {{ x: number, y: number }|null}
*/
_triggerOrigin(meta) {
if (!Environment.isBrowser()) return null;
const svg = this._svgNode();
const group = this._markGroup();
if (!svg || !group || typeof group.animate !== "function" || typeof svg.getBoundingClientRect !== "function") {
return null;
}
const svgRect = svg.getBoundingClientRect();
let el = null;
if (meta && meta.seriesIndex != null && meta.dataPointIndex != null && this.w.dom.baseEl) {
el = this.w.dom.baseEl.querySelector(
`[index="${meta.seriesIndex}"][j="${meta.dataPointIndex}"]`
);
}
if (el && typeof el.getBoundingClientRect === "function") {
const r = el.getBoundingClientRect();
return {
x: r.left + r.width / 2 - svgRect.left,
y: r.top + r.height / 2 - svgRect.top
};
}
const gRect = group.getBoundingClientRect();
return {
x: gRect.left + gRect.width / 2 - svgRect.left,
y: gRect.top + gRect.height / 2 - svgRect.top
};
}
/**
* Run the "expand from the clicked point" choreography around an instant
* (un-animated) update. Only the data-mark group is animated — the axes, grid,
* and titles stay fixed, so the effect doesn't drag the whole chart frame. The
* current marks fade out near-in-place (a quick fade, not a balloon), the child
* renders invisibly underneath, then the child marks unfold outward from the
* clicked point: a horizontal-biased scale anchored there, so the bars read as
* emerging from the column you clicked. Drilling up has no trigger column, so
* it settles gently from the marks' centre.
*
* `transform-box: view-box` resolves the origin in SVG coordinates, so the same
* origin applies cleanly to the parent and the freshly-rendered child group.
* @param {{ x: number, y: number }} origin
* @param {'down'|'up'} direction
* @param {() => Promise<any>} runUpdate
* @returns {Promise<void>}
*/
_zoomDrill(origin, direction, runUpdate) {
return __async(this, null, function* () {
const dur = this._zoomDuration();
const down = direction === "down";
const outDur = Math.round(dur * 0.55);
const outTo = down ? "scale(1.03)" : "scale(0.97)";
const inFrom = down ? "scaleX(0.55) scaleY(0.85)" : "scale(1.04)";
const anchor = (el) => {
el.style.transformBox = "view-box";
el.style.transformOrigin = `${origin.x}px ${origin.y}px`;
};
const clear = (el) => {
el.style.transform = "";
el.style.opacity = "";
el.style.transformOrigin = "";
el.style.transformBox = "";
};
const outGroup = this._markGroup();
let outAnim = null;
if (outGroup) {
anchor(outGroup);
outAnim = outGroup.animate(
[
{ transform: "scale(1)", opacity: 1 },
{ transform: outTo, opacity: 0 }
],
{ duration: outDur, easing: "ease-in", fill: "forwards" }
);
try {
yield outAnim.finished;
} catch (e) {
}
}
yield runUpdate();
const inGroup = this._markGroup();
if (inGroup) {
anchor(inGroup);
inGroup.style.opacity = "0";
inGroup.style.transform = inFrom;
if (outAnim && outGroup === inGroup) outAnim.cancel();
const inAnim = inGroup.animate(
[
{ transform: inFrom, opacity: 0 },
{ transform: "scale(1)", opacity: 1 }
],
// Decelerating ease so the unfold settles softly into place.
{ duration: dur, easing: "cubic-bezier(0.16, 1, 0.3, 1)", fill: "forwards" }
);
try {
yield inAnim.finished;
} catch (e) {
}
clear(inGroup);
inAnim.cancel();
}
});
}
/** @returns {number} per-phase zoom duration in ms. */
_zoomDuration() {
const a = this.w.config.drilldown && this.w.config.drilldown.animation;
const speed = a && typeof a.speed === "number" ? a.speed : 260;
return Math.max(80, speed);
}
/**
* Fire a drill event through both the config callback and the listener registry.
* @param {string} name
* @param {object} payload
*/
_fire(name, payload) {
const cb = this.w.config.chart.events && this.w.config.chart.events[name];
if (typeof cb === "function") cb(payload, this.ctx, this.w);
this.ctx.events.fireEvent(name, [payload, this.ctx, this.w]);
}
// ─── Click + post-render hooks ───────────────────────────────────────────────
/**
* @param {Event} _event
* @param {any} _ctx
* @param {{ seriesIndex?: number, dataPointIndex?: number }} opts
*/
_onPointSelect(_event, _ctx, opts) {
if (!opts) return void 0;
const point = this._pointAt(opts.seriesIndex, opts.dataPointIndex);
if (point && typeof point === "object" && point.drilldown != null) {
return this.drillDown(point.drilldown, point, opts);
}
if (typeof this.w.config.drilldown.onDrillDown === "function") {
return this._drillDownAsync(null, point, opts);
}
return void 0;
}
/**
* @param {number|undefined} seriesIndex
* @param {number|undefined} dataPointIndex
* @returns {any|null}
*/
_pointAt(seriesIndex, dataPointIndex) {
const series = this.w.config.series;
if (!Array.isArray(series) || seriesIndex == null || dataPointIndex == null) {
return null;
}
const s = series[seriesIndex];
if (!s || !Array.isArray(s.data)) return null;
return s.data[dataPointIndex] != null ? s.data[dataPointIndex] : null;
}
_afterRender() {
if (!this.w.config.drilldown || !this.w.config.drilldown.enabled) return;
this._markDrillableTargets();
this.breadcrumb.render(this.path);
}
/**
* Add the drilldown-target cursor class to every point that carries a
* `drilldown` field. Best-effort and cosmetic — a missed selector is harmless.
*/
_markDrillableTargets() {
if (!Environment.isBrowser()) return;
const w = this.w;
const baseEl = w.dom.baseEl;
const series = w.config.series;
if (!baseEl || !Array.isArray(series)) return;
series.forEach((s, i) => {
const data = s && Array.isArray(s.data) ? s.data : null;
if (!data) return;
data.forEach((point, j) => {
if (!point || typeof point !== "object" || point.drilldown == null) return;
const nodes = baseEl.querySelectorAll(`[index="${i}"][j="${j}"]`);
nodes.forEach(
(node) => node.classList.add("apexcharts-drilldown-target")
);
});
});
}
}
_core__default.registerFeatures({ drilldown: Drilldown });
export {
default2 as default
};