apexcharts
Version:
A JavaScript Chart Library
25,980 lines • 946 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b2) => {
for (var prop in b2 || (b2 = {}))
if (__hasOwnProp.call(b2, prop))
__defNormalProp(a, prop, b2[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b2)) {
if (__propIsEnum.call(b2, prop))
__defNormalProp(a, prop, b2[prop]);
}
return a;
};
var __spreadProps = (a, b2) => __defProps(a, __getOwnPropDescs(b2));
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 v7.0.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 Scatter = _core.__apex_charts_Scatter;
const Animations = _core.__apex_Animations;
const computeStagger = _core.__apex_Animations_computeStagger;
const applyAnimationPolicy = _core.__apex_Animations_applyAnimationPolicy;
const prefersReducedMotion = _core.__apex_Animations_prefersReducedMotion;
const applyProgressiveReveal = _core.__apex_Animations_applyProgressiveReveal;
const Base = _core.__apex_Base;
const register = _core.__apex_ChartFactory_register;
const getChartClass = _core.__apex_ChartFactory_getChartClass;
const isCustom = _core.__apex_ChartFactory_isCustom;
const Core = _core.__apex_Core;
const CoreUtils = _core.__apex_CoreUtils;
const Crosshairs = _core.__apex_Crosshairs;
const Data = _core.__apex_Data;
const DataLabels = _core.__apex_DataLabels;
const Events = _core.__apex_Events;
const Fill = _core.__apex_Fill;
const Filters = _core.__apex_Filters;
const Formatters = _core.__apex_Formatters;
const Graphics = _core.__apex_Graphics;
const Markers = _core.__apex_Markers;
const Range = _core.__apex_Range;
const Responsive = _core.__apex_Responsive;
const Scales = _core.__apex_Scales;
const Series = _core.__apex_Series;
const Theme = _core.__apex_Theme;
const TimeScale = _core.__apex_TimeScale;
const TitleSubtitle = _core.__apex_TitleSubtitle;
const Axes = _core.__apex_axes_Axes;
const AxesUtils = _core.__apex_axes_AxesUtils;
const Grid$1 = _core.__apex_axes_Grid;
const XAxis$1 = _core.__apex_axes_XAxis;
const YAxis$1 = _core.__apex_axes_YAxis;
const Dimensions = _core.__apex_dimensions_Dimensions;
const Grid = _core.__apex_dimensions_Grid;
const Helpers$4 = _core.__apex_dimensions_Helpers;
const XAxis = _core.__apex_dimensions_XAxis;
const YAxis = _core.__apex_dimensions_YAxis;
const Destroy = _core.__apex_helpers_Destroy;
const InitCtxVariables = _core.__apex_helpers_InitCtxVariables;
const Localization = _core.__apex_helpers_Localization;
const UpdateHelpers = _core.__apex_helpers_UpdateHelpers;
const Config = _core.__apex_Config;
const Defaults = _core.__apex_Defaults;
const Globals = _core.__apex_Globals;
const Options = _core.__apex_Options;
const AxesTooltip = _core.__apex_tooltip_AxesTooltip;
const Intersect = _core.__apex_tooltip_Intersect;
const Labels = _core.__apex_tooltip_Labels;
const Marker = _core.__apex_tooltip_Marker;
const Position = _core.__apex_tooltip_Position;
const Tooltip = _core.__apex_tooltip_Tooltip;
const Utils$1 = _core.__apex_tooltip_Utils;
const BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs;
const SSRDOMShim = _core.__apex_DOMShim_SSRDOMShim;
const SSRElement = _core.__apex_DOMShim_SSRElement;
const SSRClassList = _core.__apex_DOMShim_SSRClassList;
const parsePath = _core.__apex_PathMorphing_parsePath;
const morphPaths = _core.__apex_PathMorphing_morphPaths;
const pathBbox = _core.__apex_PathMorphing_pathBbox;
const arrayToPath = _core.__apex_PathMorphing_arrayToPath;
const SVGAnimationRunner = _core.__apex_SVGAnimation_SVGAnimationRunner;
const installAnimationMethods = _core.__apex_SVGAnimation_installAnimationMethods;
const SVGContainer = _core.__apex_SVGContainer;
const installDraggable = _core.__apex_SVGDraggable_installDraggable;
const SVGElement = _core.__apex_SVGElement;
const SVGFilter = _core.__apex_SVGFilter_SVGFilter;
const FilterBuilder = _core.__apex_SVGFilter_FilterBuilder;
const installFilterMethods = _core.__apex_SVGFilter_installFilterMethods;
const SVGGradient = _core.__apex_SVGGradient_SVGGradient;
const SVGPattern = _core.__apex_SVGPattern_SVGPattern;
const installSelectable = _core.__apex_SVGSelectable_installSelectable;
const SVG = _core.__apex_index_SVG;
const Box$1 = _core.__apex_index_Box;
const SVGNS$1 = _core.__apex_math_SVGNS;
const Point = _core.__apex_math_Point;
const Matrix = _core.__apex_math_Matrix;
const Box = _core.__apex_math_Box;
const LINE_HEIGHT_RATIO = _core.__apex_Constants_LINE_HEIGHT_RATIO;
const NICE_SCALE_ALLOWED_MAG_MSD = _core.__apex_Constants_NICE_SCALE_ALLOWED_MAG_MSD;
const NICE_SCALE_DEFAULT_TICKS = _core.__apex_Constants_NICE_SCALE_DEFAULT_TICKS;
const DateTime = _core.__apex_DateTime;
const Environment = _core.__apex_Environment_Environment;
const PerformanceCache = _core.__apex_PerformanceCache;
const addResizeListener = _core.__apex_Resize_addResizeListener;
const removeResizeListener = _core.__apex_Resize_removeResizeListener;
const getThemePalettes = _core.__apex_ThemePalettes_getThemePalettes;
const Utils = _core.__apex_Utils;
const coreInternals = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
__apex_Animations: Animations,
__apex_Animations_applyAnimationPolicy: applyAnimationPolicy,
__apex_Animations_applyProgressiveReveal: applyProgressiveReveal,
__apex_Animations_computeStagger: computeStagger,
__apex_Animations_prefersReducedMotion: prefersReducedMotion,
__apex_Base: Base,
__apex_BrowserAPIs_BrowserAPIs: BrowserAPIs,
__apex_ChartFactory_getChartClass: getChartClass,
__apex_ChartFactory_isCustom: isCustom,
__apex_ChartFactory_register: register,
__apex_Config: Config,
__apex_Constants_LINE_HEIGHT_RATIO: LINE_HEIGHT_RATIO,
__apex_Constants_NICE_SCALE_ALLOWED_MAG_MSD: NICE_SCALE_ALLOWED_MAG_MSD,
__apex_Constants_NICE_SCALE_DEFAULT_TICKS: NICE_SCALE_DEFAULT_TICKS,
__apex_Core: Core,
__apex_CoreUtils: CoreUtils,
__apex_Crosshairs: Crosshairs,
__apex_DOMShim_SSRClassList: SSRClassList,
__apex_DOMShim_SSRDOMShim: SSRDOMShim,
__apex_DOMShim_SSRElement: SSRElement,
__apex_Data: Data,
__apex_DataLabels: DataLabels,
__apex_DateTime: DateTime,
__apex_Defaults: Defaults,
__apex_Environment_Environment: Environment,
__apex_Events: Events,
__apex_Fill: Fill,
__apex_Filters: Filters,
__apex_Formatters: Formatters,
__apex_Globals: Globals,
__apex_Graphics: Graphics,
__apex_Markers: Markers,
__apex_Options: Options,
__apex_PathMorphing_arrayToPath: arrayToPath,
__apex_PathMorphing_morphPaths: morphPaths,
__apex_PathMorphing_parsePath: parsePath,
__apex_PathMorphing_pathBbox: pathBbox,
__apex_PerformanceCache: PerformanceCache,
__apex_Range: Range,
__apex_Resize_addResizeListener: addResizeListener,
__apex_Resize_removeResizeListener: removeResizeListener,
__apex_Responsive: Responsive,
__apex_SVGAnimation_SVGAnimationRunner: SVGAnimationRunner,
__apex_SVGAnimation_installAnimationMethods: installAnimationMethods,
__apex_SVGContainer: SVGContainer,
__apex_SVGDraggable_installDraggable: installDraggable,
__apex_SVGElement: SVGElement,
__apex_SVGFilter_FilterBuilder: FilterBuilder,
__apex_SVGFilter_SVGFilter: SVGFilter,
__apex_SVGFilter_installFilterMethods: installFilterMethods,
__apex_SVGGradient_SVGGradient: SVGGradient,
__apex_SVGPattern_SVGPattern: SVGPattern,
__apex_SVGSelectable_installSelectable: installSelectable,
__apex_Scales: Scales,
__apex_Series: Series,
__apex_Theme: Theme,
__apex_ThemePalettes_getThemePalettes: getThemePalettes,
__apex_TimeScale: TimeScale,
__apex_TitleSubtitle: TitleSubtitle,
__apex_Utils: Utils,
__apex_axes_Axes: Axes,
__apex_axes_AxesUtils: AxesUtils,
__apex_axes_Grid: Grid$1,
__apex_axes_XAxis: XAxis$1,
__apex_axes_YAxis: YAxis$1,
__apex_charts_Scatter: Scatter,
__apex_dimensions_Dimensions: Dimensions,
__apex_dimensions_Grid: Grid,
__apex_dimensions_Helpers: Helpers$4,
__apex_dimensions_XAxis: XAxis,
__apex_dimensions_YAxis: YAxis,
__apex_helpers_Destroy: Destroy,
__apex_helpers_InitCtxVariables: InitCtxVariables,
__apex_helpers_Localization: Localization,
__apex_helpers_UpdateHelpers: UpdateHelpers,
__apex_index_Box: Box$1,
__apex_index_SVG: SVG,
__apex_math_Box: Box,
__apex_math_Matrix: Matrix,
__apex_math_Point: Point,
__apex_math_SVGNS: SVGNS$1,
__apex_tooltip_AxesTooltip: AxesTooltip,
__apex_tooltip_Intersect: Intersect,
__apex_tooltip_Labels: Labels,
__apex_tooltip_Marker: Marker,
__apex_tooltip_Position: Position,
__apex_tooltip_Tooltip: Tooltip,
__apex_tooltip_Utils: Utils$1,
default: _core__default
}, Symbol.toStringTag, { value: "Module" }));
class Exports {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
}
/**
* @param {string} svgString
*/
svgStringToNode(svgString) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgString, "image/svg+xml");
return svgDoc.documentElement;
}
/**
* @param {any} svg
* @param {number} scale
*/
scaleSvgNode(svg, scale) {
const svgWidth = parseFloat(svg.getAttributeNS(null, "width"));
const svgHeight = parseFloat(svg.getAttributeNS(null, "height"));
svg.setAttributeNS(null, "width", svgWidth * scale);
svg.setAttributeNS(null, "height", svgHeight * scale);
svg.setAttributeNS(null, "viewBox", "0 0 " + svgWidth + " " + svgHeight);
}
/**
* Inline any Strata canvas series layer into the clone as an SVG `<image>`.
* A serialized `<canvas>` loses its bitmap, so a canvas-mode export would drop
* the series; an `<image>` carrying the canvas `toDataURL()` preserves it in
* place. Because it replaces the `<foreignObject>` at the same DOM position,
* the grid-behind / annotations-in-front z-order is retained automatically.
* No-op in SVG mode (no series canvas present).
* @param {any} clonedNode the cloned elWrap about to be serialized
*/
inlineCanvasLayers(clonedNode) {
const w2 = this.w;
const XLINK = "http://www.w3.org/1999/xlink";
const origCanvases = w2.dom.elWrap.querySelectorAll(
".apexcharts-series-canvas"
);
if (!origCanvases.length) return;
const clonedFOs = clonedNode.querySelectorAll(".apexcharts-canvas-series");
for (let i = 0; i < origCanvases.length && i < clonedFOs.length; i++) {
let dataURL;
try {
dataURL = /** @type {HTMLCanvasElement} */
origCanvases[i].toDataURL();
} catch (e) {
continue;
}
const fo = clonedFOs[i];
const img = document.createElementNS(SVGNS$1, "image");
img.setAttribute("x", fo.getAttribute("x") || "0");
img.setAttribute("y", fo.getAttribute("y") || "0");
img.setAttribute("width", fo.getAttribute("width") || "0");
img.setAttribute("height", fo.getAttribute("height") || "0");
img.setAttribute("href", dataURL);
img.setAttributeNS(XLINK, "xlink:href", dataURL);
if (fo.parentNode) fo.parentNode.replaceChild(img, fo);
}
}
/**
* `querySelectorAll` as a typed array. Both HTML and SVG elements carry
* `style` / `classList`, but `NodeListOf<Element>` does not.
* @param {ParentNode} root
* @param {string} selector
* @returns {Array<HTMLElement | SVGElement>}
*/
queryStyleable(root, selector) {
return (
/** @type {Array<HTMLElement | SVGElement>} */
Array.prototype.slice.call(root.querySelectorAll(selector))
);
}
/**
* Applies `styles` only where the element has no inline value for that
* property yet.
*
* The rules being re-applied here came from a stylesheet, so anything a
* module set inline has to keep winning exactly like it does in the live
* DOM: `legend.fontSize` (Legend.js) and the heatmap gradient legend's
* deliberate `display`/`overflow`/`padding` overrides on the legend wrap
* (HeatmapGradientLegend.js) would otherwise be clobbered in the export.
* @param {HTMLElement | SVGElement} el
* @param {Record<string, string>} styles
*/
setStyleDefaults(el, styles) {
Object.keys(styles).forEach((prop) => {
if (el.style.getPropertyValue(prop) === "") {
el.style.setProperty(prop, styles[prop]);
}
});
}
/**
* Re-applies, as inline styles on the clone, the rules that used to reach
* the exported SVG through an injected `<style>` block. A strict
* Content-Security-Policy without `'unsafe-inline'` blocks that block and
* breaks the export, so the export must not depend on one. See #5146.
*
* Mirrors `src/assets/apexcharts-legend.css`. Interaction-only rules
* (`cursor`, `pointer-events`) are carried over for parity even though they
* do nothing in a static image; the layout rules are what matter.
* @param {HTMLElement} clonedNode the cloned elWrap about to be serialized
*/
applyExportStyles(clonedNode) {
const w2 = this.w;
this.queryStyleable(clonedNode, "style").forEach((el) => el.remove());
this.queryStyleable(
clonedNode,
[
".apexcharts-tooltip",
".apexcharts-toolbar",
".apexcharts-xaxistooltip",
".apexcharts-yaxistooltip",
".apexcharts-xcrosshairs",
".apexcharts-ycrosshairs",
".apexcharts-zoom-rect",
".apexcharts-selection-rect"
].join(", ")
).forEach((el) => {
el.style.setProperty("display", "none", "important");
});
this.queryStyleable(clonedNode, ".apexcharts-flip-y").forEach((el) => {
this.setStyleDefaults(el, {
transform: "scaleY(-1) translateY(-100%)",
"transform-origin": "top",
"transform-box": "fill-box"
});
});
this.queryStyleable(clonedNode, ".apexcharts-flip-x").forEach((el) => {
this.setStyleDefaults(el, {
transform: "scaleX(-1)",
"transform-origin": "center",
"transform-box": "fill-box"
});
});
if (!w2.config.legend.show || !w2.dom.elLegendWrap || !w2.dom.elLegendWrap.children.length) {
return;
}
this.queryStyleable(clonedNode, ".apexcharts-legend").forEach((el) => {
this.setStyleDefaults(el, {
display: "flex",
overflow: "auto",
padding: "0 10px"
});
const cl = el.classList;
const isSide = cl.contains("apx-legend-position-left") || cl.contains("apx-legend-position-right");
const isTopOrBottom = cl.contains("apx-legend-position-top") || cl.contains("apx-legend-position-bottom");
if (cl.contains("apexcharts-legend-group-horizontal")) {
this.setStyleDefaults(el, { "flex-direction": "column" });
}
if (isSide) {
this.setStyleDefaults(el, { "flex-direction": "column", bottom: "0" });
}
if (isTopOrBottom) {
this.setStyleDefaults(el, { "flex-wrap": "wrap" });
}
if (isSide || isTopOrBottom && cl.contains("apexcharts-align-left")) {
this.setStyleDefaults(el, {
"justify-content": "flex-start",
"align-items": "flex-start"
});
} else if (isTopOrBottom && cl.contains("apexcharts-align-center")) {
this.setStyleDefaults(el, {
"justify-content": "center",
"align-items": "center"
});
} else if (isTopOrBottom && cl.contains("apexcharts-align-right")) {
this.setStyleDefaults(el, {
"justify-content": "flex-end",
"align-items": "flex-end"
});
}
});
this.queryStyleable(clonedNode, ".apexcharts-legend-group").forEach(
(el) => {
this.setStyleDefaults(el, { display: "flex" });
}
);
this.queryStyleable(
clonedNode,
".apexcharts-legend-group-vertical"
).forEach((el) => {
this.setStyleDefaults(el, { "flex-direction": "column-reverse" });
});
this.queryStyleable(clonedNode, ".apexcharts-legend-series").forEach(
(el) => {
this.setStyleDefaults(el, {
cursor: el.classList.contains("apexcharts-no-click") ? "auto" : "pointer",
"line-height": "normal",
display: "flex",
"align-items": "center"
});
}
);
this.queryStyleable(clonedNode, ".apexcharts-legend-text").forEach((el) => {
this.setStyleDefaults(el, {
position: "relative",
"font-size": "14px"
});
});
this.queryStyleable(clonedNode, ".apexcharts-legend-marker").forEach(
(el) => {
this.setStyleDefaults(el, {
position: "relative",
display: "flex",
"align-items": "center",
"justify-content": "center",
cursor: "pointer",
"margin-right": "1px"
});
}
);
this.queryStyleable(clonedNode, ".apexcharts-inactive-legend").forEach(
(el) => {
this.setStyleDefaults(el, { opacity: "0.45" });
}
);
this.queryStyleable(
clonedNode,
".apexcharts-legend .apexcharts-hidden-zero-series, .apexcharts-legend .apexcharts-hidden-null-series"
).forEach((el) => {
el.style.setProperty("display", "none", "important");
});
}
/**
* The colour a raster export paints under the chart.
*
* A PNG needs an opaque base, so `dataURI` fills the canvas before drawing
* the SVG onto it. That fill used to be `#fff` whenever `chart.background`
* was unset *or* `'transparent'`, which is wrong for a dark theme:
* `theme.mode: 'dark'` moves `chart.foreColor` to a near-white `#f6f7f8` but
* leaves the background alone, so `background: 'transparent'` produced
* near-white labels, axes and legend on white — a PNG that looked like it had
* lost all its text. See #2920.
*
* The unset-background case was already covered: `Core.setupElements` paints
* the SVG paper `#343A3F` for a dark theme, and the clone carries that inline
* style into the export. `'transparent'` is the gap, because it makes that
* paper style transparent too and nothing is left to cover the white fill.
*
* An explicit non-transparent `chart.background` still wins, including one
* written by the Facet `--apx-surface` token (Theme assigns it into the same
* field).
* @returns {string}
*/
resolveExportBackground() {
const w2 = this.w;
const bg = w2.config.chart.background;
if (bg && bg !== "transparent") return bg;
return w2.config.theme.mode === "dark" ? "#343A3F" : "#fff";
}
/**
* Font families the rendered chart actually paints with.
*
* Read off the live DOM rather than the config: computed styles resolve
* `chart.fontFamily`, every per-element `style.fontFamily` override, and any
* family the chart inherits from the page, none of which are reliably
* enumerable from config alone. The clone is not in the document, so its
* computed styles would come back empty.
* @returns {Set<string>}
*/
collectFontFamilies() {
const families = /* @__PURE__ */ new Set();
const w2 = this.w;
if (!Environment.isBrowser() || !w2.dom.elWrap) return families;
const els = this.queryStyleable(
w2.dom.elWrap,
"text, tspan, .apexcharts-legend-text, .apexcharts-title-text, .apexcharts-subtitle-text"
);
const all = [w2.dom.elWrap, ...els];
all.forEach((el) => {
const cs = (
/** @type {any} */
BrowserAPIs.getComputedStyle(
/** @type {any} */
el
)
);
const ff = cs && cs.fontFamily;
if (!ff) return;
ff.split(",").forEach((name) => {
const clean = name.trim().replace(/^['"]|['"]$/g, "");
if (clean) families.add(clean.toLowerCase());
});
});
return families;
}
/**
* Every `@font-face` rule reachable from the document, as `{ family, css }`.
*
* Same-origin sheets are read through `cssRules`. A cross-origin sheet throws
* on that access, so it is re-fetched by href and its `@font-face` blocks are
* pulled out of the text: that is the path that matters in practice, since
* hosted webfonts (the subject of #3617) are exactly the cross-origin case.
* @returns {Promise<Array<{family: string, css: string}>>}
*/
collectFontFaceRules() {
if (!Environment.isBrowser()) return Promise.resolve([]);
const found = [];
const remote = [];
const pushFromText = (cssText) => {
const blocks = cssText.match(/@font-face\s*\{[^}]*\}/gi) || [];
blocks.forEach((css) => {
const m2 = css.match(/font-family\s*:\s*([^;}]+)/i);
if (!m2) return;
const family = m2[1].trim().replace(/^['"]|['"]$/g, "");
found.push({ family: family.toLowerCase(), css });
});
};
const sheets = Array.from(document.styleSheets || []);
sheets.forEach((sheet) => {
let rules = null;
try {
rules = sheet.cssRules;
} catch (e) {
rules = null;
}
if (rules) {
Array.from(rules).forEach((rule) => {
if (rule.type === 5 && rule.cssText) pushFromText(rule.cssText);
});
return;
}
if (sheet.href) {
remote.push(
fetch(sheet.href).then((r) => r.ok ? r.text() : "").then(pushFromText).catch(() => {
})
);
}
});
return Promise.all(remote).then(() => found);
}
/**
* Inline the `@font-face` rules for the families the chart uses, with the
* font files themselves as base64 data URIs, into the exported SVG.
*
* Needed because the export is a standalone document: rasterizing it through
* `<img src="data:image/svg+xml,...">` gives it no access to the page's
* stylesheets *or* its loaded fonts, and an SVG-as-image may not fetch
* external resources at all. Without this the text silently reflows into a
* generic fallback face. See #3617.
*
* `@font-face` only exists as a stylesheet construct, so unlike the rules in
* `applyExportStyles` this cannot be expressed as inline styles and has to
* ship as a `<style>` element. A page whose CSP forbids inline styles will
* drop it and fall back to today's behaviour, which is why the whole thing is
* best-effort: any failure leaves the export exactly as it was.
* @param {any} svgNode the parsed outer <svg> about to be serialized
* @returns {Promise<void>}
*/
embedFonts(svgNode) {
const w2 = this.w;
if (!Environment.isBrowser() || !w2.config.chart.toolbar.export.embedFonts || typeof fetch !== "function") {
return Promise.resolve();
}
const used = this.collectFontFamilies();
if (!used.size) return Promise.resolve();
return this.collectFontFaceRules().then((faces) => {
const wanted = faces.filter((f) => used.has(f.family));
if (!wanted.length) return Promise.resolve([]);
return Promise.all(
wanted.map(
(face) => this.inlineFontFaceUrls(face.css).catch(() => null)
)
);
}).then((cssBlocks) => {
const css = (cssBlocks || []).filter(Boolean).join("\n");
if (!css) return;
const style = document.createElementNS(SVGNS$1, "style");
style.textContent = css;
svgNode.insertBefore(style, svgNode.firstChild);
}).catch(() => {
});
}
/**
* Replace every remote `url(...)` in one `@font-face` block with a base64
* data URI. Resolves to null if no url could be fetched, so the caller can
* drop a block that would only reference unreachable files.
* @param {string} css
* @returns {Promise<string | null>}
*/
inlineFontFaceUrls(css) {
const urls = [];
const re = /url\(\s*(['"]?)([^'")]+)\1\s*\)/gi;
let m2;
while ((m2 = re.exec(css)) !== null) {
if (!m2[2].startsWith("data:")) urls.push(m2[2]);
}
if (!urls.length) return Promise.resolve(css.includes("data:") ? css : null);
return Promise.all(
urls.map(
(url) => this.fetchAsDataUri(url).then((dataUri) => ({ url, dataUri })).catch(() => ({ url, dataUri: null }))
)
).then((results) => {
let out = css;
let replaced = 0;
results.forEach(({ url, dataUri }) => {
if (!dataUri) return;
out = out.split(url).join(dataUri);
replaced++;
});
return replaced ? out : null;
});
}
/**
* Fetch a binary asset as a base64 data URI.
*
* Uses `fetch` rather than the `<img>`+canvas route in `getBase64FromUrl`:
* that route only works for raster images and taints the canvas for any
* response without CORS headers, whereas this works for fonts too and fails
* cleanly when CORS denies it.
* @param {string} url
* @returns {Promise<string>}
*/
fetchAsDataUri(url) {
if (typeof fetch !== "function" || typeof btoa !== "function") {
return Promise.reject(new Error("fetch unavailable"));
}
return fetch(url).then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
const type = res.headers.get("content-type") || "application/octet-stream";
return res.arrayBuffer().then((buf) => {
const bytes = new Uint8Array(buf);
let binary = "";
const CHUNK = 32768;
for (let i = 0; i < bytes.length; i += CHUNK) {
binary += String.fromCharCode.apply(
null,
/** @type {any} */
bytes.subarray(i, i + CHUNK)
);
}
return `data:${type};base64,${btoa(binary)}`;
});
});
}
/**
* @param {number} [_scale]
*/
getSvgString(_scale) {
return new Promise((resolve) => {
const w2 = this.w;
let scale = _scale || w2.config.chart.toolbar.export.scale || w2.config.chart.toolbar.export.width / w2.globals.svgWidth;
if (!scale) {
scale = 1;
}
const width = w2.globals.svgWidth * scale;
const height = w2.globals.svgHeight * scale;
const clonedNode = (
/** @type {HTMLElement} */
w2.dom.elWrap.cloneNode(true)
);
clonedNode.style.width = width + "px";
clonedNode.style.height = height + "px";
this.inlineCanvasLayers(clonedNode);
this.applyExportStyles(clonedNode);
const serializedNode = new XMLSerializer().serializeToString(clonedNode);
let svgString = `
<svg xmlns="http://www.w3.org/2000/svg"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="apexcharts-svg"
xmlns:data="ApexChartsNS"
transform="translate(0, 0)"
width="${w2.globals.svgWidth}px" height="${w2.globals.svgHeight}px">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="width:${width}px; height:${height}px;">
${serializedNode}
</div>
</foreignObject>
</svg>
`;
const svgNode = this.svgStringToNode(svgString);
if (scale !== 1) {
this.scaleSvgNode(svgNode, scale);
}
Promise.all([
this.convertImagesToBase64(svgNode),
this.embedFonts(svgNode)
]).then(() => {
svgString = new XMLSerializer().serializeToString(svgNode);
resolve(svgString.replace(/ /g, " "));
});
});
}
/**
* Turn every remote `<image>` in the export into an inline data URI.
*
* This is not an optimisation: an SVG rasterized through `<img src="data:...">`
* is not allowed to fetch external resources, so any href left pointing at a
* URL vanishes from the PNG entirely. Image annotations and `hollow.image`
* were disappearing from downloads for exactly this reason. See #3170.
*
* Two things were missing before. `SVGContainer.image()` writes `xlink:href`,
* but Strata's inlined canvas layers and hand-authored `customSVG` markup use
* the plain `href` form, and only the namespaced attribute was being read.
* And conversion went through `<img>`+canvas, which taints (and therefore
* throws) for any response without CORS headers, so a cross-origin icon,
* the common case, silently failed. `fetch` is tried first and only falls
* back to the canvas route, which still helps for a same-origin image on a
* page whose CSP blocks `connect-src`.
* @param {any} svgNode
*/
convertImagesToBase64(svgNode) {
const XLINK = "http://www.w3.org/1999/xlink";
const images = svgNode.getElementsByTagName("image");
const promises = Array.from(images).map((img) => {
const nsHref = img.getAttributeNS(XLINK, "href");
const plainHref = img.getAttribute("href");
const href = nsHref || plainHref;
if (!href || href.startsWith("data:")) return Promise.resolve();
const write = (base64) => {
if (nsHref) img.setAttributeNS(XLINK, "href", base64);
if (plainHref || !nsHref) img.setAttribute("href", base64);
};
return this.fetchAsDataUri(href).then(write).catch(
() => this.getBase64FromUrl(href).then(write).catch((error) => {
console.error("Error converting image to base64:", error);
})
);
});
return Promise.all(promises);
}
/**
* @param {string} url
*/
getBase64FromUrl(url) {
if (Environment.isSSR()) return Promise.resolve(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");
if (ctx) ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL());
};
img.onerror = reject;
img.src = url;
});
}
svgUrl() {
return new Promise((resolve) => {
this.getSvgString().then((svgData) => {
const svgBlob = new Blob([svgData], {
type: "image/svg+xml;charset=utf-8"
});
resolve(URL.createObjectURL(svgBlob));
});
});
}
/**
* @param {Record<string, any> | undefined} options
*/
dataURI(options) {
if (Environment.isSSR()) return Promise.resolve({ imgURI: "" });
return new Promise((resolve) => {
const w2 = this.w;
const scale = options ? options.scale || options.width / w2.globals.svgWidth : 1;
const canvas = document.createElement("canvas");
canvas.width = w2.globals.svgWidth * scale;
canvas.height = parseInt(w2.dom.elWrap.style.height, 10) * scale;
const canvasBg = this.resolveExportBackground();
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.fillStyle = canvasBg;
ctx.fillRect(0, 0, canvas.width, canvas.height);
this.getSvgString(scale).then((svgData) => {
const svgUrl = "data:image/svg+xml," + encodeURIComponent(svgData);
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
ctx.drawImage(img, 0, 0);
const edgeCanvas = canvas;
if (edgeCanvas.msToBlob) {
const blob = edgeCanvas.msToBlob();
resolve({ blob });
} else {
const 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 } : width ? { width } : void 0;
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"
);
}
});
}
/** @param {{ series?: any, fileName?: any, columnDelimiter?: string, lineDelimiter?: string }} opts */
exportToCSV({
series,
fileName,
columnDelimiter = ",",
lineDelimiter = "\n"
}) {
const w2 = this.w;
if (!series) series = w2.config.series;
let columns = [];
const rows = [];
let result = "";
const universalBOM = "\uFEFF";
const gSeries = w2.seriesData.series.map((s, i) => {
return w2.globals.collapsedSeriesIndices.indexOf(i) === -1 ? s : [];
});
const csvSafe = (val) => {
if (val == null || Utils.isNumber(val)) return val;
const s = String(val);
return /^[=+\-@\t\r]/.test(s) ? `'${s}` : s;
};
const getFormattedCategory = (cat) => {
if (typeof w2.config.chart.toolbar.export.csv.categoryFormatter === "function") {
return w2.config.chart.toolbar.export.csv.categoryFormatter(cat);
}
if (w2.config.xaxis.type === "datetime" && String(cat).length >= 10) {
return new Date(cat).toDateString();
}
return Utils.isNumber(cat) ? cat : csvSafe(cat.split(columnDelimiter).join(""));
};
const getFormattedValue = (value) => {
return typeof w2.config.chart.toolbar.export.csv.valueFormatter === "function" ? w2.config.chart.toolbar.export.csv.valueFormatter(value) : csvSafe(value);
};
const seriesMaxDataLength = Math.max(
...series.map((s) => {
return s.data ? s.data.length : 0;
})
);
const dataFormat = new Data(this.w);
const axesUtils = new AxesUtils(this.w, {
theme: this.ctx.theme,
timeScale: this.ctx.timeScale
});
const getCat = (i) => {
let cat = "";
if (!w2.globals.axisCharts) {
cat = w2.config.labels[i];
} else {
if (w2.config.xaxis.type === "category" || w2.config.xaxis.convertedCatToNumeric) {
if (w2.globals.isBarHorizontal) {
const lbFormatter = w2.formatters.yLabelFormatters[0];
const sr = new Series(this.ctx.w);
const activeSeries = sr.getActiveConfigSeriesIndex();
cat = lbFormatter(w2.labelData.labels[i], {
seriesIndex: activeSeries,
dataPointIndex: i,
w: w2
});
} else {
cat = axesUtils.getLabel(
w2.labelData.labels,
w2.labelData.timescaleLabels,
0,
i
).text;
}
}
if (w2.config.xaxis.type === "datetime") {
if (w2.config.xaxis.categories.length) {
cat = w2.config.xaxis.categories[i];
} else if (w2.config.labels.length) {
cat = w2.config.labels[i];
}
}
}
if (cat === null) return "nullvalue";
if (Array.isArray(cat)) {
cat = cat.join(" ");
}
return Utils.isNumber(cat) ? cat : cat.split(columnDelimiter).join("");
};
const getEmptyDataForCsvColumn = () => {
return [...Array(seriesMaxDataLength)].map(() => "");
};
const handleAxisRowsColumns = (s, sI) => {
var _a, _b, _c, _d, _e, _f;
if (columns.length && sI === 0) {
rows.push(columns.join(columnDelimiter));
}
if (s.data) {
const rowData = s.data.length ? s.data : getEmptyDataForCsvColumn();
for (let i = 0; i < rowData.length; i++) {
columns = [];
let cat = getCat(i);
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) {
columns.push(getFormattedCategory(cat));
for (let ci = 0; ci < w2.seriesData.series.length; ci++) {
const value = dataFormat.isFormatXY() ? (_a = series[ci].data[i]) == null ? void 0 : _a.y : gSeries[ci][i];
columns.push(getFormattedValue(value));
}
}
if (w2.config.chart.type === "candlestick" || s.type && s.type === "candlestick") {
columns.pop();
columns.push(w2.candleData.seriesCandleO[sI][i]);
columns.push(w2.candleData.seriesCandleH[sI][i]);
columns.push(w2.candleData.seriesCandleL[sI][i]);
columns.push(w2.candleData.seriesCandleC[sI][i]);
}
if (w2.config.chart.type === "boxPlot" || s.type && s.type === "boxPlot") {
columns.pop();
columns.push(w2.candleData.seriesCandleO[sI][i]);
columns.push(w2.candleData.seriesCandleH[sI][i]);
columns.push(w2.candleData.seriesCandleM[sI][i]);
columns.push(w2.candleData.seriesCandleL[sI][i]);
columns.push(w2.candleData.seriesCandleC[sI][i]);
}
if (w2.config.chart.type === "rangeBar") {
columns.pop();
columns.push(w2.rangeData.seriesRangeStart[sI][i]);
columns.push(w2.rangeData.seriesRangeEnd[sI][i]);
}
if (w2.config.chart.type === "violin" || s.type && s.type === "violin") {
columns.pop();
columns.push((_b = w2.violinData.seriesViolinMin[sI]) == null ? void 0 : _b[i]);
columns.push((_c = w2.violinData.seriesViolinMax[sI]) == null ? void 0 : _c[i]);
columns.push((_f = (_e = (_d = w2.violinData.seriesViolinPoints[sI]) == null ? void 0 : _d[i]) == null ? void 0 : _e.length) != null ? _f : 0);
}
if (columns.length) {
rows.push(columns.join(columnDelimiter));
}
}
}
};
const handleUnequalXValues = () => {
const categories = /* @__PURE__ */ new Set();
const data = {};
series.forEach((s, sI) => {
s == null ? void 0 : 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 (!/** @type {Record<string,any>} */
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) => {
const values = (
/** @type {Record<string,any>} */
data[cat]
);
rows.push([getFormattedCategory(cat), ...values].join(columnDelimiter));
});
};
columns.push(w2.config.chart.toolbar.export.csv.headerCategory);
if (w2.config.chart.type === "boxPlot") {
columns.push("minimum");
columns.push("q1");
columns.push("median");
columns.push("q3");
columns.push("maximum");
} else if (w2.config.chart.type === "candlestick") {
columns.push("open");
columns.push("high");
columns.push("low");
columns.push("close");
} else if (w2.config.chart.type === "rangeBar") {
columns.push("minimum");
columns.push("maximum");
} else if (w2.config.chart.type === "violin") {
columns.push("minimum");
columns.push("maximum");
columns.push("observations");
} else {
series.map((s, sI) => {
const sname = (s.name ? s.name : `series-${sI}`) + "";
if (w2.globals.axisCharts) {
columns.push(
sname.split(columnDelimiter).join("") ? sname.split(columnDelimiter).join("") : `series-${sI}`
);
}
});
}
if (!w2.globals.axisCharts) {
columns.push(w2.config.chart.toolbar.export.csv.headerValue);
rows.push(columns.join(columnDelimiter));
}
if (!w2.globals.allSeriesHasEqualX && w2.globals.axisCharts && !w2.config.xaxis.categories.length && !w2.config.labels.length) {
handleUnequalXValues();
} else {
series.map((s, sI) => {
if (w2.globals.axisCharts) {
handleAxisRowsColumns(s, sI);
} else {
columns = [];
columns.push(getFormattedCategory(w2.labelData.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 : w2.config.chart.toolbar.export.csv.filename,
".csv"
);
}
/**
* @param {string} href
* @param {string} filename
* @param {string} ext
*/
triggerDownload(href, filename, ext) {
if (Environment.isSSR()) return;
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);
}
}
_core__default.registerFeatures({ exports: Exports });
const apexchartsLegendCSS = ".apexcharts-flip-y {\n transform: scaleY(-1) translateY(-100%);\n transform-origin: top;\n transform-box: fill-box;\n}\n.apexcharts-flip-x {\n transform: scaleX(-1);\n transform-origin: center;\n transform-box: fill-box;\n}\n.apexcharts-legend {\n display: flex;\n overflow: auto;\n padding: 0 10px;\n}\n.apexcharts-legend.apexcharts-legend-group-horizontal {\n flex-direction: column;\n}\n.apexcharts-legend-group {\n display: flex;\n}\n.apexcharts-legend-group-vertical {\n flex-direction: column-reverse;\n}\n.apexcharts-legend.apx-legend-position-bottom, .apexcharts-legend.apx-legend-position-top {\n flex-wrap: wrap\n}\n.apexcharts-legend.apx-legend-position-right, .apexcharts-legend.apx-legend-position-left {\n flex-direction: column;\n bottom: 0;\n}\n.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 {\n justify-content: flex-start;\n align-items: flex-start;\n}\n.apexcharts-legend.apx-legend-position-bottom.apexcharts-align-center, .apexcharts-legend.apx-legend-position-top.apexcharts-align-center {\n justify-content: center;\n align-items: center;\n}\n.apexcharts-legend.apx-legend-position-bottom.apexcharts-align-right, .apexcharts-legend.apx-legend-position-top.apexcharts-align-right {\n justify-content: flex-end;\n align-items: flex-end;\n}\n.apexcharts-legend-series {\n cursor: pointer;\n line-height: normal;\n display: flex;\n align-items: center;\n}\n.apexcharts-legend-text {\n position: relative;\n font-size: 14px;\n}\n.apexcharts-legend-text *, .apexcharts-legend-marker * {\n pointer-events: none;\n}\n.apexcharts-legend-marker {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n margin-right: 1px;\n}\n\n.apexcharts-legend-series.apexcharts-no-click {\n cursor: auto;\n}\n.apexcharts-legend .apexcharts-hidden-zero-series, .apexcharts-legend .apexcharts-hidden-null-series {\n display: none !important;\n}\n.apexcharts-inactive-legend {\n opacity: 0.45;\n} ";
let Helpers$3 = class Helpers {
/**
* @param {import('./Legend').default} lgCtx
*/
constructor(lgCtx) {
this.w = lgCtx.w;
this.lgCtx = lgCtx;
}
getLegendStyles() {
if (Environment.isSSR()) return null;
const stylesheet = document.createElement("style");
stylesheet.setAttribute("type", "text/css");
const nonce = this.w.config.chart.nonce;
if (nonce) {
stylesheet.setAttribute("nonce", nonce);
}
const rule = document.createTextNode(apexchartsLegendCSS);
stylesheet.appendChild(rule);
return stylesheet;
}
getLegendDimensions() {
const w2 = this.w;
const currLegendsWrap = w2.dom.baseEl.querySelector(".apexcharts-legend");
if (!currLegendsWrap) {
return { clwh: 0, clww: 0 };
}
const { width: currLegendsWrapWidth, height: currLegendsWrapHeight } = currLegendsWrap.getBoundingClientRect();
return {
clwh: currLegendsWrapHeight,
clww: currLegendsWrapWidth
};
}
appendToForeignObject() {
var _a;
const legendStyles = this.getLegendStyles();
if (this.w.config.chart.injectStyleSheet !== false && legendStyles) {
(_a = this.w.dom.elLegendForeign) == null ? void 0 : _a.appendChild(legendStyles);
}
}
/**
* @param {number} seriesCnt
* @param {boolean} isHidden
*/
toggleDataSeries(seriesCnt, isHidden) {
var _a, _b, _c;
const w2 = this.w;
if (w2.globals.axisCharts || w2.config.chart.type === "radialBar") {
w2.globals.resized = true;
let seriesEl = null;
let realIndex = null;
w2.globals.risingSeries = [];
if (w2.globals.axisCharts) {
seriesEl = (_a = Array.prototype.find.call(
w2.dom.baseEl.querySelectorAll(".apexcharts-series"),
(el) => el.getAttribute("data:realIndex") === String(seriesCnt)
)) != null ? _a : null;
if (!seriesEl) return;
realIndex = parseInt((_b = seriesEl.getAttribute("data:realIndex")) != null ? _b : "", 10);
} else {
seriesEl = w2.dom.baseEl.querySelector(
`.apexcharts-series[rel='${seriesCnt + 1}']`
);
if (!seriesEl) return;
realIndex = parseInt((_c = seriesEl.getAttribute("rel")) != null ? _c : "", 10) - 1;
}
if (isHidden) {
const seriesToMakeVisible = [
{
cs: w2.globals.collapsedSeries,
csi: w2.globals.collapsedSeriesIndices
},
{
cs: w2.globals.ancillaryCollapsedSeries,
csi: w2.globals.ancillaryCollapsedSeriesIndices
}
];
seriesToMakeVisible.forEach((r) => {
const cs = (
/** @type {any} */
r.cs
);
const csi = (
/** @type {any} */
r.csi
);
this.riseCollapsedSeries(
cs,
csi,
/** @type {number} */
realIndex
);
});
} else {
this.hideSeries({ seriesEl, realIndex });
}
if (w2.config.chart.accessibility.enabled) {
const legendItem = w2.dom.baseEl.querySelector(
`.apexcharts-legend-series[rel="${seriesCnt + 1}"]`
);
if (legendItem) {
const isCollapsed = w2.globals.collapsedSeriesIndices.includes(realIndex) || w2.globals.ancillaryCollapsedSeriesIndices.includes(realIndex);
legendItem.setAttribute(
"aria-pressed",
isCollapsed ? "true" : "false"
);
const legendTextEl = legendItem.querySelector(
".apexcharts-legend-text"
);
const seriesName = legendTextEl ? legendTextEl.textContent : w2.seriesData.seriesNames[seriesCnt];
const statusText = isCollapsed ? "hidden" : "visible";
legendItem.setAttribute(
"aria-label",
`${seriesName}, ${statusText}. Press Enter or Space to toggle.`
);
}
}
} else {
w2.globals.resized = true;
w2.globals.risingSeries = [];
if (isHidden) {
this.riseCollapsedSeries(
w2.globals.collapsedSeries,
w2.globals.collapsedSeriesIndices,
seriesCnt
);
} else {
const series = this.getSeriesAfterCollapsing({ realIndex: seriesCnt });
this.lgCtx.updateSeries(
series,
w2.config.chart.animations.dynamicAnimation.enabled
);
}
if (w2.config.chart.accessibility.enabled) {
const legendItem = w2.dom.baseEl.querySelector(
`.apexcharts-legend-series[rel="${seriesCnt + 1}"]`
);
if (legendItem) {
const isCollapsed = w2.globals.collapsedSeriesIndices.includes(seriesCnt);
legendItem.setAttribute(
"aria-pressed",
isCollapsed ? "true" : "false"
);
const legendTextEl = legendItem.querySelector(
".apexcharts-legend-text"
);
const seriesName = legendTextEl ? legendTextEl.textContent : w2.seriesData.seriesNames[seriesCnt];
const statusText = isCollapsed ? "hidden" : "visible";
legendItem.setAttribute(
"aria-label",
`${seriesName}, ${statusText}. Press Enter or Space to toggle.`
);
}
}
}
}
/**
* Non-axis "slice" container. A pie/donut/polarArea slice is normally a
* top-level series element (numeric form: `series = [n, n, n]`), but object
* form (`series = [{ data: [{ x, y, drilldown }, ...] }]`, which pie/donut
* drilldown requires) packs every slice as a data point inside a single
* series. Return the array that actually holds the slice values so a
* slice/legend index addresses the right thing. Scoped to the pie family so
* unit charts (which share this non-axis path) are untouched.
* @param {any[]} series
* @returns {any[]}
*/
_nonAxisSliceContainer(series) {
const type = this.w.config.chart.type;
if ((type === "pie" || type === "donut" || type === "polarArea" || type === "sunburst") && series.length === 1 && series[0] && typeof series[0] === "object" && Array.isArray(series[0].data)) {
return series[0].data;
}
return series;
}
/**
* Read a non-axis slice value (handles `{ x, y }` data points and plain
* numbers).
* @param {any} sliceEntry
* @returns {number}
*/
_readSliceValue(sliceEntry) {
if (this.w.config.chart.type === "unit" && sliceEntry && Array.isArray(sliceEntry.data)) {
return sliceEntry.data;
}
return sliceEntry && typeof sliceEntry === "object" ? sliceEntry.y : sliceEntry;
}
/**
* Write a non-axis slice value in place, preserving `{ x, drilldown, ... }`
* on object data points.
* @param {any[]} container
* @param {number} i
* @param {number} value
*/
_writeSliceValue(container, i, value) {
const entry = container[i];
if (this.w.config.chart.type === "unit" && entry && Array.isArray(entry.data)) {
entry.data = Array.isArray(value) ? value : [];
return;
}
if (entry && typeof entry === "object") {
entry.y = value;
} else {
container[i] = value;
}
}
/** @param {{realIndex: any}} opts */
getSeriesAfterCollapsing({ realIndex }) {
var _a, _b;
const w2 = this.w;
const gl = w2.globals;
const series = Utils.clone(w2.config.series);
if (gl.axisCharts) {
const yaxis = w2.config.yaxis[gl.seriesYAxisReverseMap[realIndex]];
const collapseData = {
index: realIndex,
data: series[realIndex].data.slice(),
type: series[realIndex].type || w2.config.chart.type,
// The category name pins the hide across a data update that reorders or
// regroups categories (e.g. a storyboard beat): the collapse is
// reconciled by name, not index. See Series.reconcileCollapsedByName.
name: (gl.seriesNames || [])[realIndex]
};
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);
const removeIndexOfRising = gl.risingSeries.indexOf(realIndex);
gl.risingSeries.splice(removeIndexOfRising, 1);
}
}
} else {
if (gl.collapsedSeriesIndices.indexOf(realIndex) < 0) {
const container = this._nonAxisSliceContainer(series);
gl.collapsedSeries.push({
index: realIndex,
// Store the original slice VALUE so it can be restored on rise. In
// object form this is a data point's `y`, not the whole series entry.
data: this._readSliceValue(container[realIndex]),
type: (
/** @type {any} */
(_b = (_a = w2.config.series[realIndex]) == null ? void 0 : _a.type) != null ? _b : "line"
),
// Pin the hide by category name so it survives a regroup (see above).
name: (gl.seriesNames || [])[realIndex]
});
gl.collapsedSeriesIndices.push(realIndex);
}
}
const seriesCount = gl.axisCharts ? w2.config.series.length : this._nonAxisSliceContainer(series).length;
gl.allSeriesCollapsed = gl.collapsedSeries.length + gl.ancillaryCollapsedSeries.length === seriesCount;
return this._getSeriesBasedOnCollapsedState(series);
}
/** @param {{seriesEl: any, realIndex: any}} opts */
hideSeries({ seriesEl, realIndex }) {
const w2 = this.w;
const series = this.getSeriesAfterCollapsing({
realIndex
});
const 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");
}
}
}
const animate = w2.config.chart.animations.dynamicAnimation.enabled;
if (animate) {
w2.globals.collapsingSeriesIndices = [realIndex];
}
const clearCollapsing = () => {
w2.globals.collapsingSeriesIndices = [];
};
const updated = this.lgCtx.updateSeries(series, animate);
clearCollapsing();
if (updated && typeof updated.then === "function") {
updated.then(clearCollapsing, clearCollapsing);
}
}
/**
* @param {any[]} collapsedSeries
* @param {number[]} seriesIndices
* @param {number} realIndex
*/
riseCollapsedSeries(collapsedSeries, seriesIndices, realIndex) {
const w2 = this.w;
let series = Utils.clone(w2.config.series);
if (collapsedSeries.length > 0) {
for (let c = 0; c < collapsedSeries.length; c++) {
if (collapsedSeries[c].index === realIndex) {
if (w2.globals.axisCharts) {
series[realIndex].data = collapsedSeries[c].data.slice();
series[realIndex].hidden = false;
} else {
const container = this._nonAxisSliceContainer(series);
this._writeSliceValue(container, realIndex, collapsedSeries[c].data);
}
collapsedSeries.splice(c, 1);
seriesIndices.splice(c, 1);
w2.globals.risingSeries.push(realIndex);
c--;
}
}
series = this._getSeriesBasedOnCollapsedState(series);
this.lgCtx.updateSeries(
series,
w2.config.chart.animations.dynamicAnimation.enabled
);
}
}
/**
* @param {any[]} series
*/
_getSeriesBasedOnCollapsedState(series) {
const w2 = this.w;
let collapsed = 0;
if (w2.globals.axisCharts) {
series.forEach((s, sI) => {
if (!(w2.globals.collapsedSeriesIndices.indexOf(sI) < 0 && w2.globals.ancillaryCollapsedSeriesIndices.indexOf(sI) < 0)) {
series[sI].data = [];
collapsed++;
}
});
} else {
const container = this._nonAxisSliceContainer(series);
container.forEach((s, sI) => {
if (!(w2.globals.collapsedSeriesIndices.indexOf(sI) < 0)) {
this._writeSliceValue(container, sI, 0);
collapsed++;
}
});
}
const seriesCount = w2.globals.axisCharts ? series.length : this._nonAxisSliceContainer(series).length;
w2.globals.allSeriesCollapsed = collapsed === seriesCount;
return series;
}
};
const DEFAULT_DIVERGING = ["#cf4d3f", "#8f9499", "#26a75b"];
const lerp$1 = (a, b2, t) => a + (b2 - a) * t;
function toHexPair(n) {
const v2 = Math.max(0, Math.min(255, Math.round(n)));
return v2.toString(16).padStart(2, "0");
}
function mixColors(c1, c2, t) {
const a = Utils.parseHex(normalizeHex(c1));
const b2 = Utils.parseHex(normalizeHex(c2));
if (!a || !b2) return c1;
return "#" + toHexPair(lerp$1(a[0], b2[0], t)) + toHexPair(lerp$1(a[1], b2[1], t)) + toHexPair(lerp$1(a[2], b2[2], t));
}
function normalizeHex(c) {
if (typeof c !== "string") return "#000000";
if (Utils.isColorHex(c)) return c;
const asHex = Utils.rgb2hex(c);
return asHex || "#000000";
}
function colorValueOf(w2, i, j) {
const series = (
/** @type {any} */
w2.config.series[i]
);
const datum = series && Array.isArray(series.data) ? series.data[j] : null;
return colorValueOfDatum(w2, datum, i, j);
}
function colorValueOfDatum(w2, datum, i, j) {
var _a, _b, _c;
if (!datum || typeof datum !== "object") return null;
const accessor = (_c = (_b = (_a = w2.config.plotOptions) == null ? void 0 : _a.treemap) == null ? void 0 : _b.colorScale) == null ? void 0 : _c.colorValue;
let raw;
if (typeof accessor === "function") {
raw = accessor(datum, { seriesIndex: i, dataPointIndex: j, w: w2 });
} else if (typeof accessor === "string") {
raw = datum[accessor];
} else {
raw = datum.colorValue;
}
if (raw == null) return null;
const n = Number(raw);
return Number.isFinite(n) ? n : null;
}
function resolveStops(cfg, min, max, midpoint) {
if (Array.isArray(cfg.stops) && cfg.stops.length >= 2) {
return cfg.stops.filter((s) => s && Number.isFinite(Number(s.value))).map((s) => ({
value: Number(s.value),
color: normalizeHex(s.color)
})).sort(
(a, b2) => a.value - b2.value
);
}
const colors = (Array.isArray(cfg.colors) && cfg.colors.length >= 2 ? cfg.colors : DEFAULT_DIVERGING).map(normalizeHex);
const n = colors.length;
if (midpoint != null && n >= 3) {
const mid = Math.floor((n - 1) / 2);
const out = [];
for (let k = 0; k <= mid; k++) {
out.push({ value: lerp$1(min, midpoint, k / mid), color: colors[k] });
}
for (let k = mid + 1; k < n; k++) {
out.push({
value: lerp$1(midpoint, max, (k - mid) / (n - 1 - mid)),
color: colors[k]
});
}
return out;
}
return colors.map((c, k) => ({
value: lerp$1(min, max, k / (n - 1)),
color: c
}));
}
function buildContinuousScale(w2) {
var _a, _b, _c;
const cs = (_c = (_b = (_a = w2.config) == null ? void 0 : _a.plotOptions) == null ? void 0 : _b.treemap) == null ? void 0 : _c.colorScale;
const cfg = cs && cs.gradient;
if (!cfg) return null;
if (cfg.enabled === false) return null;
const series = (
/** @type {any} */
w2.config.series || []
);
let dataMin = Infinity;
let dataMax = -Infinity;
let found = false;
for (let i = 0; i < series.length; i++) {
const data = series[i] && series[i].data;
if (!Array.isArray(data)) continue;
for (let j = 0; j < data.length; j++) {
const v2 = colorValueOfDatum(w2, data[j], i, j);
if (v2 == null) continue;
found = true;
if (v2 < dataMin) dataMin = v2;
if (v2 > dataMax) dataMax = v2;
}
}
if (!found && cfg.enabled !== true) return null;
if (!Number.isFinite(dataMin)) {
dataMin = 0;
dataMax = 0;
}
let min = Number.isFinite(Number(cfg.min)) ? Number(cfg.min) : dataMin;
let max = Number.isFinite(Number(cfg.max)) ? Number(cfg.max) : dataMax;
let midpoint = null;
if (cfg.midpoint === null) {
midpoint = null;
} else if (Number.isFinite(Number(cfg.midpoint))) {
midpoint = Number(cfg.midpoint);
} else if (min < 0 && max > 0) {
midpoint = 0;
}
if (midpoint != null && cfg.symmetric !== false && !Number.isFinite(Number(cfg.min)) && !Number.isFinite(Number(cfg.max))) {
const reach = Math.max(Math.abs(min - midpoint), Math.abs(max - midpoint));
min = midpoint - reach;
max = midpoint + reach;
}
if (max === min) {
min -= 0.5;
max += 0.5;
}
const stops = resolveStops(cfg, min, max, midpoint);
if (stops.length < 2) return null;
const at = (v2) => {
if (!Number.isFinite(v2)) return stops[Math.floor(stops.length / 2)].color;
if (v2 <= stops[0].value) return stops[0].color;
const last = stops[stops.length - 1];
if (v2 >= last.value) return last.color;
for (let k = 1; k < stops.length; k++) {
const hi = stops[k];
if (v2 <= hi.value) {
const lo = stops[k - 1];
const span2 = hi.value - lo.value;
const t = span2 === 0 ? 0 : (v2 - lo.value) / span2;
return mixColors(lo.color, hi.color, t);
}
}
return last.color;
};
const span = max - min;
const legendStops = stops.map((s) => ({
percent: span === 0 ? 0 : (s.value - min) / span,
color: s.color
}));
return { min, max, midpoint, stops, at, legendStops };
}
function readableOn(bg) {
const hex = normalizeHex(bg);
return Utils.getContrastRatio(hex, "#ffffff") >= Utils.getContrastRatio(hex, "#000000") ? "#ffffff" : "#000000";
}
const SVG_NS = "http://www.w3.org/2000/svg";
class HeatmapGradientLegend {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
this.svgEl = null;
this.arrowEl = null;
this.hoverValueEl = null;
this._min = 0;
this._max = 0;
this._geom = null;
this._bandHitEls = [];
this._activeBandIndex = -1;
this._targetEl = null;
this._onCellEnter = this._onCellEnter.bind(this);
this._onCellLeave = this._onCellLeave.bind(this);
this._onBandEnter = this._onBandEnter.bind(this);
this._onBandLeave = this._onBandLeave.bind(this);
}
/** Default value formatter for min/max labels and the hover tooltip. */
_getFormatter() {
const cfg = this._cfg();
if (typeof cfg.formatter === "function") return cfg.formatter;
return (v2) => {
if (!Number.isFinite(v2)) return String(v2);
const abs = Math.abs(v2);
if (abs >= 1e3) return v2.toFixed(0);
if (abs >= 10) return v2.toFixed(1);
return v2.toFixed(2);
};
}
/**
* The colorScale of whichever chart type is being drawn. Every chart type
* that encodes a value as colour carries the same `colorScale` shape, so one
* strip serves them all rather than a near-copy per type.
* @param {any} w
*/
static colorScaleOf(w2) {
var _a, _b, _c, _d, _e;
const type = (_b = (_a = w2 == null ? void 0 : w2.config) == null ? void 0 : _a.chart) == null ? void 0 : _b.type;
if (!type) return null;
return ((_e = (_d = (_c = w2 == null ? void 0 : w2.config) == null ? void 0 : _c.plotOptions) == null ? void 0 : _d[type]) == null ? void 0 : _e.colorScale) || null;
}
/**
* @param {any} w
*/
static configFor(w2) {
const cs = HeatmapGradientLegend.colorScaleOf(w2);
return cs && cs.gradientLegend || null;
}
/** This instance's gradient-legend config. */
_cfg() {
return HeatmapGradientLegend.configFor(this.w) || {};
}
/**
* True when the user has opted into the gradient legend variant.
* @param {any} w
*/
static isEnabled(w2) {
if (!HeatmapGradientLegend.supports(w2)) return false;
const cfg = HeatmapGradientLegend.configFor(w2);
return !!(cfg && cfg.enabled);
}
/**
* Chart types this legend can serve: those that encode a value as colour
* through a `colorScale`. Everything else gets the categorical legend.
* @param {any} w
*/
static supports(w2) {
var _a, _b;
const type = (_b = (_a = w2 == null ? void 0 : w2.config) == null ? void 0 : _a.chart) == null ? void 0 : _b.type;
return type === "heatmap" || type === "treemap";
}
/**
* Build the gradient legend DOM into `elLegendWrap`.
* Caller is responsible for clearing the wrap first.
* @param {HTMLElement|null} [targetEl] detached mode: draw into this
* element instead (a trellis's shared legend slot); the host owns layout,
* so all plot-relative positioning is skipped.
*/
draw(targetEl = null) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const w2 = this.w;
this._targetEl = targetEl;
const elLegendWrap = (
/** @type {HTMLElement} */
targetEl || w2.dom.elLegendWrap
);
if (!elLegendWrap) return;
const cfg = this._cfg();
const position = w2.config.legend.position;
const isVertical = position === "left" || position === "right";
const arrowSize = (_b = (_a = cfg.arrow) == null ? void 0 : _a.size) != null ? _b : 8;
const arrowGutter = arrowSize + 4;
const labelPadAlongStrip = cfg.showLabels ? 28 : 4;
const labelPadAcrossStrip = cfg.showLabels ? 20 : 4;
const minLabelWidth = cfg.showLabels ? 44 : 0;
const stripLength = this._resolveStripLength(isVertical ? cfg.height : cfg.width, isVertical);
const stripThickness = cfg.thickness;
const svgWidth = isVertical ? Math.max(stripThickness + arrowGutter + 4, minLabelWidth) : stripLength + labelPadAlongStrip * 2;
const svgHeight = isVertical ? stripLength + labelPadAcrossStrip * 2 : stripThickness + arrowGutter + 4;
const verticalGroupWidth = stripThickness + arrowGutter;
const verticalGroupLeftPad = (svgWidth - verticalGroupWidth) / 2;
const stripX = isVertical ? position === "left" ? verticalGroupLeftPad : verticalGroupLeftPad + arrowGutter : labelPadAlongStrip;
const stripY = isVertical ? labelPadAcrossStrip : position === "top" ? arrowGutter : 4;
const svg = BrowserAPIs.createElementNS(SVG_NS, "svg");
svg.setAttribute(
"class",
"apexcharts-heatmap-gradient-legend apexcharts-gradient-legend"
);
svg.setAttribute("width", String(svgWidth));
svg.setAttribute("height", String(svgHeight));
svg.setAttribute("overflow", "visible");
const defs = BrowserAPIs.createElementNS(SVG_NS, "defs");
const gradId = `apexcharts-heatmap-gradient-${w2.globals.cuid}`;
const linearGrad = BrowserAPIs.createElementNS(SVG_NS, "linearGradient");
linearGrad.setAttribute("id", gradId);
if (isVertical) {
linearGrad.setAttribute("x1", "0");
linearGrad.setAttribute("y1", "1");
linearGrad.setAttribute("x2", "0");
linearGrad.setAttribute("y2", "0");
} else {
linearGrad.setAttribute("x1", "0");
linearGrad.setAttribute("y1", "0");
linearGrad.setAttribute("x2", "1");
linearGrad.setAttribute("y2", "0");
}
const { min, max, stops, bands } = this._computeStops();
this._min = min;
this._max = max;
stops.forEach((s) => {
const stopEl = BrowserAPIs.createElementNS(SVG_NS, "stop");
stopEl.setAttribute("offset", `${(s.percent * 100).toFixed(2)}%`);
stopEl.setAttribute("stop-color", s.color);
linearGrad.appendChild(stopEl);
});
defs.appendChild(linearGrad);
svg.appendChild(defs);
const rect = BrowserAPIs.createElementNS(SVG_NS, "rect");
rect.setAttribute("x", String(stripX));
rect.setAttribute("y", String(stripY));
rect.setAttribute("width", String(isVertical ? stripThickness : stripLength));
rect.setAttribute("height", String(isVertical ? stripLength : stripThickness));
rect.setAttribute("rx", "2");
rect.setAttribute("fill", `url(#${gradId})`);
svg.appendChild(rect);
if (cfg.showLabels) {
const labelColor = ((_c = cfg.labelStyle) == null ? void 0 : _c.colors) || (Array.isArray(w2.config.legend.labels.colors) ? w2.config.legend.labels.colors[0] : w2.config.legend.labels.colors) || w2.config.chart.foreColor;
const labelFontSize = ((_d = cfg.labelStyle) == null ? void 0 : _d.fontSize) || "11px";
const labelFontFamily = ((_e = cfg.labelStyle) == null ? void 0 : _e.fontFamily) || w2.config.chart.fontFamily;
const fmt = this._getFormatter();
const makeLabel = (text, x, y, anchor) => {
const t = BrowserAPIs.createElementNS(SVG_NS, "text");
t.setAttribute("x", String(x));
t.setAttribute("y", String(y));
t.setAttribute("text-anchor", anchor);
t.setAttribute("dominant-baseline", "middle");
t.setAttribute("fill", labelColor);
t.setAttribute("font-size", labelFontSize);
if (labelFontFamily) t.setAttribute("font-family", labelFontFamily);
t.textContent = String(text);
return t;
};
if (isVertical) {
const midX = stripX + stripThickness / 2;
svg.appendChild(makeLabel(fmt(min), midX, stripY + stripLength + 10, "middle"));
svg.appendChild(makeLabel(fmt(max), midX, stripY - 10, "middle"));
} else {
const midY = stripY + stripThickness / 2;
svg.appendChild(makeLabel(fmt(min), stripX - 6, midY, "end"));
svg.appendChild(makeLabel(fmt(max), stripX + stripLength + 6, midY, "start"));
}
}
const arrowColor = ((_f = cfg.arrow) == null ? void 0 : _f.color) || w2.config.chart.foreColor;
const arrow = this._buildArrow(arrowSize, arrowColor, position);
svg.appendChild(arrow);
this.arrowEl = arrow;
this._bandHitEls = [];
if (w2.config.legend.onItemHover.highlightDataSeries && bands.length > 0) {
bands.forEach((b2) => {
const hit = BrowserAPIs.createElementNS(SVG_NS, "rect");
if (isVertical) {
const yTop = stripY + stripLength - b2.p2 * stripLength;
const yBot = stripY + stripLength - b2.p1 * stripLength;
hit.setAttribute("x", String(stripX));
hit.setAttribute("y", String(yTop));
hit.setAttribute("width", String(stripThickness));
hit.setAttribute("height", String(Math.max(0, yBot - yTop)));
} else {
hit.setAttribute("x", String(stripX + b2.p1 * stripLength));
hit.setAttribute("y", String(stripY));
hit.setAttribute(
"width",
String(Math.max(0, (b2.p2 - b2.p1) * stripLength))
);
hit.setAttribute("height", String(stripThickness));
}
hit.setAttribute("fill", "transparent");
hit.setAttribute("class", "apexcharts-heatmap-gradient-band");
hit.setAttribute("data:range-index", String(b2.index));
hit.style.cursor = "pointer";
svg.appendChild(hit);
this._bandHitEls.push(hit);
});
}
this._geom = {
isVertical,
position,
stripX,
stripY,
stripLength,
stripThickness,
arrowSize,
svgWidth,
svgHeight
};
if (cfg.showHoverValue) {
const tt = BrowserAPIs.createElement("div");
tt.classList.add("apexcharts-heatmap-gradient-legend-value");
tt.style.position = "absolute";
tt.style.fontSize = ((_g = cfg.labelStyle) == null ? void 0 : _g.fontSize) || "11px";
tt.style.fontFamily = ((_h = cfg.labelStyle) == null ? void 0 : _h.fontFamily) || w2.config.chart.fontFamily || "";
tt.style.color = w2.config.chart.foreColor;
tt.style.background = "rgba(0,0,0,0.65)";
tt.style.color = "#fff";
tt.style.padding = "2px 6px";
tt.style.borderRadius = "3px";
tt.style.pointerEvents = "none";
tt.style.whiteSpace = "nowrap";
tt.style.opacity = "0";
tt.style.transition = "opacity 120ms ease";
this.hoverValueEl = tt;
}
elLegendWrap.classList.add("apexcharts-heatmap-gradient-legend-wrap");
elLegendWrap.classList.add(
"apx-legend-position-" + position
);
elLegendWrap.appendChild(svg);
if (this.hoverValueEl) elLegendWrap.appendChild(this.hoverValueEl);
this.svgEl = svg;
if (targetEl) {
elLegendWrap.style.width = svgWidth + "px";
elLegendWrap.style.height = svgHeight + "px";
elLegendWrap.style.position = "relative";
elLegendWrap.style.overflow = "visible";
} else {
this._applyWrapAlignment(elLegendWrap, position, isVertical, svgWidth, svgHeight);
}
this._attachHoverListeners();
this._attachBandHoverListeners();
}
/**
* Resolve a configured length (number = px, string ending in '%' =
* percentage of the chart's SVG width/height) to a pixel length.
* @param {number|string} value
* @param {boolean} isVertical
* @returns {number}
*/
_resolveStripLength(value, isVertical) {
const w2 = this.w;
const basis = isVertical ? w2.globals.svgHeight || w2.config.chart.height || 300 : w2.globals.svgWidth || w2.config.chart.width || 600;
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.endsWith("%")) {
const pct = parseFloat(trimmed) || 0;
return Math.max(20, basis * pct / 100);
}
const n = parseFloat(trimmed);
return Number.isFinite(n) ? n : 200;
}
if (typeof value === "number" && Number.isFinite(value)) return value;
return 200;
}
/**
* Position the legend wrap and align the gradient strip within it. The
* wrap spans the chart's long axis (full width for top/bottom; full
* height for left/right) and uses flexbox to honor the `align` config.
* Bypasses the standard `setLegendWrapXY` which sizes the wrap to its
* content.
* @param {HTMLElement} elLegendWrap
* @param {'top'|'right'|'bottom'|'left'} position
* @param {boolean} isVertical
* @param {number} svgWidth
* @param {number} svgHeight
*/
_applyWrapAlignment(elLegendWrap, position, isVertical, svgWidth, svgHeight) {
const w2 = this.w;
const cfg = this._cfg();
const align = cfg.align || "center";
const edgePad = 12;
const chartWidth = w2.globals.svgWidth || w2.config.chart.width || 600;
const chartHeight = w2.globals.svgHeight || w2.config.chart.height || 300;
const userOffsetX = w2.config.legend.offsetX || 0;
const userOffsetY = w2.config.legend.offsetY || 0;
elLegendWrap.style.position = "absolute";
elLegendWrap.style.display = "block";
elLegendWrap.style.overflow = "visible";
elLegendWrap.style.padding = "0";
elLegendWrap.style.width = svgWidth + "px";
elLegendWrap.style.height = svgHeight + "px";
elLegendWrap.style.right = "auto";
elLegendWrap.style.bottom = "auto";
if (isVertical) {
const availableHeight = chartHeight - svgHeight - edgePad * 2;
let y;
if (align === "start") y = edgePad;
else if (align === "end") y = edgePad + Math.max(0, availableHeight);
else y = edgePad + Math.max(0, availableHeight) / 2;
elLegendWrap.style.top = y + userOffsetY + "px";
if (position === "left") {
elLegendWrap.style.left = edgePad + userOffsetX + "px";
} else {
elLegendWrap.style.left = chartWidth - svgWidth - edgePad + userOffsetX + "px";
}
} else {
const availableWidth = chartWidth - svgWidth - edgePad * 2;
let x;
if (align === "start") x = edgePad;
else if (align === "end") x = edgePad + Math.max(0, availableWidth);
else x = edgePad + Math.max(0, availableWidth) / 2;
elLegendWrap.style.left = x + userOffsetX + "px";
if (position === "top") {
elLegendWrap.style.top = edgePad + userOffsetY + "px";
} else {
elLegendWrap.style.top = chartHeight - svgHeight - edgePad + userOffsetY + "px";
}
}
}
/**
* Re-position the strip once the final layout is known.
*
* `_applyWrapAlignment` (called during `draw()`, before `plotCoords()`) can
* only pin to the chart's outer edge. This runs after layout — when
* `translateX/Y`, `gridWidth/Height` and `xAxisHeight` are populated — and:
* - centers the strip within its reserved band on the perpendicular axis
* (between the title and the plot for `top`; the x-axis and the chart
* bottom for `bottom`; the chart edge and the plot for `left`/`right`),
* so the slack is split evenly instead of dumped on one side, and
* - aligns it along the plot's own extent (so `align: 'center'` centers
* over the heatmap, not the whole canvas).
* Honors `legend.offsetX/offsetY` for user nudging. Safe to call repeatedly.
*/
repositionToPlot() {
var _a, _b;
if (!Environment.isBrowser()) return;
if (this._targetEl) return;
const w2 = this.w;
const g2 = w2.globals;
const wrap = (
/** @type {HTMLElement} */
w2.dom.elLegendWrap
);
if (!wrap || !this._geom) return;
if (!Number.isFinite(g2.gridWidth) || !Number.isFinite(g2.gridHeight)) return;
const { isVertical, position, svgWidth, svgHeight, stripX, stripY, stripThickness } = this._geom;
const align = this._cfg().align || "center";
const ox = w2.config.legend.offsetX || 0;
const oy = w2.config.legend.offsetY || 0;
const dimHelpers = (_b = (_a = this.ctx) == null ? void 0 : _a.dimensions) == null ? void 0 : _b.dimHelpers;
const titleArea = dimHelpers ? dimHelpers.getTitleSubtitleCoords("title").height + dimHelpers.getTitleSubtitleCoords("subtitle").height : 0;
const xAxisArea = w2.layout.xAxisHeight || 0;
const alongOffset = (extent, size) => {
const avail = Math.max(0, extent - size);
if (align === "start") return 0;
if (align === "end") return avail;
return avail / 2;
};
if (isVertical) {
wrap.style.top = g2.translateY + alongOffset(g2.gridHeight, svgHeight) + oy + "px";
const bandStart = position === "left" ? 0 : g2.translateX + g2.gridWidth;
const bandEnd = position === "left" ? g2.translateX : g2.svgWidth;
const stripCenter = (bandStart + bandEnd) / 2;
wrap.style.left = stripCenter - stripX - stripThickness / 2 + ox + "px";
} else {
wrap.style.left = g2.translateX + alongOffset(g2.gridWidth, svgWidth) + ox + "px";
const bandStart = position === "top" ? titleArea : g2.translateY + g2.gridHeight + xAxisArea;
const bandEnd = position === "top" ? g2.translateY : g2.svgHeight;
const stripCenter = (bandStart + bandEnd) / 2;
wrap.style.top = stripCenter - stripY - stripThickness / 2 + oy + "px";
}
BrowserAPIs.requestAnimationFrame(() => this._enforceMinPlotGap());
}
/**
* Guarantee a minimum gap between the strip's chart-facing edge and the plot.
* Measured in viewport space (immune to the wrap↔SVG coordinate offset) and
* applied as a *relative* shift to the wrap's current position, so it only
* nudges a strip that ended up too close — placements with ample room are
* left exactly where centering put them. Runs post-paint (see caller).
*/
_enforceMinPlotGap() {
const w2 = this.w;
const wrap = (
/** @type {HTMLElement} */
w2.dom.elLegendWrap
);
const strip = this.svgEl && this.svgEl.querySelector("rect");
const grid = w2.dom.baseEl.querySelector(".apexcharts-grid");
if (!wrap || !strip || !grid || !this._geom) return;
const s = strip.getBoundingClientRect();
const gr = grid.getBoundingClientRect();
if (!s.width || !s.height || !gr.width || !gr.height) return;
const MIN_GAP = 16;
const { isVertical, position } = this._geom;
if (isVertical) {
const gap = position === "left" ? gr.left - s.right : s.left - gr.right;
if (gap < MIN_GAP) {
const curLeft = parseFloat(wrap.style.left) || 0;
const shift = MIN_GAP - gap;
wrap.style.left = curLeft + (position === "left" ? -shift : shift) + "px";
}
} else {
const gap = position === "top" ? gr.top - s.bottom : s.top - gr.bottom;
if (gap < MIN_GAP) {
const curTop = parseFloat(wrap.style.top) || 0;
const shift = MIN_GAP - gap;
wrap.style.top = curTop + (position === "top" ? -shift : shift) + "px";
}
}
}
/**
* Tear down listeners (called before re-render).
*/
destroy() {
var _a, _b, _c, _d, _e, _f, _g;
for (let i = 0; i < this._bandHitEls.length; i++) {
const el = this._bandHitEls[i];
(_a = el.removeEventListener) == null ? void 0 : _a.call(el, "mousemove", this._onBandEnter);
(_b = el.removeEventListener) == null ? void 0 : _b.call(el, "mouseout", this._onBandLeave);
}
this._bandHitEls = [];
this._activeBandIndex = -1;
if (!((_c = this.ctx) == null ? void 0 : _c.events)) return;
try {
(_e = (_d = this.ctx.events).removeEventListener) == null ? void 0 : _e.call(
_d,
"dataPointMouseEnter",
this._onCellEnter
);
(_g = (_f = this.ctx.events).removeEventListener) == null ? void 0 : _g.call(
_f,
"dataPointMouseLeave",
this._onCellLeave
);
} catch (_) {
}
}
/** Wire mousemove/mouseout on each per-band hit-region (ranges mode). */
_attachBandHoverListeners() {
if (!Environment.isBrowser()) return;
for (let i = 0; i < this._bandHitEls.length; i++) {
const el = this._bandHitEls[i];
el.addEventListener("mousemove", this._onBandEnter);
el.addEventListener("mouseout", this._onBandLeave);
}
}
/**
* Hovering a gradient band highlights its cells and dims the rest. Guarded
* so the repeated mousemove stream only re-applies on an actual band change.
* @param {Event} e
*/
_onBandEnter(e) {
var _a, _b, _c, _d;
const w2 = this.w;
const target = (
/** @type {Element} */
e.currentTarget
);
const idx = parseInt((_a = target.getAttribute("data:range-index")) != null ? _a : "-1", 10);
if (idx < 0 || idx === this._activeBandIndex) return;
this._activeBandIndex = idx;
(_d = (_c = (_b = this.ctx) == null ? void 0 : _b.events) == null ? void 0 : _c.fireEvent) == null ? void 0 : _d.call(_c, "legendHover", [this.ctx, idx, w2]);
new Series(w2).highlightRangeInSeries(idx, "highlight");
}
/** Leaving a band clears the highlight. */
_onBandLeave() {
if (this._activeBandIndex < 0) return;
const idx = this._activeBandIndex;
this._activeBandIndex = -1;
new Series(this.w).highlightRangeInSeries(idx, "reset");
}
_attachHoverListeners() {
var _a, _b;
if (!Environment.isBrowser()) return;
if (!((_b = (_a = this.ctx) == null ? void 0 : _a.events) == null ? void 0 : _b.addEventListener)) return;
this.ctx.events.addEventListener(
"dataPointMouseEnter",
this._onCellEnter
);
this.ctx.events.addEventListener(
"dataPointMouseLeave",
this._onCellLeave
);
}
/**
* dataPointMouseEnter fires as `(e, ctx, { seriesIndex, dataPointIndex, w })`.
* Graphics._fireEvent forwards listener args in the same shape.
* @param {...any} args
*/
_onCellEnter(...args) {
var _a, _b, _c;
const w2 = this.w;
if (!this.arrowEl) return;
const opts = args[args.length - 1];
if (!opts || typeof opts !== "object") return;
const i = opts.seriesIndex;
const j = opts.dataPointIndex;
if (typeof i !== "number" || typeof j !== "number") return;
if (!HeatmapGradientLegend.supports(w2)) return;
let val;
if (this._continuous) {
val = colorValueOf(w2, i, j);
} else {
val = (_c = (_b = (_a = w2.seriesData) == null ? void 0 : _a.series) == null ? void 0 : _b[i]) == null ? void 0 : _c[j];
}
if (val == null || Number.isNaN(val)) return;
this._positionArrow(val);
}
_onCellLeave() {
if (!this.arrowEl) return;
this.arrowEl.setAttribute("opacity", "0");
if (this.hoverValueEl) {
this.hoverValueEl.style.opacity = "0";
}
}
/**
* Move the arrow to the position corresponding to `val` along the strip.
* @param {number} val
*/
_positionArrow(val) {
if (!this.arrowEl || !this._geom) return;
const { isVertical, position, stripX, stripY, stripLength, stripThickness, arrowSize } = this._geom;
const min = this._min;
const max = this._max;
const span = max - min;
let pct;
if (span === 0) {
pct = 0.5;
} else {
pct = (val - min) / span;
}
if (pct < 0) pct = 0;
if (pct > 1) pct = 1;
if (isVertical) {
const yCenter = stripY + stripLength - pct * stripLength;
let tipX, baseX;
if (position === "left") {
tipX = stripX + stripThickness;
baseX = tipX + arrowSize;
} else {
tipX = stripX;
baseX = tipX - arrowSize;
}
const points = [
`${tipX},${yCenter}`,
`${baseX},${yCenter - arrowSize / 2}`,
`${baseX},${yCenter + arrowSize / 2}`
].join(" ");
this.arrowEl.setAttribute("points", points);
} else {
const xCenter = stripX + pct * stripLength;
let tipY, baseY;
if (position === "top") {
tipY = stripY + stripThickness;
baseY = tipY + arrowSize;
} else {
tipY = stripY;
baseY = tipY - arrowSize;
}
const points = [
`${xCenter},${tipY}`,
`${xCenter - arrowSize / 2},${baseY}`,
`${xCenter + arrowSize / 2},${baseY}`
].join(" ");
this.arrowEl.setAttribute("points", points);
}
this.arrowEl.setAttribute("opacity", "1");
if (this.hoverValueEl) {
const fmt = this._getFormatter();
this.hoverValueEl.textContent = fmt(val);
if (isVertical) {
const yCenter = stripY + stripLength - pct * stripLength;
if (position === "left") {
this.hoverValueEl.style.left = `${stripX + stripThickness + arrowSize + 8}px`;
} else {
this.hoverValueEl.style.left = `${stripX - arrowSize - 8}px`;
this.hoverValueEl.style.transform = "translateX(-100%)";
}
this.hoverValueEl.style.top = `${yCenter - 9}px`;
} else {
const xCenter = stripX + pct * stripLength;
this.hoverValueEl.style.left = `${xCenter}px`;
this.hoverValueEl.style.transform = "translateX(-50%)";
if (position === "top") {
this.hoverValueEl.style.top = `${stripY + stripThickness + arrowSize + 8}px`;
} else {
this.hoverValueEl.style.top = `${stripY - arrowSize - 18}px`;
}
}
this.hoverValueEl.style.opacity = "1";
}
}
/**
* @param {number} size
* @param {string} color
* @param {'top'|'right'|'bottom'|'left'} _position
*/
_buildArrow(size, color, _position) {
const polygon = BrowserAPIs.createElementNS(SVG_NS, "polygon");
polygon.setAttribute("fill", color);
polygon.setAttribute("opacity", "0");
polygon.setAttribute("class", "apexcharts-heatmap-gradient-arrow");
polygon.setAttribute("points", "0,0 0,0 0,0");
polygon.setAttribute("pointer-events", "none");
return polygon;
}
/**
* Build gradient stops + return effective min/max.
* - If `colorScale.ranges` is set, stops are placed at each range boundary
* so the gradient reflects the user's discrete palette.
* - Otherwise, samples N stops from the same shadeColor function the cells
* use, so the strip visually matches the heatmap.
* @returns {{ min: number, max: number, stops: Array<{percent:number,color:string}>, bands: Array<{index:number,p1:number,p2:number}> }}
*/
_computeStops() {
var _a, _b;
const w2 = this.w;
const cs = HeatmapGradientLegend.colorScaleOf(w2) || {};
const cfg = this._cfg();
const continuous = buildContinuousScale(w2);
if (continuous) {
this._continuous = true;
return {
min: continuous.min,
max: continuous.max,
stops: continuous.legendStops,
bands: []
};
}
this._continuous = false;
let dataMin = Infinity;
let dataMax = -Infinity;
const rows = ((_a = w2.seriesData) == null ? void 0 : _a.series) || [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (!row) continue;
for (let j = 0; j < row.length; j++) {
const v2 = row[j];
if (v2 == null || Number.isNaN(v2)) continue;
if (v2 < dataMin) dataMin = v2;
if (v2 > dataMax) dataMax = v2;
}
}
if (!Number.isFinite(dataMin)) dataMin = 0;
if (!Number.isFinite(dataMax)) dataMax = 0;
let min = dataMin;
let max = dataMax;
if (typeof cs.min !== "undefined" && typeof cs.max !== "undefined" && cs.max > cs.min) {
min = cs.min;
max = cs.max;
} else {
if (typeof cs.min !== "undefined") {
min = cs.min < dataMin ? cs.min : dataMin;
}
if (typeof cs.max !== "undefined") {
max = cs.max > dataMax ? cs.max : dataMax;
}
}
const stops = [];
const bands = [];
if (cs.ranges && cs.ranges.length > 0) {
const ranges = cs.ranges.map((r, originalIndex) => __spreadProps(__spreadValues({}, r), {
_originalIndex: originalIndex
})).sort((a, b2) => a.from - b2.from);
const lo = ranges[0].from;
const hi = ranges[ranges.length - 1].to;
min = lo;
max = hi;
const span = hi - lo || 1;
ranges.forEach((r) => {
const p1 = (r.from - lo) / span;
const p2 = (r.to - lo) / span;
stops.push({ percent: (p1 + p2) / 2, color: r.color });
bands.push({ index: r._originalIndex, p1, p2 });
});
} else {
const baseColor = w2.globals.colors[0] || "#008FFB";
const utils = new Utils();
const plot = w2.config.plotOptions[w2.config.chart.type] || {};
const shadeIntensity = (_b = plot.shadeIntensity) != null ? _b : 0.5;
const hasNegs = (
/** @type {any} */
w2.globals.hasNegs
);
const n = Math.max(2, cfg.stops || 16);
for (let s = 0; s < n; s++) {
const t = s / (n - 1);
const v2 = min + t * (max - min);
const total = Math.abs(max) + Math.abs(min);
const percent_v = total === 0 ? 0 : 100 * v2 / total;
let colorShadePercent;
if (hasNegs) {
if (plot.reverseNegativeShade) {
colorShadePercent = percent_v < 0 ? percent_v / 100 * (shadeIntensity * 1.25) : (1 - percent_v / 100) * (shadeIntensity * 1.25);
} else {
colorShadePercent = percent_v <= 0 ? 1 - (1 + percent_v / 100) * shadeIntensity : (1 - percent_v / 100) * shadeIntensity;
}
} else {
colorShadePercent = 1 - percent_v / 100;
}
if (colorShadePercent > 1) colorShadePercent = 1;
if (colorShadePercent < -1) colorShadePercent = -1;
const shaded = plot.enableShades ? utils.shadeColor(
w2.config.theme.mode === "dark" ? colorShadePercent * -1 : colorShadePercent,
baseColor
) : baseColor;
stops.push({ percent: t, color: shaded });
}
}
return { min, max, stops, bands };
}
}
class Legend {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
this.updateSeries = (...a) => ctx.updateHelpers._updateSeries(...a);
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$3(this);
}
init() {
const w2 = this.w;
const gl = w2.globals;
const cnf = w2.config;
this.isBarsDistributed = cnf.chart.type === "bar" && cnf.plotOptions.bar.distributed && cnf.series.length === 1;
const showLegendAlways = cnf.legend.showForSingleSeries && this.w.seriesData.series.length === 1 || this.isBarsDistributed || // Heatmap legends are colorScale-driven (discrete ranges or the
// gradient strip), not series-driven, so they must render even for a
// single-row heatmap.
cnf.chart.type === "heatmap" || // Same for a treemap once it has a gradient strip: a nested treemap is
// usually one series, and the strip describes the colour metric rather
// than the series.
HeatmapGradientLegend.isEnabled(w2) || this.w.seriesData.series.length > 1;
this.legendHelpers.appendToForeignObject();
if ((showLegendAlways || !gl.axisCharts) && cnf.legend.show) {
const elLegendWrap = (
/** @type {HTMLElement} */
w2.dom.elLegendWrap
);
while (elLegendWrap.firstChild) {
elLegendWrap.removeChild(elLegendWrap.firstChild);
}
if (this.heatmapGradientLegend) {
this.heatmapGradientLegend.destroy();
this.heatmapGradientLegend = null;
}
if (HeatmapGradientLegend.isEnabled(w2)) {
this.heatmapGradientLegend = new HeatmapGradientLegend(w2, this.ctx);
this.heatmapGradientLegend.draw();
} else {
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 w2 = this.w;
const elMarker = BrowserAPIs.createElement("span");
elMarker.classList.add("apexcharts-legend-marker");
const mShape = w2.config.legend.markers.shape || w2.config.markers.shape;
let shape = mShape;
if (Array.isArray(mShape)) {
shape = mShape[i];
}
const mSize = Array.isArray(w2.config.legend.markers.size) ? parseFloat(w2.config.legend.markers.size[i]) : parseFloat(w2.config.legend.markers.size);
const mOffsetX = Array.isArray(w2.config.legend.markers.offsetX) ? parseFloat(w2.config.legend.markers.offsetX[i]) : parseFloat(w2.config.legend.markers.offsetX);
const mOffsetY = Array.isArray(w2.config.legend.markers.offsetY) ? parseFloat(w2.config.legend.markers.offsetY[i]) : parseFloat(w2.config.legend.markers.offsetY);
const mBorderWidth = Array.isArray(w2.config.legend.markers.strokeWidth) ? parseFloat(w2.config.legend.markers.strokeWidth[i]) : parseFloat(w2.config.legend.markers.strokeWidth);
const mStyle = elMarker.style;
mStyle.height = (mSize + mBorderWidth) * 2 + "px";
mStyle.width = (mSize + mBorderWidth) * 2 + "px";
mStyle.left = mOffsetX + "px";
mStyle.top = mOffsetY + "px";
if (w2.config.legend.markers.customHTML) {
mStyle.background = "transparent";
mStyle.color = fillcolor[i];
if (Array.isArray(w2.config.legend.markers.customHTML)) {
if (w2.config.legend.markers.customHTML[i]) {
elMarker.innerHTML = w2.config.legend.markers.customHTML[i]();
}
} else {
elMarker.innerHTML = w2.config.legend.markers.customHTML();
}
} else {
const markers = new Markers(this.ctx.w, this.ctx);
const markerConfig = markers.getMarkerConfig({
cssClass: `apexcharts-legend-marker apexcharts-marker apexcharts-marker-${shape}`,
seriesIndex: i,
strokeWidth: mBorderWidth,
size: mSize
});
const SVGLib = Environment.isBrowser() ? (
/** @type {any} */
window.SVG
) : (
/** @type {any} */
global.SVG
);
const SVGMarker = SVGLib().addTo(elMarker).size("100%", "100%");
const marker = new Graphics(this.w).drawMarker(0, 0, __spreadProps(__spreadValues({}, markerConfig), {
pointFillColor: Array.isArray(fillcolor) ? fillcolor[i] : markerConfig.pointFillColor,
shape
}));
const shapesEls = w2.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() {
var _a;
const me = this;
const w2 = this.w;
const elLegendWrap = (
/** @type {HTMLElement} */
w2.dom.elLegendWrap
);
const fontFamily = w2.config.legend.fontFamily;
let legendNames = w2.seriesData.seriesNames;
let fillcolor = w2.config.legend.markers.fillColors ? w2.config.legend.markers.fillColors.slice() : w2.globals.colors.slice();
if (w2.config.chart.type === "heatmap") {
const ranges = w2.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 = w2.labelData.labels.slice();
}
if (w2.config.legend.customLegendItems.length) {
legendNames = w2.config.legend.customLegendItems;
}
const legendFormatter = w2.formatters.legendFormatter;
const isLegendInversed = w2.config.legend.inverseOrder;
const legendGroups = [];
if (w2.labelData.seriesGroups.length > 1 && w2.config.legend.clusterGroupedSeries) {
w2.labelData.seriesGroups.forEach((_, gi) => {
legendGroups[gi] = BrowserAPIs.createElement("div");
legendGroups[gi].classList.add(
"apexcharts-legend-group",
`apexcharts-legend-group-${gi}`
);
if (w2.config.legend.clusterGroupedSeriesOrientation === "horizontal") {
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++) {
const text = legendFormatter(legendNames[i], { seriesIndex: i, w: w2 });
let collapsedSeries = false;
let ancillaryCollapsedSeries = false;
if (w2.globals.collapsedSeries.length > 0) {
for (let c = 0; c < w2.globals.collapsedSeries.length; c++) {
if (w2.globals.collapsedSeries[c].index === i) {
collapsedSeries = true;
}
}
}
if (w2.globals.ancillaryCollapsedSeriesIndices.length > 0) {
for (let c = 0; c < w2.globals.ancillaryCollapsedSeriesIndices.length; c++) {
if (w2.globals.ancillaryCollapsedSeriesIndices[c] === i) {
ancillaryCollapsedSeries = true;
}
}
}
const elMarker = this.createLegendMarker({ i, fillcolor });
Graphics.setAttrs(elMarker, {
rel: i + 1,
"data:collapsed": collapsedSeries || ancillaryCollapsedSeries
});
if (collapsedSeries || ancillaryCollapsedSeries) {
elMarker.classList.add("apexcharts-inactive-legend");
}
const elLegend = BrowserAPIs.createElement("div");
if (w2.config.chart.accessibility.enabled && w2.config.chart.accessibility.keyboard.enabled) {
elLegend.setAttribute("role", "button");
elLegend.setAttribute("tabindex", "0");
const seriesName = Array.isArray(text) ? text.join(" ") : text;
const isCollapsed = collapsedSeries || ancillaryCollapsedSeries;
const statusText = isCollapsed ? "hidden" : "visible";
elLegend.setAttribute(
"aria-label",
`${seriesName}, ${statusText}. Press Enter or Space to toggle.`
);
elLegend.setAttribute("aria-pressed", isCollapsed ? "true" : "false");
}
const elLegendText = BrowserAPIs.createElement("span");
elLegendText.classList.add("apexcharts-legend-text");
elLegendText.innerHTML = Array.isArray(text) ? text.join(" ") : text;
let textColor = w2.config.legend.labels.useSeriesColors ? w2.globals.colors[i] : Array.isArray(w2.config.legend.labels.colors) ? (_a = w2.config.legend.labels.colors) == null ? void 0 : _a[i] : w2.config.legend.labels.colors;
if (!textColor) {
textColor = w2.config.chart.foreColor;
}
elLegendText.style.color = textColor;
elLegendText.style.fontSize = w2.config.legend.fontSize;
elLegendText.style.fontWeight = w2.config.legend.fontWeight;
elLegendText.style.fontFamily = fontFamily || w2.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.w);
if (!w2.config.legend.showForZeroSeries) {
const total = coreUtils.getSeriesTotalByIndex(i);
if (total === 0 && coreUtils.seriesHaveSameValues(i) && !coreUtils.isSeriesNull(i) && w2.globals.collapsedSeriesIndices.indexOf(i) === -1 && w2.globals.ancillaryCollapsedSeriesIndices.indexOf(i) === -1) {
elLegend.classList.add("apexcharts-hidden-zero-series");
}
}
if (!w2.config.legend.showForNullSeries) {
if (coreUtils.isSeriesNull(i) && w2.globals.collapsedSeriesIndices.indexOf(i) === -1 && w2.globals.ancillaryCollapsedSeriesIndices.indexOf(i) === -1) {
elLegend.classList.add("apexcharts-hidden-null-series");
}
}
if (legendGroups.length) {
w2.labelData.seriesGroups.forEach((group, gi) => {
var _a2, _b;
if (group.includes(
/** @type {Record<string,any>} */
(_b = (_a2 = w2.config.series[i]) == null ? void 0 : _a2.name) != null ? _b : ""
)) {
elLegendWrap.appendChild(legendGroups[gi]);
legendGroups[gi].appendChild(elLegend);
}
});
} else {
elLegendWrap.appendChild(elLegend);
}
elLegendWrap.classList.add(
`apexcharts-align-${w2.config.legend.horizontalAlign}`
);
elLegendWrap.classList.add(
"apx-legend-position-" + w2.config.legend.position
);
elLegend.classList.add("apexcharts-legend-series");
elLegend.style.margin = `${w2.config.legend.itemMargin.vertical}px ${w2.config.legend.itemMargin.horizontal}px`;
elLegendWrap.style.width = w2.config.legend.width ? w2.config.legend.width + "px" : "";
elLegendWrap.style.height = w2.config.legend.height ? w2.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 (!w2.config.legend.onItemClick.toggleDataSeries) {
elLegend.classList.add("apexcharts-no-click");
}
}
w2.dom.elWrap.addEventListener("click", me.onLegendClick, true);
if (w2.config.legend.onItemHover.highlightDataSeries && w2.config.legend.customLegendItems.length === 0) {
w2.dom.elWrap.addEventListener("mousemove", me.onLegendHovered, true);
w2.dom.elWrap.addEventListener("mouseout", me.onLegendHovered, true);
}
if (w2.config.chart.accessibility.enabled && w2.config.chart.accessibility.keyboard.enabled) {
w2.dom.elWrap.addEventListener(
"keydown",
me.onLegendKeyDown.bind(me),
true
);
}
}
/**
* @param {number} offsetX
* @param {number} offsetY
*/
setLegendWrapXY(offsetX, offsetY) {
const w2 = this.w;
const elLegendWrap = (
/** @type {HTMLElement} */
w2.dom.elLegendWrap
);
const legendHeight = elLegendWrap.clientHeight;
let x = 0;
let y = 0;
if (w2.config.legend.position === "bottom") {
y = w2.globals.svgHeight - Math.min(legendHeight, w2.globals.svgHeight / 2) - 5;
} else if (w2.config.legend.position === "top") {
const dim = new Dimensions(this.w, 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 + w2.config.legend.offsetX;
y = y + offsetY + w2.config.legend.offsetY;
elLegendWrap.style.left = x + "px";
elLegendWrap.style.top = y + "px";
if (w2.config.legend.position === "right") {
elLegendWrap.style.left = "auto";
elLegendWrap.style.right = 25 + w2.config.legend.offsetX + "px";
}
const fixedHeigthWidth = (
/** @type {const} */
["width", "height"]
);
fixedHeigthWidth.forEach((hw) => {
if (elLegendWrap && elLegendWrap.style[hw]) {
elLegendWrap.style[hw] = parseInt(String(w2.config.legend[hw]), 10) + "px";
}
});
}
legendAlignHorizontal() {
const w2 = this.w;
const elLegendWrap = (
/** @type {HTMLElement} */
w2.dom.elLegendWrap
);
elLegendWrap.style.right = "0";
const dimensions = new Dimensions(this.w, this.ctx);
const titleRect = dimensions.dimHelpers.getTitleSubtitleCoords("title");
const subtitleRect = dimensions.dimHelpers.getTitleSubtitleCoords("subtitle");
const offsetX = 20;
let offsetY = 0;
if (w2.config.legend.position === "top") {
offsetY = titleRect.height + subtitleRect.height + w2.config.title.margin + w2.config.subtitle.margin - 10;
}
this.setLegendWrapXY(offsetX, offsetY);
}
legendAlignVertical() {
const w2 = this.w;
const lRect = this.legendHelpers.getLegendDimensions();
const offsetY = 20;
let offsetX = 0;
if (w2.config.legend.position === "left") {
offsetX = 20;
}
if (w2.config.legend.position === "right") {
offsetX = w2.globals.svgWidth - lRect.clww - 10;
}
this.setLegendWrapXY(offsetX, offsetY);
}
/**
* @param {MouseEvent} e
*/
onLegendHovered(e) {
var _a;
const w2 = this.w;
const target = (
/** @type {Element} */
e.target
);
const hoverOverLegend = target.classList.contains("apexcharts-legend-series") || target.classList.contains("apexcharts-legend-text") || target.classList.contains("apexcharts-legend-marker");
if (w2.config.chart.type !== "heatmap" && !this.isBarsDistributed) {
if (!target.classList.contains("apexcharts-inactive-legend") && hoverOverLegend) {
const series = new Series(this.ctx.w);
series.toggleSeriesOnHover(e, target);
}
} else {
if (hoverOverLegend) {
const seriesCnt = parseInt((_a = target.getAttribute("rel")) != null ? _a : "0", 10) - 1;
this.ctx.events.fireEvent("legendHover", [this.ctx, seriesCnt, this.w]);
const series = new Series(this.ctx.w);
if (e.type === "mousemove") {
series.highlightRangeInSeries(seriesCnt, "highlight");
} else if (e.type === "mouseout") {
series.highlightRangeInSeries(seriesCnt, "reset");
}
}
}
}
/**
* @param {KeyboardEvent} e
*/
onLegendKeyDown(e) {
const me = this;
const w2 = this.w;
const target = (
/** @type {Element} */
e.target
);
const isLegendItem = target.classList.contains("apexcharts-legend-series") || target.classList.contains("apexcharts-legend-text") || target.classList.contains("apexcharts-legend-marker");
if (!isLegendItem) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const rel = target.getAttribute("rel");
me.onLegendClick(e);
if (rel !== null && w2.config.legend.onItemClick.toggleDataSeries) {
requestAnimationFrame(() => {
const restored = w2.dom.baseEl.querySelector(
`.apexcharts-legend-series[rel="${rel}"]`
);
if (restored) restored.focus();
});
}
}
}
/**
* @param {Event} e
*/
onLegendClick(e) {
var _a;
const w2 = this.w;
const target = (
/** @type {Element} */
e.target
);
if (w2.config.legend.customLegendItems.length) return;
if (target.classList.contains("apexcharts-legend-series") || target.classList.contains("apexcharts-legend-text") || target.classList.contains("apexcharts-legend-marker")) {
const seriesCnt = parseInt((_a = target.getAttribute("rel")) != null ? _a : "0", 10) - 1;
const isHidden = 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" && target.classList.contains("apexcharts-legend-marker")) {
markerClick(this.ctx, seriesCnt, this.w);
this.ctx.events.fireEvent("legendMarkerClick", [
this.ctx,
seriesCnt,
this.w
]);
}
const clickAllowed = w2.config.chart.type !== "treemap" && w2.config.chart.type !== "heatmap" && !this.isBarsDistributed;
if (clickAllowed && w2.config.legend.onItemClick.toggleDataSeries) {
this.legendHelpers.toggleDataSeries(seriesCnt, isHidden);
}
}
}
}
_core__default.registerFeatures({ legend: Legend });
const icoPan = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M5 9 2 12l3 3"/>\n <path d="M9 5l3-3 3 3"/>\n <path d="M15 19l-3 3-3-3"/>\n <path d="M19 9l3 3-3 3"/>\n <path d="M2 12h20"/>\n <path d="M12 2v20"/>\n</svg>\n';
const icoZoom = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <circle cx="11" cy="11" r="7"/>\n <path d="m21 21-4.3-4.3M8 11h6M11 8v6"/>\n</svg>\n';
const icoReset = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>\n <path d="M3 3v5h5"/>\n</svg>\n';
const icoZoomIn = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M12 5v14M5 12h14"/>\n</svg>\n';
const icoZoomOut = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M5 12h14"/>\n</svg>\n';
const icoSelect = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M5 3a2 2 0 0 0-2 2"/>\n <path d="M19 3a2 2 0 0 1 2 2"/>\n <path d="M21 19a2 2 0 0 1-2 2"/>\n <path d="M5 21a2 2 0 0 1-2-2"/>\n <path d="M9 3h1M14 3h1M9 21h1M14 21h1M3 9v1M3 14v1M21 9v1M21 14v1"/>\n</svg>\n';
const icoMeasure = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.4 2.4 0 0 1 0-3.4l2.6-2.6a2.4 2.4 0 0 1 3.4 0Z"/>\n <path d="m14.5 12.5 2-2"/>\n <path d="m11.5 9.5 2-2"/>\n <path d="m8.5 6.5 2-2"/>\n <path d="m17.5 15.5 2-2"/>\n</svg>\n';
const icoMenu = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n <path d="M4 6h16M4 12h16M4 18h16"/>\n</svg>\n';
class Toolbar {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
this.ev = this.w.config.chart.events;
this.selectedClass = "apexcharts-selected";
this.localeValues = this.w.globals.locale.toolbar;
this.minX = w2.globals.minX;
this.maxX = w2.globals.maxX;
this.elZoom = null;
this.elZoomIn = null;
this.elZoomOut = null;
this.elPan = null;
this.elSelection = null;
this.elMeasure = null;
this.elZoomReset = null;
this.elMenuIcon = null;
this.elMenu = null;
this.elMenuItems = [];
this.t = null;
}
createToolbar() {
var _a, _b;
const w2 = this.w;
const createDiv = () => {
return BrowserAPIs.createElementNS("http://www.w3.org/1999/xhtml", "div");
};
const createBtn = () => {
const btn = (
/** @type {HTMLButtonElement} */
BrowserAPIs.createElementNS("http://www.w3.org/1999/xhtml", "button")
);
btn.setAttribute("type", "button");
return btn;
};
const elToolbarWrap = createDiv();
elToolbarWrap.setAttribute("class", "apexcharts-toolbar");
elToolbarWrap.style.top = w2.config.chart.toolbar.offsetY + "px";
elToolbarWrap.style.right = -w2.config.chart.toolbar.offsetX + 3 + "px";
w2.dom.elWrap.appendChild(elToolbarWrap);
this.elZoom = createBtn();
this.elZoomIn = createBtn();
this.elZoomOut = createBtn();
this.elPan = createBtn();
this.elSelection = createBtn();
this.elMeasure = createBtn();
this.elZoomReset = createBtn();
this.elMenuIcon = createBtn();
this.elMenu = createDiv();
this.elCustomIcons = [];
this.t = w2.config.chart.toolbar.tools;
if (Array.isArray(this.t.customIcons)) {
for (let i = 0; i < this.t.customIcons.length; i++) {
this.elCustomIcons.push(createBtn());
}
}
const toolbarControls = [];
const appendZoomControl = (type, el, ico) => {
const tool = type.toLowerCase();
if (this.t[tool] && w2.config.chart.zoom.enabled) {
toolbarControls.push({
el,
icon: typeof this.t[tool] === "string" ? this.t[tool] : ico,
title: (
/** @type {any} */
this.localeValues[type]
),
class: `apexcharts-${tool}-icon`
});
}
};
appendZoomControl("zoomIn", this.elZoomIn, icoZoomIn);
appendZoomControl("zoomOut", this.elZoomOut, icoZoomOut);
const zoomSelectionCtrls = (z) => {
if (this.t[z] && w2.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: (
/** @type {any} */
this.localeValues[z === "zoom" ? "selectionZoom" : "selection"]
),
class: `apexcharts-${z}-icon`
});
}
};
zoomSelectionCtrls("zoom");
zoomSelectionCtrls("selection");
if (this.t.pan && w2.config.chart.zoom.enabled) {
toolbarControls.push({
el: this.elPan,
icon: typeof this.t.pan === "string" ? this.t.pan : icoPan,
title: this.localeValues.pan,
class: "apexcharts-pan-icon"
});
}
if (this.t.measure && w2.config.chart.measure && w2.config.chart.measure.enabled) {
toolbarControls.push({
el: this.elMeasure,
icon: typeof this.t.measure === "string" ? this.t.measure : icoMeasure,
title: (
/** @type {any} */
this.localeValues.measure || "Measure"
),
class: "apexcharts-measure-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,
"aria-label": toolbarControls[i].title
});
toolbarControls[i].el.innerHTML = toolbarControls[i].icon;
elToolbarWrap.appendChild(toolbarControls[i].el);
}
if (this.elZoom.parentNode) {
this.elZoom.setAttribute("aria-pressed", String(!!w2.interact.zoomEnabled));
}
if (this.elSelection.parentNode) {
this.elSelection.setAttribute(
"aria-pressed",
String(!!w2.interact.selectionEnabled)
);
}
if (this.elPan.parentNode) {
this.elPan.setAttribute("aria-pressed", String(!!w2.interact.panEnabled));
}
if (this.elMeasure.parentNode) {
this.elMeasure.setAttribute(
"aria-pressed",
String(!!w2.interact.measureEnabled)
);
}
if (this.elMenuIcon.parentNode) {
this.elMenuIcon.setAttribute("aria-haspopup", "true");
this.elMenuIcon.setAttribute("aria-expanded", "false");
}
this._createHamburgerMenu(elToolbarWrap);
if (w2.interact.zoomEnabled) {
this.elZoom.classList.add(this.selectedClass);
} else if (w2.interact.panEnabled) {
this.elPan.classList.add(this.selectedClass);
} else if (w2.interact.selectionEnabled) {
this.elSelection.classList.add(this.selectedClass);
} else if (w2.interact.measureEnabled && this.elMeasure) {
this.elMeasure.classList.add(this.selectedClass);
(_b = (_a = this.ctx.measure) == null ? void 0 : _a.startMeasure) == null ? void 0 : _b.call(_a);
}
this.addToolbarEventListeners();
}
/**
* @param {Element} parent
*/
_createHamburgerMenu(parent) {
this.elMenuItems = [];
parent.appendChild(
/** @type {Node} */
this.elMenu
);
Graphics.setAttrs(this.elMenu, {
class: "apexcharts-menu",
role: "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(
BrowserAPIs.createElementNS("http://www.w3.org/1999/xhtml", "div")
);
this.elMenuItems[i].innerHTML = menuItems[i].title;
Graphics.setAttrs(this.elMenuItems[i], {
class: `apexcharts-menu-item ${menuItems[i].name}`,
title: menuItems[i].title,
role: "menuitem",
tabindex: "-1"
});
this.elMenu.appendChild(this.elMenuItems[i]);
}
}
addToolbarEventListeners() {
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
(_a = this.elZoomReset) == null ? void 0 : _a.addEventListener("click", this.handleZoomReset.bind(this));
(_b = this.elSelection) == null ? void 0 : _b.addEventListener(
"click",
this.toggleZoomSelection.bind(this, "selection")
);
(_c = this.elZoom) == null ? void 0 : _c.addEventListener(
"click",
this.toggleZoomSelection.bind(this, "zoom")
);
(_d = this.elZoomIn) == null ? void 0 : _d.addEventListener("click", this.handleZoomIn.bind(this));
(_e = this.elZoomOut) == null ? void 0 : _e.addEventListener("click", this.handleZoomOut.bind(this));
(_f = this.elPan) == null ? void 0 : _f.addEventListener("click", this.togglePanning.bind(this));
(_g = this.elMeasure) == null ? void 0 : _g.addEventListener("click", this.toggleMeasure.bind(this));
(_h = this.elMenuIcon) == null ? void 0 : _h.addEventListener("click", this.toggleMenu.bind(this));
this.elMenuItems.forEach((m2) => {
if (m2.classList.contains("exportSVG")) {
m2.addEventListener("click", this.handleDownload.bind(this, "svg"));
} else if (m2.classList.contains("exportPNG")) {
m2.addEventListener("click", this.handleDownload.bind(this, "png"));
} else if (m2.classList.contains("exportCSV")) {
m2.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)
);
}
const toolbarButtons = [
this.elZoomReset,
this.elSelection,
this.elZoom,
this.elZoomIn,
this.elZoomOut,
this.elPan,
this.elMeasure,
this.elMenuIcon,
...this.elCustomIcons
];
toolbarButtons.forEach((btn) => {
btn.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const btnClass = btn.className;
btn.click();
requestAnimationFrame(() => {
const baseEl = this.w.dom.baseEl;
if (!baseEl) return;
const apexClass = btnClass.split(" ").find((c) => c.startsWith("apexcharts-"));
if (!apexClass) return;
const restored = baseEl.querySelector(`.${apexClass}`);
if (restored) restored.focus();
});
}
});
});
(_i = this.elMenuIcon) == null ? void 0 : _i.addEventListener(
"keydown",
(e) => {
var _a2;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (!((_a2 = this.elMenu) == null ? void 0 : _a2.classList.contains("apexcharts-menu-open"))) {
this.toggleMenu();
}
window.setTimeout(() => {
const idx = e.key === "ArrowDown" ? 0 : this.elMenuItems.length - 1;
if (this.elMenuItems[idx])
this.elMenuItems[idx].focus();
}, 20);
}
}
);
this.elMenuItems.forEach((m2, idx) => {
m2.addEventListener("keydown", (e) => {
var _a2;
if (e.key === "ArrowDown") {
e.preventDefault();
const next = this.elMenuItems[idx + 1] || this.elMenuItems[0];
next.focus();
} else if (e.key === "ArrowUp") {
e.preventDefault();
const prev = this.elMenuItems[idx - 1] || this.elMenuItems[this.elMenuItems.length - 1];
prev.focus();
} else if (e.key === "Escape" || e.key === "Tab") {
this._closeMenu();
(_a2 = this.elMenuIcon) == null ? void 0 : _a2.focus();
if (e.key === "Tab") ;
else {
e.preventDefault();
}
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
m2.click();
}
});
});
}
/**
* @param {string} type
*/
toggleZoomSelection(type) {
const charts = this.ctx.getSyncedCharts();
charts.forEach((ch) => {
const tb = ch.ctx.toolbar;
const enabledType = type === "selection" ? "selectionEnabled" : "zoomEnabled";
const wasEnabled = !!ch.w.globals[enabledType];
tb.toggleOtherControls();
const el = type === "selection" ? tb.elSelection : tb.elZoom;
if (!wasEnabled) {
ch.w.globals[enabledType] = true;
el.classList.add(tb.selectedClass);
}
el.setAttribute("aria-pressed", String(!!ch.w.globals[enabledType]));
});
}
/**
* Toggle the measure ruler tool. Mutually exclusive with zoom/pan/selection
* (toggleOtherControls deselects those and disarms any active measure), so a
* fresh enable arms the ruler via the Measure module's sticky mode.
*/
toggleMeasure() {
var _a, _b, _c, _d;
const w2 = this.w;
const enabling = !w2.interact.measureEnabled;
this.toggleOtherControls();
if (enabling) {
w2.interact.measureEnabled = true;
(_a = this.elMeasure) == null ? void 0 : _a.classList.add(this.selectedClass);
(_c = (_b = this.ctx.measure) == null ? void 0 : _b.startMeasure) == null ? void 0 : _c.call(_b);
}
(_d = this.elMeasure) == null ? void 0 : _d.setAttribute(
"aria-pressed",
String(w2.interact.measureEnabled)
);
}
getToolbarIconsReference() {
const w2 = this.w;
if (!this.elZoom) {
this.elZoom = w2.dom.baseEl.querySelector(".apexcharts-zoom-icon");
}
if (!this.elPan) {
this.elPan = w2.dom.baseEl.querySelector(".apexcharts-pan-icon");
}
if (!this.elSelection) {
this.elSelection = w2.dom.baseEl.querySelector(
".apexcharts-selection-icon"
);
}
if (!this.elMeasure) {
this.elMeasure = w2.dom.baseEl.querySelector(".apexcharts-measure-icon");
}
}
/**
* @param {string} type
*/
enableZoomPanFromToolbar(type) {
this.toggleOtherControls();
type === "pan" ? this.w.interact.panEnabled = true : this.w.interact.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) => {
const tb = ch.ctx.toolbar;
const wasEnabled = !!ch.w.interact.panEnabled;
tb.toggleOtherControls();
if (!wasEnabled) {
ch.w.interact.panEnabled = true;
tb.elPan.classList.add(tb.selectedClass);
}
tb.elPan.setAttribute("aria-pressed", String(!!ch.w.interact.panEnabled));
});
}
toggleOtherControls() {
var _a, _b, _c;
const w2 = this.w;
w2.interact.panEnabled = false;
w2.interact.zoomEnabled = false;
w2.interact.selectionEnabled = false;
if (w2.interact.measureEnabled) {
w2.interact.measureEnabled = false;
(_b = (_a = this.ctx.measure) == null ? void 0 : _a.stopMeasure) == null ? void 0 : _b.call(_a);
(_c = this.elMeasure) == null ? void 0 : _c.setAttribute("aria-pressed", "false");
}
this.getToolbarIconsReference();
const toggleEls = [this.elPan, this.elSelection, this.elZoom, this.elMeasure];
toggleEls.forEach((el) => {
if (el) {
el.classList.remove(this.selectedClass);
}
});
}
/**
* Read the current x-range from globals at click time.
* Toolbar instance is kept alive across updates (Phase 8 lazy
* instantiation), so cached this.minX/maxX go stale after a zoom.
* @returns {{minX: number, maxX: number}}
*/
_currentXRange() {
const w2 = this.w;
if (w2.axisFlags.isRangeBar) {
return { minX: w2.globals.minY, maxX: w2.globals.maxY };
}
return { minX: w2.globals.minX, maxX: w2.globals.maxX };
}
handleZoomIn() {
const w2 = this.w;
const { minX, maxX } = this._currentXRange();
this.minX = minX;
this.maxX = maxX;
const centerX = (minX + maxX) / 2;
const newMinX = (minX + centerX) / 2;
const newMaxX = (maxX + centerX) / 2;
const newMinXMaxX = this._getNewMinXMaxX(newMinX, newMaxX);
if (!w2.interact.disableZoomIn) {
this.zoomUpdateOptions(newMinXMaxX.minX, newMinXMaxX.maxX);
}
}
handleZoomOut() {
const w2 = this.w;
const { minX, maxX } = this._currentXRange();
this.minX = minX;
this.maxX = maxX;
if (w2.config.xaxis.type === "datetime" && new Date(minX).getUTCFullYear() < 1e3) {
return;
}
const centerX = (minX + maxX) / 2;
const newMinX = minX - (centerX - minX);
const newMaxX = maxX - (centerX - maxX);
const newMinXMaxX = this._getNewMinXMaxX(newMinX, newMaxX);
if (!w2.interact.disableZoomOut) {
this.zoomUpdateOptions(newMinXMaxX.minX, newMinXMaxX.maxX);
}
}
/**
* @param {number} newMinX
* @param {number} newMaxX
*/
_getNewMinXMaxX(newMinX, newMaxX) {
const shouldFloor = this.w.config.xaxis.convertedCatToNumeric;
return {
minX: shouldFloor ? Math.floor(newMinX) : newMinX,
maxX: shouldFloor ? Math.floor(newMaxX) : newMaxX
};
}
/**
* @param {number} newMinX
* @param {number} newMaxX
*/
zoomUpdateOptions(newMinX, newMaxX) {
const w2 = this.w;
if (newMinX === void 0 && newMaxX === void 0) {
this.handleZoomReset();
return;
}
if (w2.config.xaxis.convertedCatToNumeric) {
if (newMinX < 1) {
newMinX = 1;
newMaxX = w2.globals.dataPoints;
}
if (newMaxX - newMinX < 2) {
return;
}
}
let xaxis = {
min: newMinX,
max: newMaxX
};
const beforeZoomRange = this.getBeforeZoomRange(
xaxis,
/** @type {any} */
void 0
);
if (beforeZoomRange) {
xaxis = beforeZoomRange.xaxis;
}
const options = {
xaxis
};
if (!w2.globals.initialConfig) return;
const yaxis = Utils.clone(w2.globals.initialConfig.yaxis);
if (!w2.config.chart.group) {
options.yaxis = yaxis;
}
this.w.interact.zoomed = true;
this.ctx.updateHelpers._updateOptions(
options,
false,
this.w.config.chart.animations.dynamicAnimation.enabled
);
this.zoomCallback(xaxis, yaxis);
}
/**
* @param {Record<string, any>} xaxis
* @param {Record<string, any>} yaxis
*/
zoomCallback(xaxis, yaxis) {
if (typeof this.ev.zoomed === "function") {
this.ev.zoomed(this.ctx, { xaxis, yaxis });
this.ctx.events.fireEvent("zoomed", { xaxis, yaxis });
}
}
/**
* @param {Record<string, any>} xaxis
* @param {Record<string, any>} 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(() => {
var _a, _b, _c;
if ((_a = this.elMenu) == null ? void 0 : _a.classList.contains("apexcharts-menu-open")) {
this._closeMenu();
} else {
(_b = this.elMenu) == null ? void 0 : _b.classList.add("apexcharts-menu-open");
(_c = this.elMenuIcon) == null ? void 0 : _c.setAttribute("aria-expanded", "true");
}
}, 0);
}
_closeMenu() {
var _a, _b;
(_a = this.elMenu) == null ? void 0 : _a.classList.remove("apexcharts-menu-open");
(_b = this.elMenuIcon) == null ? void 0 : _b.setAttribute("aria-expanded", "false");
}
/**
* @param {string} type
*/
handleDownload(type) {
const w2 = this.w;
const exprt = new Exports(this.w, this.ctx);
switch (type) {
case "svg":
exprt.exportToSVG();
break;
case "png":
exprt.exportToPng();
break;
case "csv":
exprt.exportToCSV({
series: w2.config.series,
columnDelimiter: w2.config.chart.toolbar.export.csv.columnDelimiter
});
break;
}
}
handleZoomReset() {
const charts = this.ctx.getSyncedCharts();
charts.forEach((ch) => {
const w2 = ch.w;
if (!w2.interact.zoomed) return;
w2.globals.lastXAxis.min = w2.globals.initialConfig.xaxis.min;
w2.globals.lastXAxis.max = w2.globals.initialConfig.xaxis.max;
ch.updateHelpers.revertDefaultAxisMinMax();
if (typeof w2.config.chart.events.beforeResetZoom === "function") {
const resetZoomRange = w2.config.chart.events.beforeResetZoom(ch, w2);
if (resetZoomRange) {
ch.updateHelpers.revertDefaultAxisMinMax(resetZoomRange);
}
}
if (typeof w2.config.chart.events.zoomed === "function") {
ch.ctx.toolbar.zoomCallback({
min: w2.config.xaxis.min,
max: w2.config.xaxis.max
});
}
const series = ch.ctx.series.emptyCollapsedSeries(
Utils.clone(w2.globals.initialSeries)
);
ch.updateHelpers._updateSeries(
series,
w2.config.chart.animations.dynamicAnimation.enabled
);
w2.interact.zoomed = false;
});
}
destroy() {
this.elZoom = null;
this.elZoomIn = null;
this.elZoomOut = null;
this.elPan = null;
this.elSelection = null;
this.elMeasure = null;
this.elZoomReset = null;
this.elMenuIcon = null;
}
}
class AxisMapping {
/**
* Pixels per data-unit on the x-axis. Derived from `minX..maxX` so it is the
* exact inverse used by both {@link dataXToPx} and {@link pxToDataX}.
* @param {import('../types/internal').ChartStateW} w
* @returns {number}
*/
static xRatio(w2) {
const gw = w2.layout.gridWidth || 1;
return (w2.globals.maxX - w2.globals.minX) / gw;
}
/**
* Data-x -> pixels from the plot origin (usable as an SVG `x` attribute).
* @param {import('../types/internal').ChartStateW} w
* @param {number} dataX
* @returns {number}
*/
static dataXToPx(w2, dataX) {
return (dataX - w2.globals.minX) / AxisMapping.xRatio(w2);
}
/**
* Pixels from the plot origin -> data-x. Feed it `screenX - svgLeft - translateX`.
* @param {import('../types/internal').ChartStateW} w
* @param {number} px
* @returns {number}
*/
static pxToDataX(w2, px) {
return w2.globals.minX + px * AxisMapping.xRatio(w2);
}
/**
* Client (screen) x -> pixels from the plot origin. The origin is the svg
* element's left edge plus `translateX`, never the `.apexcharts-grid` box
* (fact 2 above), so the result does not depend on what the grid happens to
* render. `svgWidth` is the unscaled width the svg was drawn at, so the ratio
* against the measured one is the CSS zoom of any container the chart sits in.
* @param {import('../types/internal').ChartStateW} w
* @param {number} screenX
* @returns {number}
*/
static screenXToPlotPx(w2, screenX) {
const baseEl = w2.dom.baseEl;
const svg = baseEl && baseEl.querySelector(".apexcharts-svg");
if (!svg) return screenX - w2.layout.translateX;
const svgRect = svg.getBoundingClientRect();
const zoom = w2.globals.svgWidth ? svgRect.width / w2.globals.svgWidth : 1;
return (screenX - svgRect.left) / (zoom || 1) - w2.layout.translateX;
}
}
const WHEEL_ZOOM_PIXELS_PER_2X = 240;
const INERTIA_MIN_RELEASE_VELOCITY = 0.05;
const INERTIA_DEFAULT_FRICTION = 0.92;
const INERTIA_STOP_VELOCITY = 0.02;
const FRAME_MS_60FPS = 16.6667;
const PAN_NUDGE_DIVISOR = 15;
class ZoomPanSelection extends Toolbar {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
super(w2, ctx);
this.w = w2;
this.ctx = ctx;
this.dragged = false;
this.graphics = new Graphics(this.w);
this.eventList = [
"mousedown",
"mouseleave",
"mousemove",
"touchstart",
"touchmove",
"mouseup",
"touchend"
];
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";
}
/** @param {{xyRatios: any}} opts */
init({ xyRatios }) {
const w2 = this.w;
const me = this;
this.xyRatios = xyRatios;
this.zoomRect = this.graphics.drawRect(0, 0, 0, 0);
this.selectionRect = this.graphics.drawRect(0, 0, 0, 0);
this.constraints = new Box$1(0, 0, w2.layout.gridWidth, w2.layout.gridHeight);
this.zoomRect.node.classList.add("apexcharts-zoom-rect");
this.selectionRect.node.classList.add("apexcharts-selection-rect");
w2.dom.Paper.add(this.zoomRect);
w2.dom.Paper.add(this.selectionRect);
if (w2.config.chart.selection.type === "x") {
this.slDraggableRect = this.selectionRect.draggable({
minX: 0,
minY: 0,
maxX: w2.layout.gridWidth,
maxY: w2.layout.gridHeight
}).on("dragmove.namespace", this.selectionDragging.bind(this, "dragging"));
} else if (w2.config.chart.selection.type === "y") {
this.slDraggableRect = this.selectionRect.draggable({
minX: 0,
maxX: w2.layout.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 = /** @type {Element} */
w2.dom.baseEl.querySelector(`${w2.globals.chartClass} .apexcharts-svg`);
if (!this.hoverArea) return;
this.hoverArea.classList.add("apexcharts-zoomable");
this.eventList.forEach((event) => {
var _a;
(_a = this.hoverArea) == null ? void 0 : _a.addEventListener(
event,
me.svgMouseEvents.bind(me, xyRatios),
{
capture: false,
passive: true
}
);
});
if (this._wheelZoomEnabled()) {
this.hoverArea.addEventListener("wheel", me.mouseWheelEvent.bind(me), {
capture: false,
passive: false
});
}
if (this._momentumEnabled()) {
["touchstart", "touchmove", "touchend", "touchcancel"].forEach(
(event) => {
var _a;
(_a = this.hoverArea) == null ? void 0 : _a.addEventListener(event, me.momentumTouch.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;
}
/**
* @param {import('../types/internal').XYRatios} xyRatios
* @param {any} e
*/
svgMouseEvents(xyRatios, e) {
const w2 = this.w;
const toolbar = this.ctx.toolbar;
if (w2.interact.momentum && w2.interact.momentum.busy) return;
if (this._momentumEnabled() && e.touches && e.touches.length > 1) {
return;
}
const zoomtype = w2.interact.zoomEnabled ? w2.config.chart.zoom.type : w2.config.chart.selection.type;
const autoSelected = w2.config.chart.toolbar.autoSelected;
if (autoSelected !== "measure") {
if (e.shiftKey) {
w2.interact.shiftWasPressed = true;
toolbar.enableZoomPanFromToolbar(autoSelected === "pan" ? "zoom" : "pan");
} else {
if (w2.interact.shiftWasPressed) {
toolbar.enableZoomPanFromToolbar(autoSelected);
w2.interact.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") {
const gridRectDim = this._gridRect();
if (!gridRectDim) return;
this.startX = this._screenXToPlotPx(this.clientX);
this.startY = this.clientY - gridRectDim.top;
this.dragged = false;
this.w.interact.mousedown = true;
}
if (e.type === "mousemove" && e.which === 1 || e.type === "touchmove") {
this.dragged = true;
if (w2.interact.panEnabled) {
w2.interact.selection = null;
if (this.w.interact.mousedown) {
this.panDragging({
context: this,
zoomtype,
xyRatios: this.xyRatios
});
}
} else {
if (this.w.interact.mousedown && w2.interact.zoomEnabled || this.w.interact.mousedown && w2.interact.selectionEnabled) {
this.selection = this.selectionDrawing({
context: this,
zoomtype
});
}
}
}
if (e.type === "mouseup" || e.type === "touchend" || e.type === "mouseleave") {
this.handleMouseUp({ zoomtype });
}
this.makeSelectionRectDraggable();
}
/** @param {{ zoomtype?: any, isResized?: any }} opts */
handleMouseUp({ zoomtype, isResized }) {
const w2 = this.w;
const gridRectDim = this._gridRect();
if (gridRectDim && (this.w.interact.mousedown || isResized)) {
this.endX = this._screenXToPlotPx(this.clientX);
this.endY = this.clientY - gridRectDim.top;
this.dragX = Math.abs(this.endX - this.startX);
this.dragY = Math.abs(this.endY - this.startY);
if (w2.interact.zoomEnabled || w2.interact.selectionEnabled) {
this.selectionDrawn({
context: this,
zoomtype
});
}
}
if (w2.interact.zoomEnabled) {
this.hideSelectionRect(this.selectionRect);
}
this.dragged = false;
this.w.interact.mousedown = false;
}
// ---------------------------------------------------------------------------
// Wheel zoom: continuous, cursor-anchored zoom on mouse wheel / trackpad.
//
// Each wheel event multiplies a pending zoom factor scaled to its deltaY (so
// a trackpad's stream of tiny deltas and a discrete wheel's ±100 notches both
// feel proportional), and the accumulated factor is applied at most once per
// animation frame through the same immediate, animation-free fast path the
// touch pinch uses (_applyXRange). Deliberately instant, trading-chart style:
// no per-step morph and no easing between steps (an animated variant was
// tried and rejected). The original implementation instead ran a fixed
// 0.5x/1.5x animated update at most once per 400ms and dropped every wheel
// event in between, which read as lag on continuous scrolling.
//
// Like Momentum (see the comment above momentumTouch), applying a frame
// triggers _updateOptions, which destroys and recreates this instance
// mid-gesture, so all wheel-gesture state lives on w.interact.wheel rather
// than on the instance.
// ---------------------------------------------------------------------------
/**
* A wheel or pinch zoom is an incidental gesture: the viewer can land in a
* zoomed window without meaning to (a page scroll over the chart, a two-finger
* swipe), so it is only offered when there is a way back out of it. The only
* built-in way back is the toolbar's reset button, hence 'auto' (the default
* for both allowMouseWheelZoom and pinch) resolves against that button being
* present. A page that builds its own reset control sets the option to true
* and gets the gesture with no toolbar. Drag-to-zoom is deliberate, so it is
* not gated this way.
*
* @param {boolean|'auto'} setting
*/
_incidentalZoomEnabled(setting) {
var _a, _b, _c;
const c = this.w.config.chart;
if (!c.zoom || !c.zoom.enabled) return false;
if (setting !== "auto") return !!setting;
return !!(((_a = c.toolbar) == null ? void 0 : _a.show) && ((_c = (_b = c.toolbar) == null ? void 0 : _b.tools) == null ? void 0 : _c.reset));
}
_wheelZoomEnabled() {
const { zoom } = this.w.config.chart;
return this._incidentalZoomEnabled(zoom && zoom.allowMouseWheelZoom);
}
/** Lazily-created, re-render-surviving wheel-gesture state. */
_wheel() {
const it = this.w.interact;
if (!it.wheel) {
it.wheel = {
factor: 1,
clientX: 0,
/** @type {number|null} */
rafId: null,
/** @type {any} */
endTimer: null
};
}
return it.wheel;
}
/**
* @param {any} e
*/
mouseWheelEvent(e) {
e.preventDefault();
const st = this._wheel();
let dy = e.deltaY;
if (e.deltaMode === 1) dy *= 33;
else if (e.deltaMode === 2) dy *= 330;
st.factor *= Math.pow(2, dy / WHEEL_ZOOM_PIXELS_PER_2X);
st.clientX = e.clientX;
if (st.rafId == null) {
st.rafId = requestAnimationFrame(() => this._applyWheelZoom());
}
if (st.endTimer) clearTimeout(st.endTimer);
st.endTimer = setTimeout(() => this._endWheelZoom(), 150);
}
/**
* Apply the zoom factor accumulated since the last animation frame, keeping
* the data value under the cursor pinned (both zooming in and out).
*/
_applyWheelZoom() {
const w2 = this.w;
const st = this._wheel();
st.rafId = null;
const scale = st.factor;
st.factor = 1;
if (scale === 1 || w2.globals.isDestroyed) return;
const gridRectDim = this._gridRect();
if (!gridRectDim || !gridRectDim.width) return;
const { min, max } = this._currentXWindow();
const range = max - min;
const mouseX = Math.min(
Math.max((st.clientX - gridRectDim.left) / gridRectDim.width, 0),
1
);
let newRange = range * scale;
const bounds = this._clampBounds();
if (bounds) {
const minXDiff = w2.globals.minXDiff > 0 && isFinite(w2.globals.minXDiff) ? w2.globals.minXDiff : 0;
const minRange = Math.max(minXDiff * 2, (bounds.max - bounds.min) * 1e-6);
if (newRange < minRange) newRange = minRange;
if (newRange > bounds.max - bounds.min) newRange = bounds.max - bounds.min;
}
const anchor = min + mouseX * range;
let newMinX = anchor - mouseX * newRange;
let newMaxX = newMinX + newRange;
const eps = range * 1e-9;
if (Math.abs(newMinX - min) < eps && Math.abs(newMaxX - max) < eps) return;
if (isNaN(newMinX) || isNaN(newMaxX)) return;
const beforeZoomRange = this.getBeforeZoomRange(
{ min: newMinX, max: newMaxX },
/** @type {any} */
void 0
);
if (beforeZoomRange && beforeZoomRange.xaxis) {
newMinX = beforeZoomRange.xaxis.min;
newMaxX = beforeZoomRange.xaxis.max;
}
this._applyXRange(newMinX, newMaxX, true);
}
/** Fire the zoomed callback once the wheel gesture settles (mirrors _endPinch). */
_endWheelZoom() {
const w2 = this.w;
const st = this._wheel();
st.endTimer = null;
if (w2.globals.isDestroyed || !w2.interact.zoomed) return;
const { min, max } = this._currentXWindow();
const yaxis = w2.globals.initialConfig ? Utils.clone(w2.globals.initialConfig.yaxis) : [];
const toolbar = this.ctx.toolbar;
if (toolbar) toolbar.zoomCallback({ min, max }, yaxis);
}
makeSelectionRectDraggable() {
const w2 = 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, p2, 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, p2) => {
return group.center(p2[0], p2[1]);
}
}).resize().on("resize", () => {
var _a;
if (w2.interact.selectionEnabled) {
w2.interact.selection = {
x: parseFloat(this.selectionRect.node.getAttribute("x")),
y: parseFloat(this.selectionRect.node.getAttribute("y")),
width: parseFloat(this.selectionRect.node.getAttribute("width")),
height: parseFloat(this.selectionRect.node.getAttribute("height"))
};
clearTimeout((_a = this.w.globals.selectionResizeTimer) != null ? _a : void 0);
this.w.globals.selectionResizeTimer = window.setTimeout(() => {
this._emitSelectionFromRect();
}, 30);
} else {
const zoomtype = w2.interact.zoomEnabled ? w2.config.chart.zoom.type : w2.config.chart.selection.type;
this.handleMouseUp({ zoomtype, isResized: true });
}
});
}
}
preselectedSelection() {
const w2 = this.w;
const xyRatios = this.xyRatios;
if (!w2.interact.zoomEnabled) {
if (typeof w2.interact.selection !== "undefined" && w2.interact.selection !== null) {
this.drawSelectionRect(__spreadProps(__spreadValues({}, w2.interact.selection), {
translateX: w2.layout.translateX,
translateY: w2.layout.translateY
}));
} else {
if (w2.config.chart.selection.xaxis.min !== void 0 && w2.config.chart.selection.xaxis.max !== void 0) {
let x = AxisMapping.dataXToPx(w2, w2.config.chart.selection.xaxis.min);
let width = AxisMapping.dataXToPx(w2, w2.config.chart.selection.xaxis.max) - x;
if (w2.axisFlags.isRangeBar) {
x = (w2.config.chart.selection.xaxis.min - w2.globals.yAxisScale[0].niceMin) / xyRatios.invertedYRatio;
width = (w2.config.chart.selection.xaxis.max - w2.config.chart.selection.xaxis.min) / xyRatios.invertedYRatio;
}
const selectionRect = {
x,
y: 0,
width,
height: w2.layout.gridHeight,
translateX: w2.layout.translateX,
translateY: w2.layout.translateY,
selectionEnabled: true
};
this.drawSelectionRect(selectionRect);
this.makeSelectionRectDraggable();
if (typeof w2.config.chart.events.selection === "function") {
w2.config.chart.events.selection(this.ctx, {
xaxis: {
min: w2.config.chart.selection.xaxis.min,
max: w2.config.chart.selection.xaxis.max
},
yaxis: {}
});
}
}
}
}
}
/** @param {{x: any, y: any, width: any, height: any, translateX: any, translateY: any}} opts */
drawSelectionRect({ x, y, width, height, translateX = 0, translateY = 0 }) {
const w2 = this.w;
const zoomRect = this.zoomRect;
const selectionRect = this.selectionRect;
if (this.dragged || w2.interact.selection !== null) {
const scalingAttrs = {
transform: "translate(" + translateX + ", " + translateY + ")"
};
if (w2.interact.zoomEnabled && this.dragged) {
if (width < 0) width = 1;
zoomRect.attr({
x,
y,
width,
height,
fill: w2.config.chart.zoom.zoomedArea.fill.color,
"fill-opacity": w2.config.chart.zoom.zoomedArea.fill.opacity,
stroke: w2.config.chart.zoom.zoomedArea.stroke.color,
"stroke-width": w2.config.chart.zoom.zoomedArea.stroke.width,
"stroke-opacity": w2.config.chart.zoom.zoomedArea.stroke.opacity
});
Graphics.setAttrs(zoomRect.node, scalingAttrs);
}
if (w2.interact.selectionEnabled) {
selectionRect.attr({
x,
y,
width: width > 0 ? width : 0,
height: height > 0 ? height : 0,
fill: w2.config.chart.selection.fill.color,
"fill-opacity": w2.config.chart.selection.fill.opacity,
stroke: w2.config.chart.selection.stroke.color,
"stroke-width": w2.config.chart.selection.stroke.width,
"stroke-dasharray": w2.config.chart.selection.stroke.dashArray,
"stroke-opacity": w2.config.chart.selection.stroke.opacity
});
Graphics.setAttrs(selectionRect.node, scalingAttrs);
}
}
}
/**
* @param {any} rect
*/
hideSelectionRect(rect) {
if (rect) {
rect.attr({
x: 0,
y: 0,
width: 0,
height: 0
});
}
}
selectionDrawing({ context, zoomtype }) {
const w2 = this.w;
const me = context;
const gridRectDim = this._gridRect();
if (!gridRectDim) return;
const startX = me.startX - 1;
const startY = me.startY;
let inversedX = false;
let inversedY = false;
const left = this._screenXToPlotPx(me.clientX);
const top = me.clientY - gridRectDim.top;
let selectionWidth = left - startX;
let selectionHeight = top - startY;
let selectionRect = {
translateX: w2.layout.translateX,
translateY: w2.layout.translateY
};
if (Math.abs(selectionWidth + startX) > w2.layout.gridWidth) {
selectionWidth = w2.layout.gridWidth - startX;
} else if (left < 0) {
selectionWidth = startX;
}
if (startX > left) {
inversedX = true;
selectionWidth = Math.abs(selectionWidth);
}
if (startY > top) {
inversedY = true;
selectionHeight = Math.abs(selectionHeight);
}
if (zoomtype === "x") {
selectionRect = {
x: inversedX ? startX - selectionWidth : startX,
y: 0,
width: selectionWidth,
height: w2.layout.gridHeight
};
} else if (zoomtype === "y") {
selectionRect = {
x: 0,
y: inversedY ? startY - selectionHeight : startY,
width: w2.layout.gridWidth,
height: selectionHeight
};
} else {
selectionRect = {
x: inversedX ? startX - selectionWidth : startX,
y: inversedY ? startY - selectionHeight : startY,
width: selectionWidth,
height: selectionHeight
};
}
selectionRect = __spreadProps(__spreadValues({}, selectionRect), {
translateX: w2.layout.translateX,
translateY: w2.layout.translateY
});
me.drawSelectionRect(selectionRect);
me.selectionDragging("resizing");
return selectionRect;
}
/**
* @param {string} type
* @param {CustomEvent} e
*/
selectionDragging(type, e) {
var _a;
const w2 = this.w;
if (!e) return;
e.preventDefault();
const { handler, box } = e.detail;
const constraints = (
/** @type {any} */
this.constraints
);
let { x, y } = box;
if (x < constraints.x) {
x = constraints.x;
}
if (y < constraints.y) {
y = constraints.y;
}
if (box.x2 > constraints.x2) {
x = constraints.x2 - box.w;
}
if (box.y2 > constraints.y2) {
y = constraints.y2 - box.h;
}
handler.move(x, y);
const selRect = this.selectionRect;
let timerInterval = 0;
if (type === "resizing") {
timerInterval = 30;
}
const getSelAttr = (attr) => {
return parseFloat(selRect.node.getAttribute(attr));
};
const draggedProps = {
x: getSelAttr("x"),
y: getSelAttr("y"),
width: getSelAttr("width"),
height: getSelAttr("height")
};
w2.interact.selection = draggedProps;
const link = w2.config.chart.link;
const linkActive = !!(link && (link.enabled || typeof link.dimension === "function"));
if ((typeof w2.config.chart.events.selection === "function" || linkActive) && w2.interact.selectionEnabled) {
clearTimeout((_a = this.w.globals.selectionResizeTimer) != null ? _a : void 0);
this.w.globals.selectionResizeTimer = window.setTimeout(() => {
this._emitSelectionFromRect();
}, timerInterval);
}
}
/**
* Recompute the reported x/y range from the CURRENT persistent selection rect
* (via the shared AxisMapping) and notify listeners: chart.events.selection,
* brushScrolled, and the crossfilter coordinator. Shared by the rect-body drag
* (selectionDragging) and the handle resize (makeSelectionRectDraggable) so
* every gesture re-reports through ONE mapping and the reported range always
* matches the rect the user sees. No dragged/threshold gate: reaching here
* already means the user moved or resized the persistent rect.
*/
_emitSelectionFromRect() {
var _a;
const w2 = this.w;
if (!w2.interact.selectionEnabled) return;
const link = w2.config.chart.link;
const linkActive = !!(link && (link.enabled || typeof link.dimension === "function"));
if (typeof w2.config.chart.events.selection !== "function" && !linkActive) {
return;
}
const gridRectDim = this._gridRect();
if (!gridRectDim) return;
const selectionRect = this.selectionRect.node.getBoundingClientRect();
const xyRatios = this.xyRatios;
let minX, maxX, minY, maxY;
const relLeft = this._screenXToPlotPx(selectionRect.left);
const relRight = this._screenXToPlotPx(selectionRect.right);
if (!w2.axisFlags.isRangeBar) {
if (!w2.globals.xAxisScale) return;
minX = AxisMapping.pxToDataX(w2, relLeft);
maxX = AxisMapping.pxToDataX(w2, relRight);
minY = w2.globals.yAxisScale[0].niceMin + (gridRectDim.bottom - selectionRect.bottom) * xyRatios.yRatio[0];
maxY = w2.globals.yAxisScale[0].niceMax - (selectionRect.top - gridRectDim.top) * xyRatios.yRatio[0];
} else {
minX = w2.globals.yAxisScale[0].niceMin + relLeft * xyRatios.invertedYRatio;
maxX = w2.globals.yAxisScale[0].niceMin + relRight * xyRatios.invertedYRatio;
minY = 0;
maxY = 1;
}
const xyAxis = {
xaxis: { min: minX, max: maxX },
yaxis: { min: minY, max: maxY }
};
if (typeof w2.config.chart.events.selection === "function") {
w2.config.chart.events.selection(this.ctx, xyAxis);
}
if (w2.config.chart.brush.enabled && w2.config.chart.events.brushScrolled !== void 0) {
w2.config.chart.events.brushScrolled(this.ctx, xyAxis);
}
(_a = this.ctx.linkedViews) == null ? void 0 : _a.onSourceSelection(xyAxis.xaxis);
}
/** @param {{context: any, zoomtype: any}} opts */
selectionDrawn({ context, zoomtype }) {
var _a;
const w2 = this.w;
const me = context;
const xyRatios = this.xyRatios;
const toolbar = this.ctx.toolbar;
const selRect = w2.interact.zoomEnabled ? me.zoomRect.node.getBoundingClientRect() : me.selectionRect.node.getBoundingClientRect();
const gridRectDim = me._gridRect();
if (!gridRectDim) return;
const localStartX = this._screenXToPlotPx(selRect.left);
const localEndX = this._screenXToPlotPx(selRect.right);
const localStartY = selRect.top - gridRectDim.top;
const localEndY = selRect.bottom - gridRectDim.top;
let xLowestValue, xHighestValue;
if (!w2.axisFlags.isRangeBar) {
xLowestValue = AxisMapping.pxToDataX(w2, localStartX);
xHighestValue = AxisMapping.pxToDataX(w2, localEndX);
} else {
xLowestValue = w2.globals.yAxisScale[0].niceMin + localStartX * xyRatios.invertedYRatio;
xHighestValue = w2.globals.yAxisScale[0].niceMin + localEndX * xyRatios.invertedYRatio;
}
const yHighestValue = [];
const yLowestValue = [];
w2.config.yaxis.forEach((yaxe, index) => {
const seriesIndex = w2.globals.seriesYAxisMap[index][0];
const highestVal = w2.globals.yAxisScale[index].niceMax - xyRatios.yRatio[seriesIndex] * localStartY;
const lowestVal = w2.globals.yAxisScale[index].niceMax - xyRatios.yRatio[seriesIndex] * localEndY;
yHighestValue.push(highestVal);
yLowestValue.push(lowestVal);
});
if (me.dragged && (me.dragX > 10 || me.dragY > 10) && xLowestValue !== xHighestValue) {
if (w2.interact.zoomEnabled) {
if (!w2.globals.initialConfig) return;
let yaxis = Utils.clone(w2.globals.initialConfig.yaxis);
let xaxis = Utils.clone(w2.globals.initialConfig.xaxis);
w2.interact.zoomed = true;
if (w2.config.xaxis.convertedCatToNumeric) {
xLowestValue = Math.floor(xLowestValue);
xHighestValue = Math.floor(xHighestValue);
if (xLowestValue < 1) {
xLowestValue = 1;
xHighestValue = w2.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) {
const beforeZoomRange = toolbar.getBeforeZoomRange(xaxis, yaxis);
if (beforeZoomRange) {
xaxis = beforeZoomRange.xaxis ? beforeZoomRange.xaxis : xaxis;
yaxis = beforeZoomRange.yaxis ? beforeZoomRange.yaxis : yaxis;
}
}
const options = {
xaxis
};
if (!w2.config.chart.group) {
options.yaxis = yaxis;
}
me.ctx.updateHelpers._updateOptions(
options,
false,
me.w.config.chart.animations.dynamicAnimation.enabled
);
if (typeof w2.config.chart.events.zoomed === "function") {
toolbar.zoomCallback(xaxis, yaxis);
}
} else if (w2.interact.selectionEnabled) {
let yaxis = null;
let xaxis = null;
xaxis = {
min: xLowestValue,
max: xHighestValue
};
if (zoomtype === "xy" || zoomtype === "y") {
const yaxisCopy = (
/** @type {ApexYAxis[]} */
Utils.clone(w2.config.yaxis)
);
yaxis = yaxisCopy;
yaxisCopy.forEach((yaxe, index) => {
yaxisCopy[index].min = yLowestValue[index];
yaxisCopy[index].max = yHighestValue[index];
});
}
w2.interact.selection = me.selection;
if (typeof w2.config.chart.events.selection === "function") {
w2.config.chart.events.selection(me.ctx, {
xaxis,
yaxis
});
}
(_a = me.ctx.linkedViews) == null ? void 0 : _a.onSourceSelection(xaxis);
}
}
}
/** @param {{ context?: any, zoomtype?: any, xyRatios?: any }} opts */
panDragging({ context }) {
var _a;
const w2 = this.w;
const me = context;
if (typeof w2.interact.lastClientPosition.x !== "undefined") {
const deltaX = w2.interact.lastClientPosition.x - me.clientX;
const deltaY = ((_a = w2.interact.lastClientPosition.y) != null ? _a : 0) - me.clientY;
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";
}
}
w2.interact.lastClientPosition = {
x: me.clientX,
y: me.clientY
};
const xLowestValue = w2.axisFlags.isRangeBar ? w2.globals.minY : w2.globals.minX;
const xHighestValue = w2.axisFlags.isRangeBar ? w2.globals.maxY : w2.globals.maxX;
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
// )
// }
/**
* @param {number} xLowestValue
* @param {number} xHighestValue
*/
panScrolled(xLowestValue, xHighestValue) {
var _a, _b;
const w2 = this.w;
const xyRatios = this.xyRatios;
if (!w2.globals.initialConfig) return;
const yaxis = Utils.clone(w2.globals.initialConfig.yaxis);
let xRatio = xyRatios.xRatio;
let minX = w2.globals.minX;
let maxX = w2.globals.maxX;
if (w2.axisFlags.isRangeBar) {
xRatio = xyRatios.invertedYRatio;
minX = w2.globals.minY;
maxX = w2.globals.maxY;
}
if (this.moveDirection === "left") {
xLowestValue = minX + w2.layout.gridWidth / PAN_NUDGE_DIVISOR * xRatio;
xHighestValue = maxX + w2.layout.gridWidth / PAN_NUDGE_DIVISOR * xRatio;
} else if (this.moveDirection === "right") {
xLowestValue = minX - w2.layout.gridWidth / PAN_NUDGE_DIVISOR * xRatio;
xHighestValue = maxX - w2.layout.gridWidth / PAN_NUDGE_DIVISOR * xRatio;
}
if (!w2.axisFlags.isRangeBar) {
const clampMin = (_a = w2.globals.dataReducerRawMinX) != null ? _a : w2.globals.initialMinX;
const clampMax = (_b = w2.globals.dataReducerRawMaxX) != null ? _b : w2.globals.initialMaxX;
if (xLowestValue < clampMin || xHighestValue > clampMax) {
xLowestValue = minX;
xHighestValue = maxX;
}
}
const xaxis = {
min: xLowestValue,
max: xHighestValue
};
const options = {
xaxis
};
if (!w2.config.chart.group) {
options.yaxis = yaxis;
}
this.updateScrolledChart(options, xLowestValue, xHighestValue);
}
/**
* @param {object} options
* @param {number} xLowestValue
* @param {number} xHighestValue
*/
updateScrolledChart(options, xLowestValue, xHighestValue) {
const w2 = this.w;
this.ctx.updateHelpers._updateOptions(options, false, false);
if (typeof w2.config.chart.events.scrolled === "function") {
const args = {
xaxis: {
min: xLowestValue,
max: xHighestValue
}
};
w2.config.chart.events.scrolled(this.ctx, args);
this.ctx.events.fireEvent("scrolled", args);
}
}
// ---------------------------------------------------------------------------
// Momentum: multi-touch pinch-zoom, two-finger pan and kinetic inertia.
//
// Every _updateOptions destroys and recreates this instance, and applying a
// gesture frame IS an _updateOptions, so the gesture must not depend on the
// instance surviving. All runtime state lives on w.interact.momentum (the
// interaction slice that persists across re-renders, like the crude pan's
// lastClientPosition). The instance that received touchstart keeps driving
// the gesture off the persistent state; inertia is a self-contained rAF loop
// that stops on w.globals.isDestroyed (a real destroy) rather than being
// cancelled by the per-update destroy().
// ---------------------------------------------------------------------------
_momentumEnabled() {
return this._pinchEnabled() || this._panInertiaEnabled();
}
_pinchEnabled() {
return this._incidentalZoomEnabled(this.w.config.chart.zoom.pinch);
}
_panInertiaEnabled() {
const c = this.w.config.chart;
return !!(c.pan && c.pan.inertia);
}
/** Lazily-created, re-render-surviving gesture state on the interaction slice. */
_m() {
const it = this.w.interact;
if (!it.momentum) {
it.momentum = {
busy: false,
/** @type {any} */
pinch: null,
/** @type {any} */
panState: null,
/** @type {{x:number,t:number}[]} */
samples: [],
/** @type {number|null} */
inertiaRAF: null
};
}
return it.momentum;
}
/** Current x data-window (rangeBars carry the datetime domain on y). */
_currentXWindow() {
const w2 = this.w;
return w2.axisFlags.isRangeBar ? { min: w2.globals.minY, max: w2.globals.maxY } : { min: w2.globals.minX, max: w2.globals.maxX };
}
/** Live grid rect from the current DOM. Never cache the grid node on the
* instance: a full render replaces this whole instance, but the fast update
* path (fastUpdate/_fastAxisChromeRefresh) keeps the instance while swapping
* the grid node, and a cached node would go stale (detached nodes report an
* all-zero bounding rect, silently corrupting selection geometry). */
_gridRect() {
const baseEl = this.w.dom.baseEl;
const grid = baseEl && baseEl.querySelector(".apexcharts-grid");
return grid ? grid.getBoundingClientRect() : null;
}
/**
* Convert an absolute (client) x pixel to the plot-origin coordinate space
* that bar placement and the selection rect transform both use:
* `screenX - svgLeft - translateX`. This is the ONLY correct reference for the
* numeric/datetime x mapping (see AxisMapping): do NOT measure from the
* `.apexcharts-grid` box and subtract barPadForNumericAxis, because on a
* numeric bar chart that box extends barPad to the LEFT of the plot origin, so
* the two corrections are a fragile pair that only cancels while the grid box
* happens to extend exactly barPad. Anchoring on translateX (the same origin
* the bars use) is stable regardless of grid padding.
* @param {number} screenX
* @returns {number}
*/
_screenXToPlotPx(screenX) {
return AxisMapping.screenXToPlotPx(this.w, screenX);
}
/**
* Raw data bounds to clamp against. When zoom-aware downsampling is active,
* the raw stash tracks the full domain; fall back to the initial window.
* Returns null for rangeBars (no raw-x clamp available).
* @returns {{min:number, max:number}|null}
*/
_clampBounds() {
var _a, _b;
const w2 = this.w;
if (w2.axisFlags.isRangeBar) return null;
return {
min: (_a = w2.globals.dataReducerRawMinX) != null ? _a : w2.globals.initialMinX,
max: (_b = w2.globals.dataReducerRawMaxX) != null ? _b : w2.globals.initialMaxX
};
}
/**
* Apply an x-window immediately (no animation), mirroring panScrolled but
* pixel-accurate: clamp to the raw bounds (preserving window width so a pan
* stops flush at the edge rather than shrinking), floor for category axes,
* then route through the fast _updateOptions path.
* @param {number} newMinX @param {number} newMaxX @param {boolean} isZoom
* @returns {{minX:number, maxX:number}|false} applied window, or false if rejected
*/
_applyXRange(newMinX, newMaxX, isZoom) {
const w2 = this.w;
if (!w2.globals.initialConfig) return false;
const cur = this._currentXWindow();
const zoomingOut = isZoom && newMaxX - newMinX > cur.max - cur.min;
const bounds = this._clampBounds();
if (bounds) {
const range = newMaxX - newMinX;
if (newMinX < bounds.min) {
newMinX = bounds.min;
newMaxX = newMinX + range;
}
if (newMaxX > bounds.max) {
newMaxX = bounds.max;
newMinX = newMaxX - range;
}
if (newMinX < bounds.min) newMinX = bounds.min;
}
if (w2.config.xaxis.convertedCatToNumeric) {
newMinX = Math.floor(newMinX);
newMaxX = zoomingOut ? Math.ceil(newMaxX) : Math.floor(newMaxX);
if (newMinX < 1) newMinX = 1;
if (bounds && newMaxX > bounds.max) newMaxX = Math.floor(bounds.max);
if (newMaxX - newMinX < 2) return false;
}
if (!(newMaxX > newMinX)) return false;
const options = { xaxis: { min: newMinX, max: newMaxX } };
if (!w2.config.chart.group) {
options.yaxis = Utils.clone(w2.globals.initialConfig.yaxis);
}
if (isZoom) w2.interact.zoomed = true;
this.ctx.updateHelpers._updateOptions(options, false, false);
return { minX: newMinX, maxX: newMaxX };
}
_cancelInertia() {
const m2 = this._m();
if (m2.inertiaRAF != null) {
cancelAnimationFrame(m2.inertiaRAF);
m2.inertiaRAF = null;
}
}
_fireScrolled() {
const w2 = this.w;
if (typeof w2.config.chart.events.scrolled !== "function") return;
const { min, max } = this._currentXWindow();
const args = { xaxis: { min, max } };
w2.config.chart.events.scrolled(this.ctx, args);
this.ctx.events.fireEvent("scrolled", args);
}
/** @param {number} x @param {number} t */
_pushSample(x, t) {
const s = this._m().samples;
s.push({ x, t });
while (s.length > 6) s.shift();
}
/**
* Single passive:false handler for all touch phases. Two fingers => pinch /
* two-finger pan (zoom). One finger, in pan mode => kinetic pan with inertia.
* @param {any} e
*/
momentumTouch(e) {
const w2 = this.w;
const m2 = this._m();
const type = e.type;
if (type === "touchstart") {
this._cancelInertia();
const gridRectDim = this._gridRect();
if (!gridRectDim) return;
if (e.touches.length >= 2 && this._pinchEnabled()) {
e.preventDefault();
m2.busy = true;
m2.panState = null;
this._beginPinch(e, gridRectDim);
} else if (e.touches.length === 1 && this._panInertiaEnabled() && w2.interact.panEnabled) {
m2.busy = true;
m2.pinch = null;
const t = e.touches[0];
const win = this._currentXWindow();
const gw = w2.layout.gridWidth || 1;
m2.panState = {
startX: t.clientX,
startY: t.clientY,
axis: null,
// decided on first move (rails)
minX0: win.min,
maxX0: win.max,
ratio0: (win.max - win.min) / gw
};
m2.samples = [{ x: t.clientX, t: e.timeStamp }];
}
return;
}
if (type === "touchmove") {
if (m2.pinch && e.touches.length >= 2) {
e.preventDefault();
this._movePinch(e);
} else if (m2.panState && e.touches.length === 1) {
this._movePan(e);
}
return;
}
if (m2.pinch) {
if (e.touches.length < 2) this._endPinch();
} else if (m2.panState) {
if (e.touches.length === 0) this._endPan();
}
if (e.touches.length === 0) {
w2.interact.mousedown = false;
this.dragged = false;
if (m2.inertiaRAF == null && !m2.pinch && !m2.panState) {
m2.busy = false;
}
}
}
/** @param {any} e @param {DOMRect} gridRectDim */
_beginPinch(e, gridRectDim) {
const w2 = this.w;
const t0 = e.touches[0];
const t1 = e.touches[1];
const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY) || 1;
const cx = (t0.clientX + t1.clientX) / 2 - gridRectDim.left - w2.globals.barPadForNumericAxis;
const { min, max } = this._currentXWindow();
this._m().pinch = {
d0: dist,
cx0: cx,
minX0: min,
maxX0: max,
gridWidth: w2.layout.gridWidth || 1
};
}
/** @param {any} e */
_movePinch(e) {
const w2 = this.w;
const p2 = this._m().pinch;
if (!p2) return;
const gridRectDim = this._gridRect();
if (!gridRectDim) return;
const t0 = e.touches[0];
const t1 = e.touches[1];
const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY) || 1;
const cx = (t0.clientX + t1.clientX) / 2 - gridRectDim.left - w2.globals.barPadForNumericAxis;
const range0 = p2.maxX0 - p2.minX0;
const newRange = range0 * (p2.d0 / dist);
const anchorData = p2.minX0 + p2.cx0 / p2.gridWidth * range0;
let newMinX = anchorData - cx / p2.gridWidth * newRange;
let newMaxX = newMinX + newRange;
const bounds = this._clampBounds();
if (bounds) {
const minXDiff = w2.globals.minXDiff > 0 && isFinite(w2.globals.minXDiff) ? w2.globals.minXDiff : 0;
const minRange = Math.max(minXDiff * 2, (bounds.max - bounds.min) * 1e-6);
if (newMaxX - newMinX < minRange) {
const mid = (newMinX + newMaxX) / 2;
newMinX = mid - minRange / 2;
newMaxX = mid + minRange / 2;
}
}
this._applyXRange(newMinX, newMaxX, true);
}
_endPinch() {
const w2 = this.w;
const m2 = this._m();
m2.pinch = null;
const { min, max } = this._currentXWindow();
const xaxis = { min, max };
const yaxis = w2.globals.initialConfig ? Utils.clone(w2.globals.initialConfig.yaxis) : [];
const toolbar = this.ctx.toolbar;
if (toolbar) toolbar.zoomCallback(xaxis, yaxis);
}
/** @param {any} e */
_movePan(e) {
const m2 = this._m();
const s = m2.panState;
const t = e.touches[0];
if (!s.axis) {
const dx = Math.abs(t.clientX - s.startX);
const dy = Math.abs(t.clientY - s.startY);
if (dx < 6 && dy < 6) {
this._pushSample(t.clientX, e.timeStamp);
return;
}
if (dy > dx) {
m2.busy = false;
m2.panState = null;
return;
}
s.axis = "x";
}
if (s.axis !== "x") return;
e.preventDefault();
const totalDeltaPx = t.clientX - s.startX;
const deltaData = totalDeltaPx * s.ratio0;
this._pushSample(t.clientX, e.timeStamp);
this._applyXRange(s.minX0 - deltaData, s.maxX0 - deltaData, false);
}
_endPan() {
const m2 = this._m();
const s = m2.panState;
m2.panState = null;
let vel = 0;
const samples = m2.samples;
if (samples.length >= 2) {
const a = samples[0];
const b2 = samples[samples.length - 1];
const dt = b2.t - a.t;
if (dt > 0) vel = (b2.x - a.x) / dt;
}
m2.samples = [];
if (s && s.axis === "x" && this._panInertiaEnabled() && Math.abs(vel) > INERTIA_MIN_RELEASE_VELOCITY) {
this._startInertia(vel);
} else {
m2.busy = false;
this._fireScrolled();
}
}
/**
* Kinetic glide after a one-finger pan release: decay the velocity by
* `friction` each frame and shift the window, stopping at the data edge
* (clamp, not elastic overshoot). The loop is w-driven, so it keeps running
* across the re-renders each frame triggers and stops only on a real destroy.
* @param {number} vel0 px/ms, sign is the finger direction
*/
_startInertia(vel0) {
const w2 = this.w;
const m2 = this._m();
const cfgFriction = w2.config.chart.pan && w2.config.chart.pan.friction;
const friction = typeof cfgFriction === "number" ? Math.min(Math.max(cfgFriction, 0.5), 0.999) : INERTIA_DEFAULT_FRICTION;
let vel = vel0;
let lastT = null;
m2.busy = true;
const step = (ts) => {
if (w2.globals.isDestroyed) {
m2.inertiaRAF = null;
m2.busy = false;
return;
}
if (lastT == null) {
lastT = ts;
m2.inertiaRAF = requestAnimationFrame(step);
return;
}
const dt = ts - lastT;
lastT = ts;
vel *= Math.pow(friction, dt / FRAME_MS_60FPS);
if (Math.abs(vel) < INERTIA_STOP_VELOCITY) {
m2.inertiaRAF = null;
m2.busy = false;
this._fireScrolled();
return;
}
const win = this._currentXWindow();
const gw = w2.layout.gridWidth || 1;
const ratio = (win.max - win.min) / gw;
const deltaData = vel * dt * ratio;
const applied = this._applyXRange(
win.min - deltaData,
win.max - deltaData,
false
);
const bounds = this._clampBounds();
const hitEdge = !applied || bounds && (deltaData > 0 && applied.minX <= bounds.min + (bounds.max - bounds.min) * 1e-6 || deltaData < 0 && applied.maxX >= bounds.max - (bounds.max - bounds.min) * 1e-6);
if (hitEdge) {
m2.inertiaRAF = null;
m2.busy = false;
this._fireScrolled();
return;
}
m2.inertiaRAF = requestAnimationFrame(step);
};
m2.inertiaRAF = requestAnimationFrame(step);
}
}
_core__default.registerFeatures({
toolbar: Toolbar,
zoomPanSelection: ZoomPanSelection
});
let Helpers$2 = class Helpers2 {
/**
* @param {import('./Annotations').default} annoCtx
*/
constructor(annoCtx) {
this.w = annoCtx.w;
this.annoCtx = annoCtx;
}
/**
* @param {Record<string, any>} anno
* @param {number | null} [annoIndex]
*/
setOrientations(anno, annoIndex = null) {
var _a, _b;
const w2 = this.w;
if (anno.label.orientation === "vertical") {
const i = annoIndex !== null ? annoIndex : 0;
const xAnno = w2.dom.baseEl.querySelector(
`.apexcharts-xaxis-annotations .apexcharts-xaxis-annotation-label[rel='${i}']`
);
if (xAnno !== null) {
const xAnnoCoord = (
/** @type {SVGGraphicsElement} */
xAnno.getBBox()
);
xAnno.setAttribute(
"x",
String(
parseFloat((_a = xAnno.getAttribute("x")) != null ? _a : "0") - xAnnoCoord.height + 4
)
);
const yOffset = anno.label.position === "top" ? xAnnoCoord.width : -xAnnoCoord.width;
xAnno.setAttribute(
"y",
String(parseFloat((_b = xAnno.getAttribute("y")) != null ? _b : "0") + yOffset)
);
const { x, y } = this.annoCtx.graphics.rotateAroundCenter(xAnno);
xAnno.setAttribute("transform", `rotate(-90 ${x} ${y})`);
}
}
}
/**
* @param {any} annoEl
* @param {Record<string, any>} anno
*/
addBackgroundToAnno(annoEl, anno) {
const w2 = this.w;
if (!annoEl || !anno.label.text || !String(anno.label.text).trim()) {
return null;
}
const gridEl = w2.dom.baseEl.querySelector(".apexcharts-grid");
if (!gridEl) return null;
const elGridRect = gridEl.getBoundingClientRect();
const gridBBox = (
/** @type {SVGGraphicsElement} */
gridEl.getBBox()
);
const zoom = elGridRect.width / gridBBox.width || 1;
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 gridLeft = elGridRect.left - gridBBox.x * zoom;
const gridTop = elGridRect.top - gridBBox.y * zoom;
const x1 = (coords.left - gridLeft) / zoom - pleft;
const y1 = (coords.top - gridTop) / zoom - ptop;
const elRect = this.annoCtx.graphics.drawRect(
x1,
y1,
coords.width / zoom + pleft + pright,
coords.height / zoom + 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 w2 = this.w;
const add = (anno, i, type) => {
const annoLabel = w2.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 == null ? void 0 : parent.insertBefore(elRect.node, annoLabel);
const labelX = annoLabel.getAttribute("x");
if (labelX !== null) {
applyProgressiveReveal(elRect, parseFloat(labelX), w2);
}
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)
);
}
}
}
};
w2.config.annotations.xaxis.forEach(
(anno, i) => add(anno, i, "xaxis")
);
w2.config.annotations.yaxis.forEach(
(anno, i) => add(anno, i, "yaxis")
);
w2.config.annotations.points.forEach(
(anno, i) => add(anno, i, "point")
);
}
/**
* Does the x position take the category branch of `getX1X2` (a label lookup)
* rather than projecting through a numeric domain? Mirrors the conditions
* applied there, so the two cannot drift apart.
*
* @returns {boolean}
*/
usesCategoryX() {
const w2 = this.w;
return (w2.config.xaxis.type === "category" || w2.config.xaxis.convertedCatToNumeric) && !this.annoCtx.invertAxis && !w2.axisFlags.dataFormatXNumeric && !w2.config.chart.sparkline.enabled;
}
/**
* Is there a real domain for an x position to project through?
*
* An empty series still gets a laid-out grid and a y scale (the default 0..6,
* or the configured `yaxis.min`/`max`), which is why a y-axis annotation is
* always placeable. Nothing bounds the x domain though: `maxX` is left
* undefined and `xRange` is NaN, and a category axis has no labels to index
* into. Projecting through that is silently wrong rather than merely absent:
* NaN sails past the clip comparisons in `getX1X2` (both `NaN > gridWidth`
* and `NaN < 0` are false), and the category branch hands back the raw value
* as a pixel offset, so `x: 5` draws 5px from the grid's left edge.
*
* Gating each annotation on this replaces the chart-wide `dataPoints` check
* that used to sit in `drawAxesAnnotations()` (#1832), which suppressed the
* placeable y-axis annotations along with the unplaceable x ones (#5278).
*
* @returns {boolean}
*/
hasXDomain() {
const w2 = this.w;
if (this.annoCtx.invertAxis) {
return Utils.isNumber(w2.globals.minY) && Utils.isNumber(w2.globals.yRange[0]);
}
if (this.usesCategoryX()) {
return w2.labelData.labels.length > 0 || w2.labelData.categoryLabels.length > 0;
}
return Utils.isNumber(w2.globals.minX) && Utils.isNumber(w2.globals.xRange);
}
/**
* @param {string} type
* @param {Record<string, any>} anno
*/
getY1Y2(type, anno) {
var _a, _b, _c;
const w2 = this.w;
const y = type === "y1" ? anno.y : anno.y2;
const isPx = typeof y === "string" && y.includes("px");
let yP;
let clipped = false;
if (this.annoCtx.invertAxis) {
const labels = w2.config.xaxis.convertedCatToNumeric ? w2.labelData.categoryLabels : w2.labelData.labels;
const catIndex = labels.indexOf(y);
if (!isPx && catIndex === -1) {
return { yP: 0, clipped: true };
}
const xLabel = w2.dom.baseEl.querySelector(
`.apexcharts-yaxis-texts-g text:nth-child(${catIndex + 1})`
);
yP = xLabel ? parseFloat((_a = xLabel.getAttribute("y")) != null ? _a : "0") : (w2.layout.gridHeight / labels.length - 1) * (catIndex + 1) - w2.globals.barHeight;
if (anno.seriesIndex !== void 0 && w2.globals.barHeight) {
yP -= w2.globals.barHeight / 2 * (w2.seriesData.series.length - 1) - w2.globals.barHeight * anno.seriesIndex;
}
} else {
if (!w2.config.yaxis[anno.yAxisIndex]) {
return { yP: 0, clipped: true };
}
const yAxisMap = w2.globals.seriesYAxisMap[anno.yAxisIndex];
const seriesIndex = (_b = yAxisMap == null ? void 0 : yAxisMap[0]) != null ? _b : null;
if (seriesIndex === null && w2.seriesData.series.length) {
return { yP: 0, clipped: true };
}
const yMin = seriesIndex === null ? w2.globals.minY : w2.globals.minYArr[seriesIndex];
const yRange = seriesIndex === null ? w2.globals.maxY - w2.globals.minY : w2.globals.yRange[seriesIndex];
const yPos = w2.config.yaxis[anno.yAxisIndex].logarithmic && seriesIndex !== null ? new CoreUtils(this.w).getLogVal(
w2.config.yaxis[anno.yAxisIndex].logBase,
y,
seriesIndex
) / /** @type {any} */
w2.globals.yLogRatio[seriesIndex] : (y - yMin) / (yRange / w2.layout.gridHeight);
yP = w2.layout.gridHeight - Math.min(Math.max(yPos, 0), w2.layout.gridHeight);
clipped = yPos > w2.layout.gridHeight || yPos < 0;
if (anno.marker && (anno.y === void 0 || anno.y === null)) {
yP = 0;
}
if ((_c = w2.config.yaxis[anno.yAxisIndex]) == null ? void 0 : _c.reversed) {
yP = yPos;
}
}
if (isPx) {
yP = parseFloat(
/** @type {string} */
y
);
}
return { yP, clipped };
}
/**
* @param {string} type
* @param {Record<string, any>} anno
*/
getX1X2(type, anno) {
const w2 = this.w;
const x = type === "x1" ? anno.x : anno.x2;
const min = this.annoCtx.invertAxis ? w2.globals.minY : w2.globals.minX;
const max = this.annoCtx.invertAxis ? w2.globals.maxY : w2.globals.maxX;
const range = this.annoCtx.invertAxis ? w2.globals.yRange[0] : w2.globals.xRange;
let clipped = false;
const isPx = typeof x === "string" && x.includes("px");
const isEdgeMarker = (x === void 0 || x === null) && anno.marker;
if (!isPx && !isEdgeMarker && !this.hasXDomain()) {
return { x: 0, clipped: true };
}
let xP = this.annoCtx.inversedReversedAxis ? (max - x) / (range / w2.layout.gridWidth) : (x - min) / (range / w2.layout.gridWidth);
if ((w2.config.xaxis.type === "category" || w2.config.xaxis.convertedCatToNumeric) && !this.annoCtx.invertAxis && !w2.axisFlags.dataFormatXNumeric) {
if (!w2.config.chart.sparkline.enabled) {
xP = this.getStringX(x);
}
}
if (typeof x === "string" && x.includes("px")) {
xP = parseFloat(x);
}
if ((x === void 0 || x === null) && anno.marker) {
xP = w2.layout.gridWidth;
}
if (anno.seriesIndex !== void 0 && w2.globals.barWidth && !this.annoCtx.invertAxis) {
xP -= w2.globals.barWidth / 2 * (w2.seriesData.series.length - 1) - w2.globals.barWidth * anno.seriesIndex;
}
if (typeof xP !== "number") {
xP = 0;
clipped = true;
}
if (parseFloat(xP.toFixed(10)) > parseFloat(w2.layout.gridWidth.toFixed(10))) {
xP = w2.layout.gridWidth;
clipped = true;
} else if (xP < 0) {
xP = 0;
clipped = true;
}
return { x: xP, clipped };
}
/**
* @param {number} x
*/
getStringX(x) {
var _a;
const w2 = this.w;
let rX = x;
if (w2.config.xaxis.convertedCatToNumeric && w2.labelData.categoryLabels.length) {
const strX = String(x);
x = w2.labelData.categoryLabels.findIndex(
(l) => String(l) === strX
) + 1;
}
const catIndex = w2.labelData.labels.map(
(item) => Array.isArray(item) ? item.join(" ") : item
).indexOf(x);
const xLabel = w2.dom.baseEl.querySelector(
`.apexcharts-xaxis-texts-g text:nth-child(${catIndex + 1})`
);
if (xLabel) {
rX = parseFloat((_a = xLabel.getAttribute("x")) != null ? _a : "0");
}
return rX;
}
};
class XAnnotations {
/**
* @param {import('./Annotations').default} annoCtx
*/
constructor(annoCtx) {
this.w = annoCtx.w;
this.annoCtx = annoCtx;
this.invertAxis = this.annoCtx.invertAxis;
this.helpers = new Helpers$2(this.annoCtx);
}
/**
* @param {XAxisAnnotations} anno
* @param {Element} parent
* @param {number} index
*/
addXaxisAnnotation(anno, parent, index) {
const w2 = this.w;
const result = this.helpers.getX1X2("x1", anno);
let x1 = result.x;
const clipX1 = result.clipped;
let clipX2 = true;
let x2;
const text = anno.label.text;
const strokeDashArray = anno.strokeDashArray;
if (!Utils.isNumber(x1)) return;
if (anno.x2 === null || typeof anno.x2 === "undefined") {
if (!clipX1) {
const line = this.annoCtx.graphics.drawLine(
x1 + anno.offsetX,
// x1
0 + anno.offsetY,
// y1
x1 + anno.offsetX,
// x2
w2.layout.gridHeight + anno.offsetY,
// y2
anno.borderColor,
// lineColor
strokeDashArray,
//dashArray
anno.borderWidth
);
parent.appendChild(line.node);
if (anno.id) {
line.node.classList.add(anno.id);
}
applyProgressiveReveal(line, x1 + anno.offsetX, w2);
}
} else {
const result2 = this.helpers.getX1X2("x2", anno);
x2 = result2.x;
clipX2 = result2.clipped;
if (x2 < x1) {
const temp = x1;
x1 = x2;
x2 = temp;
}
const rect = this.annoCtx.graphics.drawRect(
x1 + anno.offsetX,
// x1
0 + anno.offsetY,
// y1
x2 - x1,
// x2
w2.layout.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${w2.globals.cuid})`);
parent.appendChild(rect.node);
if (anno.id) {
rect.node.classList.add(anno.id);
}
applyProgressiveReveal(rect, x1 + anno.offsetX, w2);
}
if (!(clipX1 && clipX2)) {
const textRects = this.annoCtx.graphics.getTextRects(
text,
anno.label.style.fontSize
);
const textY = anno.label.position === "top" ? 4 : anno.label.position === "center" ? w2.layout.gridHeight / 2 + (anno.label.orientation === "vertical" ? textRects.width / 2 : 0) : w2.layout.gridHeight;
const 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);
applyProgressiveReveal(elText, x1 + anno.label.offsetX, w2);
this.annoCtx.helpers.setOrientations(anno, index);
}
}
drawXAxisAnnotations() {
const w2 = this.w;
const elg = this.annoCtx.graphics.group({
class: "apexcharts-xaxis-annotations"
});
w2.config.annotations.xaxis.map(
(anno, index) => {
this.addXaxisAnnotation(anno, elg.node, index);
}
);
return elg;
}
}
class YAnnotations {
/**
* @param {import('./Annotations').default} annoCtx
*/
constructor(annoCtx) {
this.w = annoCtx.w;
this.annoCtx = annoCtx;
this.helpers = new Helpers$2(this.annoCtx);
this.axesUtils = new AxesUtils(this.annoCtx.w, {
theme: this.annoCtx.theme,
timeScale: this.annoCtx.timeScale
});
}
/**
* @param {YAxisAnnotations} anno
* @param {Element} parent
* @param {number} index
*/
addYaxisAnnotation(anno, parent, index) {
const w2 = this.w;
const strokeDashArray = anno.strokeDashArray;
let result = this.helpers.getY1Y2("y1", anno);
let y1 = result.yP;
const 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;
const 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) {
const temp = y1;
y1 = y2;
y2 = temp;
}
if (!(clipY1 && clipY2)) {
drawn = true;
const 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${w2.globals.cuid})`);
parent.appendChild(rect.node);
if (anno.id) {
rect.node.classList.add(anno.id);
}
}
}
if (drawn) {
const textX = anno.label.position === "right" ? w2.layout.gridWidth : anno.label.position === "center" ? w2.layout.gridWidth / 2 : 0;
const 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);
}
}
/**
* @param {YAxisAnnotations} anno
*/
_getYAxisAnnotationWidth(anno) {
const w2 = this.w;
let width = w2.layout.gridWidth;
if (anno.width.indexOf("%") > -1) {
width = w2.layout.gridWidth * parseInt(anno.width, 10) / 100;
} else {
width = parseInt(anno.width, 10);
}
return width + anno.offsetX;
}
drawYAxisAnnotations() {
const w2 = this.w;
const elg = this.annoCtx.graphics.group({
class: "apexcharts-yaxis-annotations"
});
w2.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;
}
}
class PointAnnotations {
/**
* @param {import('./Annotations').default} annoCtx
*/
constructor(annoCtx) {
this.w = annoCtx.w;
this.annoCtx = annoCtx;
this.helpers = new Helpers$2(this.annoCtx);
}
/**
* @param {Record<string, any>} anno
* @param {Element} parent
* @param {number} index
*/
addPointAnnotation(anno, parent, index) {
const w2 = this.w;
if (w2.globals.collapsedSeriesIndices.indexOf(anno.seriesIndex) > -1) {
return;
}
const resultX = this.helpers.getX1X2("x1", anno);
const x = resultX.x;
const clipX = resultX.clipped;
const resultY = this.helpers.getY1Y2("y1", anno);
const y = resultY.yP;
const clipY = resultY.clipped;
if (!Utils.isNumber(x)) return;
if (!(clipY || clipX)) {
const 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 tooltipTargets = [point.node];
applyProgressiveReveal(point, x, w2);
const text = anno.label.text ? anno.label.text : "";
const 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);
applyProgressiveReveal(elText, x, w2);
if (anno.customSVG.SVG) {
const g2 = this.annoCtx.graphics.group({
class: "apexcharts-point-annotations-custom-svg " + anno.customSVG.cssClass
});
g2.attr({
transform: `translate(${x + anno.customSVG.offsetX}, ${y + anno.customSVG.offsetY})`
});
g2.node.innerHTML = anno.customSVG.SVG;
parent.appendChild(g2.node);
tooltipTargets.push(g2.node);
}
if (anno.image.path) {
const imgWidth = anno.image.width ? anno.image.width : 20;
const 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"
});
tooltipTargets.push(point.node);
}
if (anno.tooltip && anno.tooltip.enabled) {
tooltipTargets.forEach((node) => {
node.addEventListener("mouseenter", () => {
this.showPointTooltip(anno, node);
});
node.addEventListener("mouseleave", () => {
this.hidePointTooltip();
});
});
}
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));
}
}
}
/**
* Lazily create (once per chart) and return the shared HTML element used to
* render point-annotation tooltips. Reuses the `.apexcharts-tooltip` glass
* styling; the `.apexcharts-annotation-tooltip` modifier adds padding and
* text wrapping for free-form content.
* @returns {HTMLElement}
*/
getPointTooltipEl() {
const w2 = this.w;
let el = (
/** @type {HTMLElement | null} */
w2.dom.elWrap.querySelector(".apexcharts-annotation-tooltip")
);
if (!el) {
el = /** @type {HTMLElement} */
BrowserAPIs.createElementNS("http://www.w3.org/1999/xhtml", "div");
el.classList.add("apexcharts-tooltip", "apexcharts-annotation-tooltip");
w2.dom.elWrap.appendChild(el);
}
return el;
}
/**
* Resolve the tooltip markup for a point annotation. Precedence:
* `tooltip.formatter` (fn) -> `tooltip.text` -> `label.text`. Arrays are
* joined with line breaks.
* @param {Record<string, any>} anno
* @returns {string}
*/
getPointTooltipContent(anno) {
const w2 = this.w;
const tt = anno.tooltip || {};
if (typeof tt.formatter === "function") {
return tt.formatter({
annotation: anno,
seriesIndex: anno.seriesIndex,
id: anno.id,
w: w2
});
}
let content = tt.text != null ? tt.text : anno.label && anno.label.text;
if (Array.isArray(content)) {
content = content.join("<br/>");
}
return content == null ? "" : String(content);
}
/**
* @param {Record<string, any>} anno
* @param {Element} targetNode the hovered marker / image / custom-SVG node
*/
showPointTooltip(anno, targetNode) {
const w2 = this.w;
const content = this.getPointTooltipContent(anno);
if (!content) return;
const el = this.getPointTooltipEl();
el.innerHTML = content;
const theme = anno.tooltip.theme || w2.config.tooltip.theme || "light";
el.classList.remove("apexcharts-theme-light", "apexcharts-theme-dark");
el.classList.add(`apexcharts-theme-${theme}`);
el.classList.add("apexcharts-active");
const wrapRect = w2.dom.elWrap.getBoundingClientRect();
const markRect = targetNode.getBoundingClientRect();
const ttRect = el.getBoundingClientRect();
const offsetX = anno.tooltip.offsetX || 0;
const offsetY = anno.tooltip.offsetY || 0;
let left = markRect.left - wrapRect.left + markRect.width / 2 - ttRect.width / 2;
let top = markRect.top - wrapRect.top - ttRect.height - 10;
left = Math.max(0, Math.min(left, wrapRect.width - ttRect.width));
if (top < 0) {
top = markRect.top - wrapRect.top + markRect.height + 10;
}
el.style.left = left + offsetX + "px";
el.style.top = top + offsetY + "px";
}
hidePointTooltip() {
const el = (
/** @type {HTMLElement | null} */
this.w.dom.elWrap.querySelector(".apexcharts-annotation-tooltip")
);
if (el) {
el.classList.remove("apexcharts-active");
}
}
drawPointAnnotations() {
const w2 = this.w;
const elg = this.annoCtx.graphics.group({
class: "apexcharts-point-annotations"
});
w2.config.annotations.points.map(
(anno, index) => {
this.addPointAnnotation(anno, elg.node, index);
}
);
return elg;
}
}
class Annotations {
/**
* @param {import('../../types/internal').ChartStateW} w
*/
constructor(w2, { theme = null, timeScale = null } = {}) {
this.w = w2;
this.theme = theme;
this.timeScale = timeScale;
this.invertAxis = void 0;
this.inversedReversedAxis = void 0;
this.graphics = new Graphics(this.w);
if (this.w.globals.isBarHorizontal) {
this.invertAxis = true;
}
this.helpers = new Helpers$2(this);
this.xAxisAnnotations = new XAnnotations(this);
this.yAxisAnnotations = new YAnnotations(this);
this.pointsAnnotations = new PointAnnotations(this);
if (this.w.globals.isBarHorizontal && this.w.config.yaxis[0].reversed) {
this.inversedReversedAxis = true;
}
this.xDivision = this.w.layout.gridWidth / this.w.globals.dataPoints;
}
drawAxesAnnotations() {
const w2 = this.w;
if (w2.globals.axisCharts) {
const yAnnotations = this.yAxisAnnotations.drawYAxisAnnotations();
const xAnnotations = this.xAxisAnnotations.drawXAxisAnnotations();
const pointAnnotations = this.pointsAnnotations.drawPointAnnotations();
const initialAnim = w2.config.chart.animations.enabled;
const annoArray = [yAnnotations, xAnnotations, pointAnnotations];
const annoElArray = [
xAnnotations.node,
yAnnotations.node,
pointAnnotations.node
];
const progressiveAnnos = w2.config.chart.type === "line" || w2.config.chart.type === "area" || w2.config.chart.type === "rangeArea";
const skipGroupHide = [progressiveAnnos, false, progressiveAnnos];
for (let i = 0; i < 3; i++) {
w2.dom.elGraphical.add(annoArray[i]);
if (initialAnim && !w2.globals.resized && !w2.globals.dataChanged) {
if (w2.config.chart.type !== "scatter" && w2.config.chart.type !== "bubble" && w2.globals.dataPoints > 1 && !skipGroupHide[i]) {
annoElArray[i].classList.add("apexcharts-element-hidden");
}
}
w2.globals.delayedElements.push({ el: annoElArray[i], index: 0 });
}
this.helpers.annotationsBackground();
}
}
drawImageAnnos() {
const w2 = this.w;
w2.config.annotations.images.map((s) => {
this.addImage(s);
});
}
drawTextAnnos() {
const w2 = this.w;
w2.config.annotations.texts.map((t) => {
this.addText(t);
});
}
/**
* @param {Record<string, any>} anno
* @param {Element} parent
* @param {number} index
*/
addXaxisAnnotation(anno, parent, index) {
this.xAxisAnnotations.addXaxisAnnotation(anno, parent, index);
}
/**
* @param {Record<string, any>} anno
* @param {Element} parent
* @param {number} index
*/
addYaxisAnnotation(anno, parent, index) {
this.yAxisAnnotations.addYaxisAnnotation(anno, parent, index);
}
/**
* @param {Record<string, any>} anno
* @param {Element} parent
* @param {number} index
*/
addPointAnnotation(anno, parent, index) {
this.pointsAnnotations.addPointAnnotation(anno, parent, index);
}
/**
* @param {Record<string, any>} params
*/
addText(params) {
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 w2 = this.w;
const elText = this.graphics.drawText({
x,
y,
text,
textAnchor: textAnchor || "start",
fontSize: fontSize || "12px",
fontWeight: fontWeight || "regular",
fontFamily: fontFamily || w2.config.chart.fontFamily,
foreColor: foreColor || w2.config.chart.foreColor,
cssClass: "apexcharts-text " + cssClass ? cssClass : ""
});
const parent = w2.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);
}
}
/**
* @param {Record<string, any>} params
*/
addImage(params) {
const w2 = this.w;
const {
path,
x = 0,
y = 0,
width = 20,
height = 20,
appendTo = ".apexcharts-svg"
} = params;
const img = w2.dom.Paper.image(path);
img.size(width, height).move(x, y);
const parent = w2.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
/**
* @param {Record<string, any>} params
* @param {boolean} pushToMemory
* @param {any} context
*/
addXaxisAnnotationExternal(params, pushToMemory, context) {
this.addAnnotationExternal({
params,
pushToMemory,
context,
type: "xaxis",
contextMethod: context.addXaxisAnnotation
});
return context;
}
/**
* @param {Record<string, any>} params
* @param {boolean} pushToMemory
* @param {any} context
*/
addYaxisAnnotationExternal(params, pushToMemory, context) {
this.addAnnotationExternal({
params,
pushToMemory,
context,
type: "yaxis",
contextMethod: context.addYaxisAnnotation
});
return context;
}
/**
* @param {Record<string, any>} params
* @param {boolean} pushToMemory
* @param {any} 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;
}
/** @param {{params: any, pushToMemory: any, context: any, type: any, contextMethod: any}} opts */
addAnnotationExternal({
params,
pushToMemory,
context,
type,
contextMethod
}) {
const me = context;
const w2 = me.w;
const parent = w2.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;
}
const axesAnnoLabel = w2.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) {
w2.globals.memory.methodsToExec.push({
context: me,
id: anno.id ? anno.id : Utils.randomId(),
method: contextMethod,
label: "addAnnotation",
params
});
}
return context;
}
/**
* Remove the shared point-annotation hover tooltip node.
*
* `hidePointTooltip` is wired only to the marker's `mouseleave`; if the marker
* is torn down while hovered (clearAnnotations / removeAnnotation / a redraw),
* that never fires and the tooltip is left `.apexcharts-active`. A stale active
* annotation tooltip then permanently suppresses the series tooltip (see the
* guard in Tooltip.drawTooltip added by b1369f5ab). Removing the node clears
* both the ghost box and the stale state; it is recreated on the next hover.
* @param {any} w
*/
_removeAnnotationTooltip(w2) {
const el = w2.dom.elWrap && w2.dom.elWrap.querySelector(".apexcharts-annotation-tooltip");
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
}
/**
* @param {import('../../types/internal').ChartContext} ctx
*/
clearAnnotations(ctx) {
const w2 = ctx.w;
this._removeAnnotationTooltip(w2);
const annos = w2.dom.baseEl.querySelectorAll(
".apexcharts-yaxis-annotations, .apexcharts-xaxis-annotations, .apexcharts-point-annotations"
);
for (let i = w2.globals.memory.methodsToExec.length - 1; i >= 0; i--) {
if (w2.globals.memory.methodsToExec[i].label === "addText" || w2.globals.memory.methodsToExec[i].label === "addAnnotation") {
w2.globals.memory.methodsToExec.splice(i, 1);
}
}
Array.prototype.forEach.call(annos, (a) => {
while (a.firstChild) {
a.removeChild(a.firstChild);
}
});
}
/**
* @param {import('../../types/internal').ChartContext} ctx
* @param {string} id
*/
removeAnnotation(ctx, id) {
const w2 = ctx.w;
this._removeAnnotationTooltip(w2);
const annos = w2.dom.baseEl.querySelectorAll(`.${id}`);
if (annos) {
w2.globals.memory.methodsToExec.map((m2, i) => {
if (m2.id === id) {
w2.globals.memory.methodsToExec.splice(i, 1);
}
});
Object.keys(w2.config.annotations).forEach((key) => {
const annotationArray = w2.config.annotations[key];
if (Array.isArray(annotationArray)) {
w2.config.annotations[key] = annotationArray.filter((m2) => m2.id !== id);
}
});
Array.prototype.forEach.call(annos, (a) => {
a.parentElement.removeChild(a);
});
}
}
}
_core__default.registerFeatures({ annotations: Annotations });
class KeyboardNavigation {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
this.seriesIndex = 0;
this.dataPointIndex = 0;
this.active = false;
this._tooltipDismissed = false;
this._focusedEl = null;
this._hoveredBarEl = null;
this._enlargedScatterMarker = null;
this._onKeyDown = this._onKeyDown.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onBlur = this._onBlur.bind(this);
this._onLegendClick = this._onLegendClick.bind(this);
this._onPointerDown = this._onPointerDown.bind(this);
this._lastPointerDownAt = 0;
}
// ─── Public API ───────────────────────────────────────────────────────────
/**
* Called after the chart and tooltip have been fully rendered.
* Attaches event listeners and makes the SVG keyboard-focusable.
*/
init() {
const w2 = this.w;
const svgEl = w2.dom.Paper.node;
if (!svgEl) return;
svgEl.setAttribute("tabindex", "0");
svgEl.addEventListener("focus", this._onFocus);
svgEl.addEventListener("blur", this._onBlur);
svgEl.addEventListener("mousedown", this._onPointerDown, { capture: true });
svgEl.addEventListener("pointerdown", this._onPointerDown, {
capture: true
});
svgEl.addEventListener("touchstart", this._onPointerDown, {
capture: true,
passive: true
});
svgEl.addEventListener("keydown", this._onKeyDown, { passive: false });
this.ctx.events.addEventListener("legendClick", this._onLegendClick);
}
/**
* Removes all event listeners. Called from chart.destroy().
*/
destroy() {
const w2 = this.w;
const svgEl = w2.dom.Paper && w2.dom.Paper.node;
this.ctx.events.removeEventListener("legendClick", this._onLegendClick);
if (!svgEl) return;
svgEl.removeEventListener("focus", this._onFocus);
svgEl.removeEventListener("blur", this._onBlur);
svgEl.removeEventListener("keydown", this._onKeyDown);
svgEl.removeEventListener(
"mousedown",
this._onPointerDown,
/** @type {any} */
{ capture: true }
);
svgEl.removeEventListener(
"pointerdown",
this._onPointerDown,
/** @type {any} */
{ capture: true }
);
svgEl.removeEventListener(
"touchstart",
this._onPointerDown,
/** @type {any} */
{ capture: true }
);
}
// Records the timestamp of the most recent pointer-down inside the SVG.
// `_onFocus` reads this to distinguish keyboard-driven focus (no recent
// pointer activity) from mouse-driven focus (pointer event within the
// last 100 ms). Stays a no-op for keyboard users.
_onPointerDown() {
this._lastPointerDownAt = Date.now();
}
/**
* Called from Events.js keydown handler. Navigation keys are already handled
* by the direct SVG listener (which can call preventDefault). This entry
* point is intentionally a no-op — Events.js still fires the public keyDown
* callback and fireEvent('keydown') independently.
* @param {Event} _e
*/
handleKey(_e) {
}
// ─── Focus / blur ─────────────────────────────────────────────────────────
_onFocus() {
if (!this._isNavEnabled()) return;
if (Date.now() - this._lastPointerDownAt < 100) {
return;
}
this.active = true;
this._clampCursor();
this._snapToVisibleRange();
this._showCurrentPoint();
}
_onBlur() {
this.active = false;
this._tooltipDismissed = false;
this._hideFocus();
}
// Called when the user clicks a legend item (collapse/expand a series).
// Hide the keyboard-nav tooltip — the chart is about to re-render and the
// current position may no longer be valid.
_onLegendClick() {
if (!this.active) return;
this.active = false;
this._hideFocus();
}
// ─── Key handler ──────────────────────────────────────────────────────────
/**
* @param {KeyboardEvent} e
*/
_onKeyDown(e) {
var _a, _b, _c;
if (!this._isNavEnabled() || !this.active) return;
if (e.shiftKey && (e.key === "ArrowRight" || e.key === "ArrowLeft") && this._canPan()) {
e.preventDefault();
this._panBy(e.key === "ArrowRight" ? 1 : -1);
return;
}
switch (e.key) {
case "ArrowRight":
e.preventDefault();
this._move(0, 1);
break;
case "ArrowLeft":
e.preventDefault();
this._move(0, -1);
break;
case "ArrowUp":
e.preventDefault();
this._move(-1, 0);
break;
case "ArrowDown":
e.preventDefault();
this._move(1, 0);
break;
case "Home":
e.preventDefault();
this.dataPointIndex = 0;
this._skipNullForward();
this._showCurrentPoint();
break;
case "End":
e.preventDefault();
this.dataPointIndex = this._getDataPointCount(this.seriesIndex) - 1;
this._skipNullBackward();
this._showCurrentPoint();
break;
case "Enter":
case " ":
e.preventDefault();
this._fireClick();
break;
case "+":
case "=":
if (this._canZoom()) {
e.preventDefault();
(_a = this.ctx.toolbar) == null ? void 0 : _a.handleZoomIn();
this._announce("Zoomed in");
}
break;
case "-":
case "_":
if (this._canZoom()) {
e.preventDefault();
(_b = this.ctx.toolbar) == null ? void 0 : _b.handleZoomOut();
this._announce("Zoomed out");
}
break;
case "0":
if (this._canZoom() && this.w.interact.zoomed) {
e.preventDefault();
(_c = this.ctx.toolbar) == null ? void 0 : _c.handleZoomReset();
this._announce("Zoom reset");
}
break;
case "Escape":
e.preventDefault();
if (!this._tooltipDismissed) {
this._tooltipDismissed = true;
this._hideFocus();
} else {
this.active = false;
this._tooltipDismissed = false;
this._hideFocus();
}
break;
}
}
// ─── Zoom / pan (keyboard alternatives for drag gestures) ─────────────────
_canZoom() {
const w2 = this.w;
return Boolean(
w2.globals.axisCharts && w2.config.chart.zoom && w2.config.chart.zoom.enabled
);
}
_canPan() {
return this._canZoom();
}
/**
* Shift the visible x-range by ~10% in the given direction.
* @param {number} direction +1 = right, -1 = left
*/
_panBy(direction) {
const w2 = this.w;
const toolbar = this.ctx.toolbar;
if (!toolbar) return;
const minX = Number(w2.globals.minX);
const maxX = Number(w2.globals.maxX);
if (!isFinite(minX) || !isFinite(maxX) || minX === maxX) return;
const span = maxX - minX;
const step = span * 0.1 * direction;
toolbar.zoomUpdateOptions(minX + step, maxX + step);
this._announce(direction > 0 ? "Panned right" : "Panned left");
}
// ─── Navigation ───────────────────────────────────────────────────────────
/**
* @param {number} dSeries
* @param {number} dPoint
*/
_move(dSeries, dPoint) {
const w2 = this.w;
const wrapAround = w2.config.chart.accessibility.keyboard.navigation.wrapAround;
if (dSeries !== 0) {
const ttCtx = this.w.globals.tooltip;
if (ttCtx && ttCtx.tConfig && ttCtx.tConfig.shared) {
const j = this.dataPointIndex;
const isActuallyShared = ttCtx.tooltipUtil && ttCtx.tooltipUtil.isXoverlap(j) && ttCtx.tooltipUtil.isInitialSeriesSameLen();
if (isActuallyShared) return;
}
const total = this._getSeriesCount();
let si = this.seriesIndex + dSeries;
let attempts = 0;
while (attempts < total) {
if (si < 0) si = wrapAround ? total - 1 : 0;
if (si >= total) si = wrapAround ? 0 : total - 1;
if (!w2.globals.collapsedSeriesIndices.includes(si)) break;
si += dSeries;
attempts++;
}
this.seriesIndex = si;
const dpCount = this._getDataPointCount(si);
if (this.dataPointIndex >= dpCount) {
this.dataPointIndex = dpCount - 1;
}
}
if (dPoint !== 0) {
const dpCount = this._getDataPointCount(this.seriesIndex);
let di = this.dataPointIndex + dPoint;
if (di < 0) di = wrapAround ? dpCount - 1 : 0;
if (di >= dpCount) di = wrapAround ? 0 : dpCount - 1;
this.dataPointIndex = di;
if (dPoint > 0) {
this._skipNullForward();
} else {
this._skipNullBackward();
}
if (!this._isDataPointVisible(this.seriesIndex, this.dataPointIndex)) {
this._snapToVisibleRangeInDirection(dPoint);
}
}
this._showCurrentPoint();
}
/** Advance dataPointIndex forward past any nulls */
_skipNullForward() {
const w2 = this.w;
const si = this.seriesIndex;
const dpCount = this._getDataPointCount(si);
let di = this.dataPointIndex;
let attempts = 0;
if (!Array.isArray(w2.seriesData.series[si])) return;
while (attempts < dpCount && w2.seriesData.series[si][di] === null) {
di = (di + 1) % dpCount;
attempts++;
}
this.dataPointIndex = di;
}
/** Retreat dataPointIndex backward past any nulls */
_skipNullBackward() {
const w2 = this.w;
const si = this.seriesIndex;
const dpCount = this._getDataPointCount(si);
let di = this.dataPointIndex;
let attempts = 0;
if (!Array.isArray(w2.seriesData.series[si])) return;
while (attempts < dpCount && w2.seriesData.series[si][di] === null) {
di = (di - 1 + dpCount) % dpCount;
attempts++;
}
this.dataPointIndex = di;
}
// ─── Display ──────────────────────────────────────────────────────────────
_showCurrentPoint() {
const { seriesIndex: i, dataPointIndex: j } = this;
const w2 = this.w;
const ttCtx = w2.globals.tooltip;
if (!ttCtx || !ttCtx.ttItems) return;
w2.interact.capturedSeriesIndex = i;
w2.interact.capturedDataPointIndex = j;
this._applyFocusClass(i, j);
this._showTooltip(
i,
j,
/** @type {any} */
ttCtx
);
}
_hideFocus() {
const w2 = this.w;
const ttCtx = (
/** @type {any} */
w2.globals.tooltip
);
this._removeFocusClass();
this._leaveHoveredBar();
if (!ttCtx) return;
if (ttCtx.marker) {
ttCtx.marker.resetPointsSize();
}
this._enlargedScatterMarker = null;
const tooltipEl = ttCtx.getElTooltip();
if (tooltipEl) {
tooltipEl.classList.remove("apexcharts-active");
if (w2.config.chart.accessibility.enabled && w2.config.chart.accessibility.announcements.enabled) {
tooltipEl.setAttribute("aria-hidden", "true");
}
}
w2.dom.baseEl.classList.remove("apexcharts-tooltip-active");
const xcrosshairs = ttCtx.getElXCrosshairs();
if (xcrosshairs) xcrosshairs.classList.remove("apexcharts-active");
}
// ─── Tooltip display per chart type ───────────────────────────────────────
/**
* @param {number} i
* @param {number} j
* @param {import('../tooltip/Tooltip').default} ttCtx
*/
_showTooltip(i, j, ttCtx) {
const w2 = this.w;
const type = w2.config.chart.type;
const tooltipEl = ttCtx.getElTooltip();
if (!tooltipEl) return;
const cachedDims = ttCtx.getCachedDimensions();
ttCtx.tooltipRect = {
x: 0,
y: 0,
ttWidth: cachedDims.ttWidth || 0,
ttHeight: cachedDims.ttHeight || 0
};
this._setSyntheticEvent(i, j, ttCtx);
w2.dom.baseEl.classList.add("apexcharts-tooltip-active");
tooltipEl.classList.add("apexcharts-active");
if (w2.config.chart.accessibility.enabled && w2.config.chart.accessibility.announcements.enabled) {
tooltipEl.removeAttribute("aria-hidden");
}
if (type === "pie" || type === "donut" || type === "polarArea") {
this._showTooltipNonAxis(i, j, ttCtx, tooltipEl);
} else if (type === "radialBar") {
this._showTooltipRadialBar(i, j, ttCtx, tooltipEl);
} else if (type === "heatmap" || type === "treemap") {
this._showTooltipHeatTree(i, j, ttCtx, tooltipEl, type);
} else if (type === "bar" || type === "candlestick" || type === "boxPlot" || type === "violin" || type === "rangeBar") {
this._showTooltipBar(i, j, ttCtx);
} else {
this._showTooltipAxisLine(i, j, ttCtx);
}
}
/**
* Set ttCtx.e to a synthetic mouse-event-like object whose clientX/Y point
* to the centre of the current data-point element. This ensures that any
* positioning helper that reads ttCtx.e (followCursor path in moveTooltip,
* moveStickyTooltipOverBars, moveDynamicPointsOnHover, etc.) gets valid
* coordinates rather than crashing on undefined.
*
* For chart types that don't have a concrete SVG element per data point
* (pie, radialBar) we fall back to the SVG centre.
* @param {number} i
* @param {number} j
* @param {import('../tooltip/Tooltip').default} ttCtx
*/
_setSyntheticEvent(i, j, ttCtx) {
const w2 = this.w;
const type = w2.config.chart.type;
let clientX = 0;
let clientY = 0;
const el = this._getFocusableElement(i, j);
if (el) {
const rect = el.getBoundingClientRect();
clientX = rect.left + rect.width / 2;
clientY = rect.top + rect.height / 2;
} else if (w2.globals.pointsArray && w2.globals.pointsArray[i] && w2.globals.pointsArray[i][j]) {
const pt = w2.globals.pointsArray[i][j];
const elGrid = ttCtx.getElGrid && ttCtx.getElGrid();
if (elGrid) {
const gridRect = elGrid.getBoundingClientRect();
clientX = gridRect.left + (pt[0] || 0);
clientY = gridRect.top + (pt[1] || 0);
}
} else {
const svgEl = w2.dom.Paper && w2.dom.Paper.node;
if (svgEl) {
const svgRect = svgEl.getBoundingClientRect();
clientX = svgRect.left + svgRect.width / 2;
clientY = svgRect.top + svgRect.height / 2;
}
}
if (type === "line" || type === "area" || type === "rangeArea" || type === "scatter" || type === "bubble" || type === "radar") {
if (w2.globals.pointsArray && w2.globals.pointsArray[i] && w2.globals.pointsArray[i][j]) {
const pt = w2.globals.pointsArray[i][j];
const elGrid = ttCtx.getElGrid && ttCtx.getElGrid();
if (elGrid) {
const gridRect = elGrid.getBoundingClientRect();
clientX = gridRect.left + (pt[0] || 0);
clientY = gridRect.top + (pt[1] || 0);
}
}
}
ttCtx.e = { type: "mousemove", clientX, clientY };
}
/**
* bar / column / candlestick / boxPlot / rangeBar
* @param {number} i
* @param {number} j
* @param {import('../tooltip/Tooltip').default} ttCtx
*/
_showTooltipBar(i, j, ttCtx) {
var _a, _b, _c, _d;
const w2 = this.w;
const shared = ttCtx.tConfig.shared && (ttCtx.tooltipUtil.isXoverlap(j) || w2.globals.isBarHorizontal) && ttCtx.tooltipUtil.isInitialSeriesSameLen();
const rangeData = (
/** @type {any} */
(_d = (_c = (_b = (_a = w2.rangeData.seriesRange) == null ? void 0 : _a[i]) == null ? void 0 : _b[j]) == null ? void 0 : _c.y) == null ? void 0 : _d[0]
);
ttCtx.tooltipLabels.drawSeriesTexts(__spreadProps(__spreadValues(__spreadValues({
ttItems: ttCtx.ttItems,
i,
j
}, (rangeData == null ? void 0 : rangeData.y1) !== void 0 && { y1: rangeData.y1 }), (rangeData == null ? void 0 : rangeData.y2) !== void 0 && { y2: rangeData.y2 }), {
shared
}));
const parent = `.apexcharts-series[data\\:realIndex='${i}']`;
const elPath = w2.dom.Paper.findOne(
`${parent} path[j='${j}'], ${parent} circle[j='${j}'], ${parent} rect[j='${j}']`
);
if (elPath) {
this._leaveHoveredBar();
const graphics = new Graphics(this.w, this.ctx);
graphics.pathMouseEnter(elPath, null);
this._hoveredBarEl = elPath;
}
if (w2.globals.isBarHorizontal) {
const barDomEl = elPath && elPath.node;
if (barDomEl) {
const wrapRect = w2.dom.elWrap.getBoundingClientRect();
const barRect = barDomEl.getBoundingClientRect();
const barCx = barRect.left - wrapRect.left;
const barCy = barRect.top - wrapRect.top;
const bh = barRect.height;
const bw = barRect.width;
const ttWidth = ttCtx.tooltipRect.ttWidth || 0;
const ttHeight = ttCtx.tooltipRect.ttHeight || 0;
const y = barCy + bh / 2 - ttHeight / 2;
let x = barCx + bw;
const baselineX = ttCtx.xyRatios && ttCtx.xyRatios.baseLineInvertedY != null ? ttCtx.xyRatios.baseLineInvertedY : wrapRect.width / 2;
if (barCx < baselineX) {
x = barCx - ttWidth;
}
const tooltipEl = ttCtx.getElTooltip();
if (tooltipEl) {
tooltipEl.style.left = x + "px";
tooltipEl.style.top = y + "px";
}
}
} else {
ttCtx.tooltipPosition.moveStickyTooltipOverBars(j, i);
}
}
/**
* line / area / scatter / bubble / radar / rangeArea
* @param {number} i
* @param {number} j
* @param {import('../tooltip/Tooltip').default} ttCtx
*/
_showTooltipAxisLine(i, j, ttCtx) {
const w2 = this.w;
const type = w2.config.chart.type;
const sharedConfigured = ttCtx.tConfig.shared;
const shared = sharedConfigured && ttCtx.tooltipUtil.isXoverlap(j) && ttCtx.tooltipUtil.isInitialSeriesSameLen();
ttCtx.tooltipLabels.drawSeriesTexts({
ttItems: ttCtx.ttItems,
i,
j,
shared
});
const isScatterLike = type === "scatter" || type === "bubble";
const hasVisibleMarkers = w2.globals.markers.largestSize > 0 && !w2.globals.markers.batched;
if (isScatterLike) {
this._showScatterBubblePoint(i, j, ttCtx);
} else if (hasVisibleMarkers) {
if (shared) {
ttCtx.marker.enlargePoints(j);
} else {
ttCtx.tooltipPosition.moveDynamicPointOnHover(j, i);
}
} else if (shared) {
ttCtx.tooltipPosition.moveDynamicPointsOnHover(j);
} else {
ttCtx.tooltipPosition.moveDynamicPointOnHover(j, i);
}
}
/**
* Scatter / bubble: find the specific marker element for (seriesIndex i,
* dataPointIndex j), resize only that element, and position the tooltip at
* its coordinates — mirroring what Position.moveMarkers does for mouse hover.
*
* Unlike enlargePoints(j) which queries ALL series for rel===j (causing
* multiple bubbles to enlarge and tooltip to land on the wrong one), this
* method queries by both series index AND data-point index for precision.
* @param {number} i
* @param {number} j
* @param {import('../tooltip/Tooltip').default} ttCtx
*/
_showScatterBubblePoint(i, j, ttCtx) {
const baseEl = this.w.dom.baseEl;
if (this._enlargedScatterMarker) {
ttCtx.marker.oldPointSize(this._enlargedScatterMarker);
this._enlargedScatterMarker = null;
}
const seriesEl = baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${i}']`
);
if (!seriesEl) return;
const markerEl = seriesEl.querySelector(`.apexcharts-marker[rel='${j}']`);
if (!markerEl) return;
ttCtx.marker.enlargeCurrentPoint(j, markerEl);
this._enlargedScatterMarker = markerEl;
}
/**
* pie / donut / polarArea
* @param {number} i
* @param {number} j
* @param {import('../tooltip/Tooltip').default} ttCtx
* @param {HTMLElement} tooltipEl
*/
_showTooltipNonAxis(i, j, ttCtx, tooltipEl) {
const w2 = this.w;
ttCtx.tooltipLabels.drawSeriesTexts({
ttItems: ttCtx.ttItems,
i: j,
shared: false
});
const tooltipBound = tooltipEl.getBoundingClientRect();
const ttWidth = tooltipBound.width || ttCtx.tooltipRect.ttWidth || 0;
const ttHeight = tooltipBound.height || ttCtx.tooltipRect.ttHeight || 0;
const sliceEl = w2.dom.baseEl.querySelector(`.apexcharts-pie-area[j='${j}']`);
const anchor = ttCtx.getSliceAnchor(sliceEl);
if (anchor) {
tooltipEl.style.left = anchor.x - ttWidth / 2 + "px";
tooltipEl.style.top = anchor.y - ttHeight - 10 + "px";
}
}
/**
* radialBar — one ring per series, single value each
* @param {number} i
* @param {any} _j
* @param {import('../tooltip/Tooltip').default} ttCtx
* @param {HTMLElement} tooltipEl
*/
_showTooltipRadialBar(i, _j, ttCtx, tooltipEl) {
var _a;
const w2 = this.w;
ttCtx.tooltipLabels.drawSeriesTexts({
ttItems: ttCtx.ttItems,
i,
shared: false
});
const { ttWidth = 0, ttHeight = 0 } = ttCtx.getCachedDimensions();
const arcEl = w2.dom.baseEl.querySelector(
`.apexcharts-radialbar-series[data\\:realIndex='${i}'] path`
);
if (arcEl) {
const angle = parseFloat((_a = arcEl.getAttribute("data:angle")) != null ? _a : "") || 0;
const initialAngle = w2.config.plotOptions.radialBar.startAngle || 0;
const midAngle = initialAngle + angle / 2;
const centerX = w2.layout.gridWidth / 2;
const centerY = w2.layout.gridHeight / 2;
const radialSize = w2.globals.radialSize || Math.min(w2.layout.gridWidth, w2.layout.gridHeight) / 2;
const seriesCount = w2.seriesData.series.length;
const trackSize = radialSize / Math.max(seriesCount, 1);
const outerRadius = radialSize - i * trackSize;
const innerRadius = outerRadius - trackSize;
const ringRadius = (outerRadius + innerRadius) / 2;
const centroid = Utils.polarToCartesian(
centerX,
centerY,
ringRadius,
midAngle
);
const x = centroid.x + (w2.layout.translateX || 0);
const y = centroid.y + (w2.layout.translateY || 0);
tooltipEl.style.left = x - ttWidth / 2 + "px";
tooltipEl.style.top = y - ttHeight - 10 + "px";
}
}
/**
* heatmap / treemap — position tooltip using element bounding rect
* @param {number} i
* @param {number} j
* @param {import('../tooltip/Tooltip').default} ttCtx
* @param {HTMLElement} tooltipEl
* @param {string} type
*/
_showTooltipHeatTree(i, j, ttCtx, tooltipEl, type) {
var _a, _b;
const w2 = this.w;
ttCtx.tooltipLabels.drawSeriesTexts({
ttItems: ttCtx.ttItems,
i,
j,
shared: false
});
const tooltipRect = tooltipEl.getBoundingClientRect();
const ttWidth = tooltipRect.width || ttCtx.tooltipRect.ttWidth || 0;
const ttHeight = tooltipRect.height || ttCtx.tooltipRect.ttHeight || 0;
const rectClass = type === "heatmap" ? "apexcharts-heatmap-rect" : "apexcharts-treemap-rect";
const cell = w2.dom.baseEl.querySelector(`.${rectClass}[i='${i}'][j='${j}']`);
if (cell) {
const wrapRect = w2.dom.elWrap.getBoundingClientRect();
const cellRect = cell.getBoundingClientRect();
const cellCx = cellRect.left - wrapRect.left;
const cellCy = cellRect.top - wrapRect.top;
const cellWidth = cellRect.width;
const cellHeight = cellRect.height;
const cx = parseFloat((_a = cell.getAttribute("cx")) != null ? _a : "");
const cellWidthAttr = parseFloat((_b = cell.getAttribute("width")) != null ? _b : "");
ttCtx.tooltipPosition.moveXCrosshairs(cx + cellWidthAttr / 2);
let x = cellCx + cellWidth + ttWidth / 2;
const y = cellCy + cellHeight / 2 - ttHeight / 2;
if (cellCx + cellWidth > w2.layout.gridWidth / 2) {
x = cellCx - ttWidth / 2;
}
tooltipEl.style.left = x + "px";
tooltipEl.style.top = y + "px";
}
}
// ─── Focus class management ───────────────────────────────────────────────
/**
* @param {number} i
* @param {number} j
*/
_applyFocusClass(i, j) {
this._removeFocusClass();
const el = this._getFocusableElement(i, j) || this._getBatchedFocusEl(i);
if (el) {
el.classList.add("apexcharts-keyboard-focused");
el.setAttribute("role", "img");
const label = this._buildPointLabel(i, j);
if (label) el.setAttribute("aria-label", label);
this._focusedEl = el;
}
}
/**
* A batched series has no `.apexcharts-marker[rel]` node to carry the focus
* ring and aria-label, so the focus lands on the tooltip's own marker for
* that series instead: `_showTooltip` moves it onto the focused point in this
* same task, so it is the element the reader sees highlighted. Only used when
* batching is on, since with per-point nodes the exact node is better.
* @param {number} i
* @returns {Element | null}
*/
_getBatchedFocusEl(i) {
if (!this.w.globals.markers.batched) return null;
return this.w.dom.baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${i}'] .apexcharts-series-markers path`
);
}
_removeFocusClass() {
if (this._focusedEl) {
this._focusedEl.classList.remove("apexcharts-keyboard-focused");
this._focusedEl.removeAttribute("role");
this._focusedEl.removeAttribute("aria-label");
this._focusedEl = null;
}
}
/**
* Build an accessible label for the data point at (i, j) using the same
* formatters the visible tooltip / axis labels use, so SR output matches
* the visual presentation.
* @param {number} i
* @param {number} j
* @returns {string}
*/
_buildPointLabel(i, j) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const w2 = this.w;
const type = w2.config.chart.type;
const seriesNames = w2.seriesData.seriesNames || [];
const series = w2.seriesData.series || [];
if (type === "pie" || type === "donut" || type === "polarArea") {
const sliceLabel = (_b = ((_a = w2.labelData) == null ? void 0 : _a.labels) && w2.labelData.labels[j]) != null ? _b : "";
const value = Array.isArray(series) ? series[j] : "";
return sliceLabel ? `${sliceLabel}: ${value}` : `${value}`;
}
if (type === "radialBar") {
const seriesName2 = seriesNames[i] || `Series ${i + 1}`;
const value = Array.isArray(series) ? series[i] : "";
return `${seriesName2}: ${value}`;
}
const seriesName = seriesNames[i] || `Series ${i + 1}`;
const row = Array.isArray(series[i]) ? series[i] : [];
const rawValue = row[j];
let formattedValue = rawValue == null ? "" : String(rawValue);
const yFormatter = (_d = (_c = w2.formatters) == null ? void 0 : _c.yLabelFormatters) == null ? void 0 : _d[i];
if (typeof yFormatter === "function") {
try {
formattedValue = yFormatter(rawValue, {
seriesIndex: i,
dataPointIndex: j,
w: w2
});
} catch (e) {
}
}
let category = "";
const categoryLabels = (_e = w2.labelData) == null ? void 0 : _e.categoryLabels;
const seriesX = (_g = (_f = w2.seriesData) == null ? void 0 : _f.seriesX) == null ? void 0 : _g[i];
if (Array.isArray(categoryLabels) && categoryLabels[j] != null) {
category = String(categoryLabels[j]);
} else if (Array.isArray(seriesX) && seriesX[j] != null) {
const xFormatter = (_h = w2.formatters) == null ? void 0 : _h.xLabelFormatter;
if (typeof xFormatter === "function") {
try {
category = String(
xFormatter(seriesX[j], { seriesIndex: i, dataPointIndex: j, w: w2 })
);
} catch (e) {
category = String(seriesX[j]);
}
} else {
category = String(seriesX[j]);
}
}
return category ? `${seriesName}: ${formattedValue}, ${category}` : `${seriesName}: ${formattedValue}`;
}
_leaveHoveredBar() {
if (this._hoveredBarEl) {
const graphics = new Graphics(this.w, this.ctx);
graphics.pathMouseLeave(this._hoveredBarEl, null);
this._hoveredBarEl = null;
}
}
/**
* @param {number} i
* @param {number} j
*/
_getFocusableElement(i, j) {
const w2 = this.w;
const type = w2.config.chart.type;
const baseEl = w2.dom.baseEl;
if (type === "pie" || type === "donut" || type === "polarArea") {
return baseEl.querySelector(`.apexcharts-pie-area[j='${j}']`);
}
if (type === "heatmap") {
return baseEl.querySelector(
`.apexcharts-heatmap-rect[i='${i}'][j='${j}']`
);
}
if (type === "treemap") {
return baseEl.querySelector(
`.apexcharts-treemap-rect[i='${i}'][j='${j}']`
);
}
if (type === "radialBar") {
return baseEl.querySelector(
`.apexcharts-radialbar-series[data\\:realIndex='${i}'] path`
);
}
if (type === "bar" || type === "candlestick" || type === "boxPlot" || type === "violin" || type === "rangeBar") {
return baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${i}'] path[j='${j}']`
);
}
const marker = baseEl.querySelector(
`.apexcharts-series[data\\:realIndex='${i}'] .apexcharts-marker[rel='${j}']`
);
return marker || null;
}
// ─── Click / Enter ────────────────────────────────────────────────────────
_fireClick() {
const w2 = this.w;
const ttCtx = w2.globals.tooltip;
if (!ttCtx) return;
const syntheticEvent = {
type: "mouseup",
clientX: 0,
clientY: 0
};
ttCtx.markerClick(syntheticEvent, this.seriesIndex, this.dataPointIndex);
}
// ─── Helpers ──────────────────────────────────────────────────────────────
_isNavEnabled() {
const a11y = this.w.config.chart.accessibility;
return a11y.enabled && a11y.keyboard.enabled && a11y.keyboard.navigation.enabled;
}
_getSeriesCount() {
const w2 = this.w;
const type = w2.config.chart.type;
if (type === "pie" || type === "donut" || type === "polarArea") {
return 1;
}
return w2.seriesData.series.length;
}
/**
* @param {number} si
*/
_getDataPointCount(si) {
const w2 = this.w;
const type = w2.config.chart.type;
if (type === "pie" || type === "donut" || type === "polarArea") {
return w2.seriesData.series.length;
}
const series = w2.seriesData.series;
return series[si] && Array.isArray(series[si]) ? series[si].length : 0;
}
_clampCursor() {
const seriesCount = this._getSeriesCount();
if (this.seriesIndex >= seriesCount) this.seriesIndex = seriesCount - 1;
if (this.seriesIndex < 0) this.seriesIndex = 0;
const dpCount = this._getDataPointCount(this.seriesIndex);
if (this.dataPointIndex >= dpCount) this.dataPointIndex = dpCount - 1;
if (this.dataPointIndex < 0) this.dataPointIndex = 0;
}
/**
* When the chart is zoomed in, the current dataPointIndex may point to a
* data point that is outside the visible viewport. Snap the cursor to the
* first data point whose x-value falls within [minX, maxX].
*
* Only adjusts when w.seriesData.seriesX is populated (numeric/datetime axes).
* Category-only charts (seriesX entries are strings or auto-indices) are
* unaffected — all points are always visible.
*/
_snapToVisibleRange() {
const w2 = this.w;
const gl = w2.globals;
const si = this.seriesIndex;
if (!w2.interact.zoomed) return;
const seriesX = w2.seriesData.seriesX && w2.seriesData.seriesX[si];
if (!seriesX || !seriesX.length) return;
const minX = gl.minX;
const maxX = gl.maxX;
if (minX === void 0 || maxX === void 0) return;
const currentX = seriesX[this.dataPointIndex];
if (currentX >= minX && currentX <= maxX) return;
const dpCount = seriesX.length;
for (let di = 0; di < dpCount; di++) {
if (seriesX[di] >= minX && seriesX[di] <= maxX) {
this.dataPointIndex = di;
return;
}
}
}
/**
* Snap to the nearest visible data point in the given navigation direction.
* direction > 0 → find the first visible point (left boundary of zoomed range)
* direction < 0 → find the last visible point (right boundary of zoomed range)
* @param {number} direction
*/
_snapToVisibleRangeInDirection(direction) {
const w2 = this.w;
const gl = w2.globals;
const si = this.seriesIndex;
const seriesX = w2.seriesData.seriesX && w2.seriesData.seriesX[si];
if (!seriesX || !seriesX.length) return;
const minX = gl.minX;
const maxX = gl.maxX;
if (minX === void 0 || maxX === void 0) return;
const dpCount = seriesX.length;
if (direction >= 0) {
for (let di = 0; di < dpCount; di++) {
if (seriesX[di] >= minX && seriesX[di] <= maxX) {
this.dataPointIndex = di;
return;
}
}
} else {
for (let di = dpCount - 1; di >= 0; di--) {
if (seriesX[di] >= minX && seriesX[di] <= maxX) {
this.dataPointIndex = di;
return;
}
}
}
}
/**
* Check whether the data point at (si, di) is within the current visible
* x-axis range. Used to skip out-of-viewport points during keyboard nav.
* @param {number} si
* @param {number} di
*/
_isDataPointVisible(si, di) {
const w2 = this.w;
const gl = w2.globals;
if (!w2.interact.zoomed) return true;
const seriesX = w2.seriesData.seriesX && w2.seriesData.seriesX[si];
if (!seriesX) return true;
const x = seriesX[di];
if (x === void 0) return true;
return x >= gl.minX && x <= gl.maxX;
}
/**
* Push a short status message to the visually-hidden aria-live region so
* screen readers announce zoom / pan / reset events that have no inherent
* tooltip update. Silently no-op if the region is missing or announcements
* are disabled.
* @param {string} message
*/
_announce(message) {
const w2 = this.w;
if (!w2.config.chart.accessibility.announcements.enabled) return;
const baseEl = w2.dom.baseEl;
if (!baseEl) return;
const region = baseEl.querySelector(".apexcharts-sr-status");
if (!region) return;
region.textContent = "";
setTimeout(() => {
region.textContent = message;
}, 0);
}
}
_core__default.registerFeatures({ keyboardNavigation: KeyboardNavigation });
function gridDivideRect(bbox, count) {
if (!(count > 0)) return [];
if (count === 1) return [__spreadValues({}, bbox)];
const horizontal = bbox.width >= bbox.height;
const rowExtent = horizontal ? bbox.width : bbox.height;
const colExtent = horizontal ? bbox.height : bbox.width;
const ratio = colExtent > 0 ? rowExtent / colExtent : count;
let rows = Math.max(1, Math.ceil(Math.sqrt(ratio * count)));
if (rows > count) rows = count;
const baseCols = Math.floor(count / rows);
let remainder = count - baseCols * rows;
const cells = [];
const rowSize = rowExtent / rows;
let rowStart = 0;
for (let r = 0; r < rows; r++) {
const cols = baseCols + (remainder > 0 ? 1 : 0);
if (remainder > 0) remainder--;
const colSize = cols > 0 ? colExtent / cols : 0;
for (let c = 0; c < cols; c++) {
cells.push(
horizontal ? {
x: bbox.x + rowStart,
y: bbox.y + c * colSize,
width: rowSize,
height: colSize
} : {
x: bbox.x + c * colSize,
y: bbox.y + rowStart,
width: colSize,
height: rowSize
}
);
}
rowStart += rowSize;
}
return cells;
}
function gridDivideShape(bbox, count, intervalsAt) {
if (!(count > 0)) return [];
const horizontal = bbox.width >= bbox.height;
const rowExtent = horizontal ? bbox.width : bbox.height;
const colExtent = horizontal ? bbox.height : bbox.width;
const minorLo = horizontal ? bbox.y : bbox.x;
const minorHi = minorLo + colExtent;
const ratio = colExtent > 0 ? rowExtent / colExtent : count;
let rows = Math.max(
Math.ceil(Math.sqrt(ratio * count)),
Math.ceil(rowExtent / 16)
);
if (!(rows >= 1)) rows = 1;
if (rows > count) rows = count;
const baseCols = Math.floor(count / rows);
let remainder = count - baseCols * rows;
const cells = [];
const rowSize = rowExtent / rows;
let rowStart = horizontal ? bbox.x : bbox.y;
for (let r = 0; r < rows; r++) {
const cols = baseCols + (remainder > 0 ? 1 : 0);
if (remainder > 0) remainder--;
const spans = [];
const raw = intervalsAt(rowStart, rowStart + rowSize, horizontal);
if (Array.isArray(raw)) {
for (let s = 0; s < raw.length; s++) {
const iv = raw[s];
if (!iv) continue;
const lo = Math.max(minorLo, Math.min(iv[0], iv[1]));
const hi = Math.min(minorHi, Math.max(iv[0], iv[1]));
if (hi > lo) spans.push([lo, hi]);
}
}
if (!spans.length) spans.push([minorLo, minorHi]);
const totalLen = spans.reduce((a, s) => a + (s[1] - s[0]), 0);
const exact = spans.map(
(s) => totalLen > 0 ? (s[1] - s[0]) / totalLen * cols : cols / spans.length
);
const share = exact.map((v2) => Math.floor(v2));
let used = share.reduce((a, b2) => a + b2, 0);
const byFrac = exact.map((v2, i) => ({ i, frac: v2 - Math.floor(v2) })).sort((a, b2) => b2.frac - a.frac);
for (let k = 0; used < cols; k++, used++) {
share[byFrac[k % byFrac.length].i]++;
}
for (let s = 0; s < spans.length; s++) {
const n = share[s];
if (n <= 0) continue;
const lo = spans[s][0];
const colSize = (spans[s][1] - lo) / n;
for (let c = 0; c < n; c++) {
cells.push(
horizontal ? { x: rowStart, y: lo + c * colSize, width: rowSize, height: colSize } : { x: lo + c * colSize, y: rowStart, width: colSize, height: rowSize }
);
}
}
rowStart += rowSize;
}
return cells;
}
function hilbertIndex(x, y, minX, minY, maxX, maxY) {
let ix = maxX === minX ? 0 : Math.round(32767 * ((x - minX) / (maxX - minX)));
let iy = maxY === minY ? 0 : Math.round(32767 * ((y - minY) / (maxY - minY)));
let d = 0;
for (let s = 32768; s >= 1; s /= 2) {
const rx = (ix & s) > 0 ? 1 : 0;
const ry = (iy & s) > 0 ? 1 : 0;
d += s * s * (3 * rx ^ ry);
if (ry === 0) {
if (rx === 1) {
ix = s - 1 - ix;
iy = s - 1 - iy;
}
const t = ix;
ix = iy;
iy = t;
}
}
return d;
}
function sortByHilbert(items, getXY) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
const pts = items.map((it) => {
const [x, y] = getXY(it);
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
return [x, y];
});
return items.map((item, k) => ({
item,
d: hilbertIndex(pts[k][0], pts[k][1], minX, minY, maxX, maxY)
})).sort((a, b2) => a.d - b2.d).map((e) => e.item);
}
function parseColor(str) {
if (!str || typeof str !== "string") return null;
const s = str.trim();
if (s[0] === "#") {
const hex = s.slice(1);
if (hex.length === 3) {
return [
parseInt(hex[0] + hex[0], 16),
parseInt(hex[1] + hex[1], 16),
parseInt(hex[2] + hex[2], 16),
1
];
}
if (hex.length === 6 || hex.length === 8) {
return [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1
];
}
return null;
}
const m2 = s.match(/^rgba?\(([^)]+)\)$/i);
if (m2) {
const parts = m2[1].split(",").map((p2) => parseFloat(p2));
if (parts.length < 3 || parts.some((v2) => !isFinite(v2))) return null;
return [parts[0], parts[1], parts[2], parts.length > 3 ? parts[3] : 1];
}
return null;
}
function makeColorLerp(from, to) {
const a = parseColor(from);
const b2 = parseColor(to);
if (!a || !b2) return null;
return (t) => {
const r = Math.round(a[0] + (b2[0] - a[0]) * t);
const g2 = Math.round(a[1] + (b2[1] - a[1]) * t);
const bl = Math.round(a[2] + (b2[2] - a[2]) * t);
const al = a[3] + (b2[3] - a[3]) * t;
return al >= 1 ? `rgb(${r},${g2},${bl})` : `rgba(${r},${g2},${bl},${al})`;
};
}
function easeInOutCubic$1(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function runPieceTween({ pieces, duration, onPieceDone, onAllDone }) {
let cancelled = false;
const start = Date.now();
const dur = Math.max(1, duration);
const write = (p2, e) => {
const f = p2.from;
const t = p2.to;
const el = p2.el;
el.setAttribute("x", String(f.x + (t.x - f.x) * e));
el.setAttribute("y", String(f.y + (t.y - f.y) * e));
el.setAttribute("width", String(Math.max(0, f.width + (t.width - f.width) * e)));
el.setAttribute("height", String(Math.max(0, f.height + (t.height - f.height) * e)));
el.setAttribute("rx", String(Math.max(0, f.rx + (t.rx - f.rx) * e)));
if (p2.fill) el.setAttribute("fill", p2.fill(e));
else if (e >= 1 && p2.fillEnd) el.setAttribute("fill", p2.fillEnd);
};
const frame = () => {
if (cancelled) return;
const elapsed = Date.now() - start;
let live = false;
for (let k = 0; k < pieces.length; k++) {
const p2 = pieces[k];
if (
/** @type {any} */
p2._done
) continue;
const raw = (elapsed - p2.delay) / dur;
if (raw < 1) live = true;
if (raw <= 0) continue;
const t = Math.min(1, raw);
write(p2, easeInOutCubic$1(t));
if (t >= 1) {
p2._done = true;
if (onPieceDone) onPieceDone(p2);
}
}
if (live) BrowserAPIs.requestAnimationFrame(frame);
else if (onAllDone) onAllDone();
};
BrowserAPIs.requestAnimationFrame(frame);
return () => {
cancelled = true;
};
}
const BAR_FAMILY = /* @__PURE__ */ new Set(["bar", "funnel", "pyramid", "histogram"]);
const RADIAL_FAMILY = /* @__PURE__ */ new Set(["pie", "donut", "polarArea", "radialBar", "gauge"]);
const UNIT_FAMILY = /* @__PURE__ */ new Set(["unit", "waffle"]);
const PARTITION_FAMILY = /* @__PURE__ */ new Set(["treemap", "sunburst"]);
const SUMMARY_FAMILY = /* @__PURE__ */ new Set(["boxPlot", "violin"]);
const GHOST_FADE_FRACTION = 0.55;
const PIECE_BUDGET = 1500;
const PIECE_STAGGER_MAX = 300;
function familyOf(type) {
if (BAR_FAMILY.has(type)) return "bar";
if (RADIAL_FAMILY.has(type)) return "radial";
if (UNIT_FAMILY.has(type)) return "unit";
if (PARTITION_FAMILY.has(type)) return "partition";
if (SUMMARY_FAMILY.has(type)) return "summary";
return null;
}
class MorphTypeChange {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
this._snapshot = null;
this._ghost = null;
this._pieceLayer = null;
this._pieceCancel = 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;
if (ff === "partition" !== (tf === "partition")) {
return ff !== "unit" && tf !== "unit";
}
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 === "unit") {
if (newSeries.every((v2) => typeof v2 === "number")) return true;
return newSeries.every(
(s) => s && typeof s === "object" && Array.isArray(s.data)
);
}
if (tf === "partition") {
return true;
}
if (tf === "radial") {
if (newSeries.every((v2) => typeof v2 === "number")) return true;
return newSeries.length === 1 && newSeries[0] && typeof newSeries[0] === "object" && Array.isArray(newSeries[0].data);
}
if (tf === "bar" || tf === "summary") {
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;
this._removeGhost();
this._cancelPieces();
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 { marks, branches, unitDots } = this._captureFromDOM(fromType);
if (!marks.length) return false;
const mapping = this._buildMapping(
marks,
fromType,
toType,
newSeries,
branches
);
if (mapping.size === 0) return false;
this._snapshot = {
fromType,
toType,
mapping,
oldLayout: {
translateX: this.w.layout.translateX || 0,
translateY: this.w.layout.translateY || 0
}
};
const ff = familyOf(fromType);
const tf = familyOf(toType);
const canShape = this._canProbePaths();
const pieceFamilies = ff === "bar" || ff === "summary" || ff === "radial" && canShape;
if (tf === "unit" && pieceFamilies) {
const total = this._countUnitSeries(newSeries);
this._snapshot.pieceOut = total > 0 && total <= PIECE_BUDGET;
} else if (ff === "unit" && (tf === "bar" || tf === "summary" || tf === "radial" && canShape)) {
let total = 0;
unitDots.forEach((list) => {
total += list.length;
});
if (total > 0 && total <= PIECE_BUDGET) {
this._snapshot.pieceIn = true;
this._snapshot.sourceDots = unitDots;
const keyOrder = [];
if (tf === "radial") {
(Array.isArray(newSeries) ? newSeries : []).forEach(
(_v, i) => {
keyOrder.push(`${i}:0`);
}
);
} else {
(Array.isArray(newSeries) ? newSeries : []).forEach(
(s, seriesIdx) => {
const data = s && Array.isArray(s.data) ? s.data : [];
for (let j = 0; j < data.length; j++) {
keyOrder.push(`${seriesIdx}:${j}`);
}
}
);
}
this._snapshot.keyOrder = keyOrder;
}
}
if (this._needsGhost(fromType, toType) && !this._snapshot.pieceOut && !this._snapshot.pieceIn) {
this._captureGhost();
}
this.w.globals.previousPaths = [];
return true;
}
/**
* Whether the outgoing marks need an exit animation of their own.
*
* Most pairs do not. bar → pie hands every wedge the exact `d` of the bar it
* replaces, and treemap → sunburst does the same for its tiles: the outgoing
* mark IS the incoming mark's first frame, so it never needs to leave, and
* drawing a copy of it would only double the image at t=0.
*
* The unit pairs are the exception, in both directions, because the
* correspondence is not 1:1. Going in, one bar becomes N dots, so the bar has
* no successor to become. Coming out, N dots become one bar: the bar does
* grow from the cloud's footprint, but no individual dot has anywhere to go.
* Either way something on screen simply stops existing, which is exactly the
* hard cut that made these pairs read as "the old chart vanished and the new
* one animated" rather than as a morph.
*
* @param {string} fromType
* @param {string} toType
* @returns {boolean}
*/
_needsGhost(fromType, toType) {
return familyOf(fromType) === "unit" || familyOf(toType) === "unit";
}
/**
* Take a detached copy of the outgoing chart's marks, to be mounted over the
* incoming chart once it exists (see `_mountGhost`).
*
* The whole `<svg>` is cloned and the chrome then removed from the copy,
* rather than lifting the series groups out on their own: every mark's
* position depends on the transforms of the groups above it, and cloning
* from the root is what keeps those intact without re-deriving any geometry.
*
* The chrome is dropped because `applyChromeFade` already fades the incoming
* axes, grid and legend in from zero. Keeping the outgoing set as well would
* put two sets of axis labels on screen at half opacity each.
*/
_captureGhost() {
var _a, _b;
const paper = (_a = this.w.dom) == null ? void 0 : _a.Paper;
const node = paper && paper.node;
if (!node || typeof node.cloneNode !== "function") return;
const clone = node.cloneNode(true);
const drop = [
".apexcharts-xaxis",
".apexcharts-yaxis",
".apexcharts-grid",
".apexcharts-gridlines-horizontal",
".apexcharts-gridlines-vertical",
".apexcharts-legend",
".apexcharts-title-text",
".apexcharts-subtitle-text",
".apexcharts-annotations",
".apexcharts-zoom-rect",
".apexcharts-selection-rect",
".apexcharts-xcrosshairs",
".apexcharts-ycrosshairs"
];
if (typeof clone.querySelectorAll === "function") {
drop.forEach((sel) => {
clone.querySelectorAll(sel).forEach((el) => {
if (el.parentNode) el.parentNode.removeChild(el);
});
});
}
if (typeof clone.querySelectorAll === "function") {
clone.querySelectorAll("[id]").forEach((el) => {
el.removeAttribute("id");
});
}
(_b = clone.removeAttribute) == null ? void 0 : _b.call(clone, "id");
this._ghost = clone;
}
/**
* Mount the captured copy over the newly-rendered chart and fade it out.
*
* It goes ON TOP of the live svg, which is what makes both directions read
* as one motion rather than as a swap. Going into a unit chart the bars
* dissolve and the dots are uncovered already in flight, having left from
* inside the bar that held them. Coming out of one, the dots are still there
* to fade while the bar grows underneath them; behind the incoming mark they
* would be hidden on the first frame, because that mark starts out exactly
* the size of the cloud it is replacing.
*
* The fade runs over a fraction of the morph so the outgoing marks are gone
* before the incoming ones settle. Holding them for the full duration leaves
* two charts overlapping right at the moment the eye is reading the final
* shape, which looks like a rendering fault rather than a transition.
*/
_mountGhost() {
var _a, _b, _c;
const ghost = this._ghost;
if (!ghost || !Environment.isBrowser()) return;
const wrap = (_a = this.w.dom) == null ? void 0 : _a.elWrap;
if (!wrap || typeof wrap.appendChild !== "function") {
this._ghost = null;
return;
}
const style = ghost.style;
if (style) {
style.position = "absolute";
style.left = "0";
style.top = "0";
style.background = "transparent";
style.pointerEvents = "none";
style.opacity = "1";
}
(_b = ghost.setAttribute) == null ? void 0 : _b.call(ghost, "aria-hidden", "true");
(_c = ghost.setAttribute) == null ? void 0 : _c.call(ghost, "class", "apexcharts-morph-ghost");
wrap.appendChild(ghost);
const speed = this.getSpeed();
const fade = Math.max(120, Math.round(speed * GHOST_FADE_FRACTION));
BrowserAPIs.requestAnimationFrame(() => {
if (!style) return;
style.transition = `opacity ${fade}ms ease-in`;
style.opacity = "0";
});
setTimeout(() => this._removeGhost(), fade + 60);
}
/** Detach the ghost if one is mounted. Safe to call at any time. */
_removeGhost() {
const ghost = this._ghost;
this._ghost = null;
if (ghost && ghost.parentNode) ghost.parentNode.removeChild(ghost);
}
/* ------------------------------------------------------------------ *
* The piece layer (see MorphPieces for the geometry).
* ------------------------------------------------------------------ */
/**
* Object count of an incoming unit series: one datum per dot in the object
* form. The numeric form ([3, 5]) scales values by `plotOptions.unit
* .unitValue`, which is not resolvable pre-merge, so it counts as zero and
* keeps the burst-and-ghost behaviour.
*
* @param {any} newSeries
* @returns {number}
*/
_countUnitSeries(newSeries) {
if (!Array.isArray(newSeries)) return 0;
let total = 0;
for (const s of newSeries) {
if (!s || typeof s !== "object" || !Array.isArray(s.data)) return 0;
total += s.data.length;
}
return total;
}
/**
* Whether the incoming unit chart should hold its dots for the piece layer:
* render them at their final slots, hidden, and let the pieces do the
* flying. The reveal happens per dot as its piece lands.
*
* Consulted by the unit renderer during its draw, which runs after
* `captureBeforeDestroy` and before `applyChromeFade`, so the decision was
* already made from the same series the renderer is now drawing.
*
* @returns {boolean}
*/
usesPieceTakeover() {
return !!(this._snapshot && this._snapshot.pieceOut);
}
/**
* Whether the piece layer claims the incoming mark at (realIndex, j): a
* source cluster's dots will fly to it and tile it, so it must render
* hidden and reveal only when its mosaic is complete. Consulted by the bar
* renderer (boxPlot and violin render through it).
*
* @param {number|string} realIndex
* @param {number|string} j
* @returns {boolean}
*/
claimsTargetMark(realIndex, j) {
return !!(this._snapshot && this._snapshot.pieceIn && this._snapshot.mapping.has(`${realIndex}:${j}`));
}
/**
* Create the overlay group the pieces are driven in. It lives INSIDE the
* new chart's elGraphical so every coordinate matches the marks' own local
* space, and it never takes a pointer event.
* @returns {any} the <g> node, or null
*/
_makePieceLayer() {
var _a, _b;
const graph = (_a = this.w.dom) == null ? void 0 : _a.elGraphical;
const host = graph && graph.node;
if (!host || typeof host.appendChild !== "function") return null;
const g2 = BrowserAPIs.createElementNS("http://www.w3.org/2000/svg", "g");
if (!g2) return null;
g2.setAttribute("class", "apexcharts-morph-pieces");
g2.setAttribute("pointer-events", "none");
const cuid = (_b = this.w.globals) == null ? void 0 : _b.cuid;
if (cuid) g2.setAttribute("clip-path", `url(#gridRectBarMask${cuid})`);
host.appendChild(g2);
this._pieceLayer = g2;
return g2;
}
/**
* Reveal everything a piece takeover hid, whether or not the pieces ran.
* The attribute is plain (no namespace colon) so it stays selectable
* everywhere.
*/
_revealPieceHidden() {
var _a;
const baseEl = (_a = this.w.globals.dom) == null ? void 0 : _a.baseEl;
if (!baseEl || typeof baseEl.querySelectorAll !== "function") return;
baseEl.querySelectorAll("[data-piece-hidden]").forEach(
(el) => {
el.removeAttribute("opacity");
el.removeAttribute("data-piece-hidden");
}
);
}
/** Stop the piece run, drop the overlay, and reveal anything still hidden. */
_cancelPieces() {
if (this._pieceCancel) {
this._pieceCancel();
this._pieceCancel = null;
}
const layer = this._pieceLayer;
this._pieceLayer = null;
if (layer && layer.parentNode) layer.parentNode.removeChild(layer);
this._revealPieceHidden();
}
/**
* Whether this environment can hit-test path geometry at all. Decided
* before the ghost clone is taken, because a family whose cells are only
* honest when probed (radial) must keep the fade rather than fall back to a
* rectangular grid it cannot justify. jsdom answers no.
* @returns {boolean}
*/
_canProbePaths() {
if (!Environment.isBrowser()) return false;
const probe = BrowserAPIs.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
return !!probe && typeof /** @type {any} */
probe.isPointInFill === "function";
}
/**
* A per-band ink prober over a mark's path, for gridDivideShape: given a
* major-axis band it measures where the mark actually has ink across the
* minor axis, so a violin's cells follow its density outline, a boxPlot's
* whisker rows collapse to slivers, and a wedge's rows stop at the wedge
* instead of spanning the bounding box (which stamped a rectangle over the
* mark at frame one).
*
* The probe path is mounted (hidden) inside the piece layer so its user
* space is exactly the space the cells are laid out in. The stroke test
* catches zero-area subpaths (a boxPlot's whisker line has no fill to
* hit), and its width is what a whisker's slivers will measure.
*
* Returns null when the environment cannot hit-test path geometry (jsdom);
* the caller then keeps the plain grid.
*
* @param {string} d - path data, in piece-layer coordinates
* @param {{x:number,y:number,width:number,height:number}} bbox
* @param {any} layer - the mounted piece layer
* @returns {{ intervalsAt: (bandLo: number, bandHi: number, horizontal: boolean) => Array<[number, number]> | null, dispose: () => void } | null}
*/
_makeExtentProber(d, bbox, layer) {
const doc = layer.ownerDocument;
const probe = doc.createElementNS("http://www.w3.org/2000/svg", "path");
probe.setAttribute("d", d);
probe.setAttribute("fill", "#000");
probe.setAttribute("stroke", "#000");
probe.setAttribute("stroke-width", "3");
probe.setAttribute("visibility", "hidden");
layer.appendChild(probe);
const svg = probe.ownerSVGElement;
if (typeof /** @type {any} */
probe.isPointInFill !== "function" || !svg || typeof svg.createSVGPoint !== "function") {
layer.removeChild(probe);
return null;
}
const pt = svg.createSVGPoint();
const hit = (x, y) => {
pt.x = x;
pt.y = y;
const p2 = (
/** @type {any} */
probe
);
return p2.isPointInFill(pt) || typeof p2.isPointInStroke === "function" && p2.isPointInStroke(pt);
};
const SCAN = 48;
const intervalsAt = (bandLo, bandHi, horizontal) => {
const lo = horizontal ? bbox.y : bbox.x;
const hi = lo + (horizontal ? bbox.height : bbox.width);
if (!(hi > lo)) return null;
const at = (v2, major) => horizontal ? hit(major, v2) : hit(v2, major);
const majors = [
bandLo + (bandHi - bandLo) * 0.1,
(bandLo + bandHi) / 2,
bandHi - (bandHi - bandLo) * 0.1
];
const edge = (inside, outside, major) => {
let a = outside;
let b2 = inside;
for (let it = 0; it < 6; it++) {
const m2 = (a + b2) / 2;
if (at(m2, major)) b2 = m2;
else a = m2;
}
return (a + b2) / 2;
};
const step = (hi - lo) / SCAN;
const proof = new Array(SCAN + 1).fill(null);
let any = false;
for (const major of majors) {
for (let s = 0; s <= SCAN; s++) {
if (proof[s] !== null) continue;
if (at(lo + s * step, major)) {
proof[s] = major;
any = true;
}
}
}
if (!any) return null;
const out = [];
let runStart = -1;
for (let s = 0; s <= SCAN + 1; s++) {
const inside = s <= SCAN && proof[s] !== null;
if (inside && runStart < 0) runStart = s;
if (!inside && runStart >= 0) {
const last = s - 1;
const major = (
/** @type {number} */
proof[runStart]
);
const left = runStart === 0 ? lo : edge(lo + runStart * step, lo + (runStart - 1) * step, major);
const right = last === SCAN ? hi : edge(
lo + last * step,
lo + (last + 1) * step,
/** @type {number} */
proof[last]
);
if (right > left) out.push([left, right]);
runStart = -1;
}
}
return out.length ? out : null;
};
return {
intervalsAt,
dispose: () => {
if (probe.parentNode) probe.parentNode.removeChild(probe);
}
};
}
/**
* mark -> objects. Cut each captured mark into one cell per dot and fly
* every cell to its dot, corners rounding off and fill blending on the way.
* The real dots (rendered hidden by the unit chart, see usesPieceTakeover)
* are revealed one by one as their piece lands, so the handoff is
* geometrically exact and nothing ever fades.
*/
_separatePieces() {
var _a;
const snap = this._snapshot;
const baseEl = (_a = this.w.globals.dom) == null ? void 0 : _a.baseEl;
if (!snap || !baseEl) return this._revealPieceHidden();
const byCluster = /* @__PURE__ */ new Map();
let total = 0;
baseEl.querySelectorAll(".apexcharts-unit-area").forEach((dot) => {
var _a2, _b, _c, _d, _e, _f, _g;
const i = parseInt((_a2 = dot.getAttribute("i")) != null ? _a2 : "", 10);
if (isNaN(i)) return;
const cxAttr = dot.getAttribute("cx");
let x;
let y;
let r = 3;
if (cxAttr != null) {
x = parseFloat(cxAttr);
y = parseFloat((_b = dot.getAttribute("cy")) != null ? _b : "");
r = parseFloat((_c = dot.getAttribute("r")) != null ? _c : "3") || 3;
} else {
const wAttr = parseFloat((_d = dot.getAttribute("width")) != null ? _d : "0") || 0;
const hAttr = parseFloat((_e = dot.getAttribute("height")) != null ? _e : "0") || 0;
x = parseFloat((_f = dot.getAttribute("x")) != null ? _f : "") + wAttr / 2;
y = parseFloat((_g = dot.getAttribute("y")) != null ? _g : "") + hAttr / 2;
r = Math.max(wAttr, hAttr) / 2 || 3;
}
if (!isFinite(x) || !isFinite(y)) return;
let list = byCluster.get(i);
if (!list) {
list = [];
byCluster.set(i, list);
}
list.push({ el: dot, x, y, r, fill: dot.getAttribute("fill") });
total++;
});
if (total === 0 || total > PIECE_BUDGET) return this._revealPieceHidden();
const layer = this._makePieceLayer();
if (!layer) return this._revealPieceHidden();
const pieces = [];
const doc = layer.ownerDocument;
const sourceFam = familyOf(snap.fromType);
const shapedSource = sourceFam === "summary" || sourceFam === "radial";
Array.from(byCluster.keys()).sort((a, b2) => a - b2).forEach((i) => {
var _a2;
const dots = (
/** @type {any[]} */
byCluster.get(i)
);
const box = this.getInitialBBoxFor(i);
const entry = snap.mapping.get(`${i}:0`);
if (!box || !entry) {
dots.forEach((d) => {
d.el.removeAttribute("opacity");
d.el.removeAttribute("data-piece-hidden");
});
return;
}
const markFill = entry.fill && entry.fill.indexOf("url(") !== 0 ? entry.fill : ((_a2 = this.w.globals.colors) == null ? void 0 : _a2[i]) || dots[0].fill;
let prober = null;
if (shapedSource) {
const shifted = this.getInitialPathFor(i, 0);
if (shifted) prober = this._makeExtentProber(shifted, box, layer);
}
const divided = prober ? gridDivideShape(box, dots.length, prober.intervalsAt) : gridDivideRect(box, dots.length);
if (prober) prober.dispose();
const cells = sortByHilbert(divided, (c) => [
c.x + c.width / 2,
c.y + c.height / 2
]);
const ordered = sortByHilbert(dots, (d) => [d.x, d.y]);
for (let k = 0; k < ordered.length; k++) {
const cell = cells[k];
const dot = ordered[k];
const el = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
el.setAttribute("data-i", String(i));
el.setAttribute("x", String(cell.x));
el.setAttribute("y", String(cell.y));
el.setAttribute("width", String(cell.width));
el.setAttribute("height", String(cell.height));
el.setAttribute("rx", "0");
el.setAttribute("fill", String(markFill));
layer.appendChild(el);
pieces.push({
el,
from: { x: cell.x, y: cell.y, width: cell.width, height: cell.height, rx: 0 },
to: {
x: dot.x - dot.r,
y: dot.y - dot.r,
width: dot.r * 2,
height: dot.r * 2,
rx: dot.r
},
fill: makeColorLerp(markFill, dot.fill),
fillEnd: dot.fill,
delay: 0,
meta: { dotEl: dot.el }
});
}
});
if (!pieces.length) return this._cancelPieces();
this._runPieces(pieces, (piece) => {
const dotEl = piece.meta.dotEl;
dotEl.removeAttribute("opacity");
dotEl.removeAttribute("data-piece-hidden");
if (piece.el.parentNode) piece.el.parentNode.removeChild(piece.el);
});
}
/**
* objects -> mark. Each captured outgoing dot flies to one cell of the
* incoming mark, squaring off and blending towards the mark's fill; the
* mark itself (rendered hidden, see claimsTargetMark) is revealed the
* moment its last piece lands and the mosaic is complete, which is also the
* moment the seams disappear.
*/
_combinePieces() {
var _a, _b;
const snap = this._snapshot;
const baseEl = (_a = this.w.globals.dom) == null ? void 0 : _a.baseEl;
if (!snap || !snap.sourceDots || !snap.keyOrder || !baseEl) {
return this._revealPieceHidden();
}
const targets = this._collectTargetMarks(snap.toType);
if (!targets.size) return this._revealPieceHidden();
const dx = snap.oldLayout.translateX - (this.w.layout.translateX || 0);
const dy = snap.oldLayout.translateY - (this.w.layout.translateY || 0);
const clusterIdx = Array.from(snap.sourceDots.keys()).sort((a, b2) => a - b2);
const layer = this._makePieceLayer();
if (!layer) return this._revealPieceHidden();
const doc = layer.ownerDocument;
const pieces = [];
const targetFam = familyOf(snap.toType);
const shapedTarget = targetFam === "summary" || targetFam === "radial";
for (let k = 0; k < clusterIdx.length; k++) {
const dots = (
/** @type {any[]} */
snap.sourceDots.get(clusterIdx[k])
);
const key = snap.keyOrder[k];
const target = key ? targets.get(key) : null;
if (!target || !dots || !dots.length) {
if (target) {
target.els.forEach((el) => {
el.removeAttribute("opacity");
el.removeAttribute("data-piece-hidden");
});
}
continue;
}
const markFill = target.fill && target.fill.indexOf("url(") !== 0 ? target.fill : ((_b = this.w.globals.colors) == null ? void 0 : _b[target.realIndex]) || dots[0].fill;
let prober = null;
if (shapedTarget) {
const d = target.d || target.els.map(
(p2) => p2.getAttribute("pathTo") || p2.getAttribute("d")
).filter(Boolean).join(" ");
if (d) prober = this._makeExtentProber(d, target.bbox, layer);
}
const divided = prober ? gridDivideShape(target.bbox, dots.length, prober.intervalsAt) : gridDivideRect(target.bbox, dots.length);
if (prober) prober.dispose();
const cells = sortByHilbert(divided, (c) => [
c.x + c.width / 2,
c.y + c.height / 2
]);
const ordered = sortByHilbert(dots, (d) => [d.x, d.y]);
const markState = { remaining: ordered.length, els: target.els, tiles: (
/** @type {any[]} */
[]
) };
for (let m2 = 0; m2 < ordered.length; m2++) {
const dot = ordered[m2];
const cell = cells[m2];
const el = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
const fx = dot.x + dx - dot.r;
const fy = dot.y + dy - dot.r;
el.setAttribute("data-key", key);
el.setAttribute("x", String(fx));
el.setAttribute("y", String(fy));
el.setAttribute("width", String(dot.r * 2));
el.setAttribute("height", String(dot.r * 2));
el.setAttribute("rx", String(dot.r));
el.setAttribute("fill", String(dot.fill || markFill));
layer.appendChild(el);
markState.tiles.push(el);
pieces.push({
el,
from: { x: fx, y: fy, width: dot.r * 2, height: dot.r * 2, rx: dot.r },
to: { x: cell.x, y: cell.y, width: cell.width, height: cell.height, rx: 0 },
fill: makeColorLerp(dot.fill, markFill),
fillEnd: String(markFill),
delay: 0,
meta: { markState }
});
}
}
if (!pieces.length) return this._cancelPieces();
this._runPieces(pieces, (piece) => {
const state = piece.meta.markState;
state.remaining--;
if (state.remaining === 0) {
state.els.forEach((el) => {
el.removeAttribute("opacity");
el.removeAttribute("data-piece-hidden");
});
state.tiles.forEach((t) => {
if (t.parentNode) t.parentNode.removeChild(t);
});
}
});
}
/**
* Stagger and start a piece run. Delays sweep the (already spatially
* sorted) list front to back, and the last piece still lands within the
* configured morph speed.
*
* @param {import('./MorphPieces').Piece[]} pieces
* @param {(piece: import('./MorphPieces').Piece) => void} onPieceDone
*/
_runPieces(pieces, onPieceDone) {
const speed = this.getSpeed();
const stagger = Math.min(PIECE_STAGGER_MAX, speed * 0.35);
const flight = Math.max(180, speed - stagger);
for (let k = 0; k < pieces.length; k++) {
pieces[k].delay = pieces.length > 1 ? k / (pieces.length - 1) * stagger : 0;
}
this._pieceCancel = runPieceTween({
pieces,
duration: flight,
onPieceDone,
onAllDone: () => {
this._pieceCancel = null;
this._cancelPieces();
}
});
}
/**
* The incoming chart's marks, read live: every `path[pathTo]` grouped into
* one mark per (realIndex, j), with the union bbox of its final geometry.
* Bar marks are one path each; summary marks (boxPlot, violin) may be
* several, walked exactly like the capture branch walks the outgoing ones.
*
* @param {string} toType
* @returns {Map<string, { realIndex: number, j: number, bbox: {x:number,y:number,width:number,height:number}, fill: string|null, els: any[], d?: string }>}
*/
_collectTargetMarks(toType) {
var _a;
const baseEl = (_a = this.w.globals.dom) == null ? void 0 : _a.baseEl;
const out = /* @__PURE__ */ new Map();
if (!baseEl) return out;
const fam = familyOf(toType);
if (fam === "radial") {
baseEl.querySelectorAll(".apexcharts-pie-series .apexcharts-pie-area").forEach((p2, i) => {
const d = p2.getAttribute("data:pathFinal") || p2.getAttribute("d");
if (!d || !d.trim()) return;
const box = this._pathBBox(d);
if (!box) return;
out.set(`${i}:0`, {
realIndex: i,
j: 0,
d,
bbox: {
x: box.minX,
y: box.minY,
width: box.maxX - box.minX,
height: box.maxY - box.minY
},
fill: p2.getAttribute("fill"),
els: [p2]
});
});
return out;
}
const wrapClass = fam === "summary" ? `.apexcharts-${toType}-series` : ".apexcharts-bar-series";
baseEl.querySelectorAll(`${wrapClass} .apexcharts-series`).forEach((group) => {
var _a2;
const realIndex = parseInt((_a2 = group.getAttribute("data:realIndex")) != null ? _a2 : "0", 10) || 0;
let order = 0;
group.querySelectorAll("path[pathTo]").forEach((p2) => {
var _a3;
const d = p2.getAttribute("pathTo") || p2.getAttribute("d");
if (!d || !d.trim()) return;
const jAttr = parseInt((_a3 = p2.getAttribute("j")) != null ? _a3 : "", 10);
const j = isNaN(jAttr) ? order++ : jAttr;
const box = this._pathBBox(d);
if (!box) return;
const key = `${realIndex}:${j}`;
const prev = out.get(key);
if (prev) {
prev.els.push(p2);
prev.bbox = {
x: Math.min(prev.bbox.x, box.minX),
y: Math.min(prev.bbox.y, box.minY),
width: Math.max(prev.bbox.x + prev.bbox.width, box.maxX) - Math.min(prev.bbox.x, box.minX),
height: Math.max(prev.bbox.y + prev.bbox.height, box.maxY) - Math.min(prev.bbox.y, box.minY)
};
} else {
out.set(key, {
realIndex,
j,
bbox: {
x: box.minX,
y: box.minY,
width: box.maxX - box.minX,
height: box.maxY - box.minY
},
fill: p2.getAttribute("fill"),
els: [p2]
});
}
});
});
return out;
}
/**
* 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 {{ marks: Array<{ realIndex: number, j: number, d: string, fill: string|null, key?: string|null }>, branches: Array<{ key: string, d: string, fill: string|null }>, unitDots: Map<number, Array<{x:number,y:number,r:number,fill:string|null}>> }}
*/
_captureFromDOM(fromType) {
var _a;
const baseEl = (_a = this.w.globals.dom) == null ? void 0 : _a.baseEl;
if (!baseEl) return { marks: [], branches: [], unitDots: /* @__PURE__ */ new Map() };
const captured = [];
const branches = [];
const unitDots = /* @__PURE__ */ new Map();
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((p2, j) => {
const d = p2.getAttribute("pathTo") || p2.getAttribute("d");
if (!d) return;
captured.push({
realIndex,
j,
d,
fill: p2.getAttribute("fill")
});
});
});
} else if (fam === "summary") {
const byMark = /* @__PURE__ */ new Map();
baseEl.querySelectorAll(`.apexcharts-${fromType}-area`).forEach((p2) => {
var _a2, _b;
const j = parseInt((_a2 = p2.getAttribute("j")) != null ? _a2 : "", 10);
if (isNaN(j)) return;
const d = p2.getAttribute("pathTo") || p2.getAttribute("d");
if (!d || !d.trim()) return;
const group = typeof p2.closest === "function" ? p2.closest(".apexcharts-series") : null;
const realIndex = parseInt((_b = group == null ? void 0 : group.getAttribute("data:realIndex")) != null ? _b : "0", 10) || 0;
const key = `${realIndex}:${j}`;
const prev = byMark.get(key);
if (prev) prev.d += ` ${d}`;
else byMark.set(key, { realIndex, j, d, fill: p2.getAttribute("fill") });
});
Array.from(byMark.values()).sort((a, b2) => a.realIndex - b2.realIndex || a.j - b2.j).forEach((m2) => captured.push(m2));
} else if (fam === "partition") {
if (fromType === "treemap") {
const rectPath = (el) => {
var _a2, _b, _c, _d;
const x = parseFloat((_a2 = el.getAttribute("x")) != null ? _a2 : "");
const y = parseFloat((_b = el.getAttribute("y")) != null ? _b : "");
const width = parseFloat((_c = el.getAttribute("width")) != null ? _c : "");
const height = parseFloat((_d = el.getAttribute("height")) != null ? _d : "");
if (![x, y, width, height].every((v2) => isFinite(v2))) return null;
return `M ${x} ${y} L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} Z`;
};
const tiles = baseEl.querySelectorAll(".apexcharts-treemap-rect");
tiles.forEach((t) => {
var _a2, _b;
const d = rectPath(t);
if (!d) return;
captured.push({
realIndex: parseInt((_a2 = t.getAttribute("i")) != null ? _a2 : "0", 10) || 0,
j: parseInt((_b = t.getAttribute("j")) != null ? _b : "0", 10) || 0,
d,
fill: t.getAttribute("fill"),
key: t.getAttribute("data:key") || null
});
});
const containers = baseEl.querySelectorAll(
".apexcharts-treemap-parent-rect"
);
containers.forEach((c) => {
const d = rectPath(c);
const key = c.getAttribute("data:key");
if (!d || !key) return;
branches.push({ key, d, fill: c.getAttribute("fill") });
});
} else {
const arcs = baseEl.querySelectorAll(".apexcharts-sunburst-arc");
const leaves = [];
arcs.forEach((a) => {
if (a.getAttribute("data:leaf") === "true") leaves.push(a);
});
const source = leaves.length ? leaves : Array.from(arcs);
source.forEach((a, i) => {
const d = a.getAttribute("d");
if (!d || !d.trim()) return;
captured.push({
realIndex: i,
j: 0,
d,
fill: a.getAttribute("fill"),
key: a.getAttribute("data:key") || null
});
});
arcs.forEach((a) => {
if (a.getAttribute("data:leaf") === "true") return;
const d = a.getAttribute("d");
const key = a.getAttribute("data:key");
if (!d || !d.trim() || !key) return;
branches.push({ key, d, fill: a.getAttribute("fill") });
});
}
} else if (fam === "unit") {
const dots = baseEl.querySelectorAll(".apexcharts-unit-area");
const boxes = /* @__PURE__ */ new Map();
dots.forEach((dot) => {
var _a2, _b, _c, _d, _e, _f, _g;
const i = parseInt((_a2 = dot.getAttribute("i")) != null ? _a2 : "", 10);
if (isNaN(i)) return;
const cxAttr = dot.getAttribute("cx");
let x;
let y;
let r = 3;
if (cxAttr != null) {
x = parseFloat(cxAttr);
y = parseFloat((_b = dot.getAttribute("cy")) != null ? _b : "");
r = parseFloat((_c = dot.getAttribute("r")) != null ? _c : "3") || 3;
} else {
const wAttr = parseFloat((_d = dot.getAttribute("width")) != null ? _d : "0") || 0;
const hAttr = parseFloat((_e = dot.getAttribute("height")) != null ? _e : "0") || 0;
x = parseFloat((_f = dot.getAttribute("x")) != null ? _f : "") + wAttr / 2;
y = parseFloat((_g = dot.getAttribute("y")) != null ? _g : "") + hAttr / 2;
r = Math.max(wAttr, hAttr) / 2 || 3;
}
if (!isFinite(x) || !isFinite(y)) return;
let list = unitDots.get(i);
if (!list) {
list = [];
unitDots.set(i, list);
}
list.push({ x, y, r, fill: dot.getAttribute("fill") });
const box = boxes.get(i);
if (!box) {
boxes.set(i, {
minX: x,
minY: y,
maxX: x,
maxY: y,
fill: dot.getAttribute("fill")
});
return;
}
if (x < box.minX) box.minX = x;
if (x > box.maxX) box.maxX = x;
if (y < box.minY) box.minY = y;
if (y > box.maxY) box.maxY = y;
});
Array.from(boxes.keys()).sort((a, b2) => a - b2).forEach((i) => {
const b2 = (
/** @type {any} */
boxes.get(i)
);
const pad = 2;
const x1 = b2.minX - pad;
const y1 = b2.minY - pad;
const x2 = b2.maxX + pad;
const y2 = b2.maxY + pad;
captured.push({
realIndex: i,
j: 0,
d: `M ${x1} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x1} ${y2} Z`,
fill: b2.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((p2) => {
var _a2;
const parent = (
/** @type {Element|null} */
p2.parentElement
);
const realIndex = parseInt(
(_a2 = parent == null ? void 0 : parent.getAttribute("data:realIndex")) != null ? _a2 : "0",
10
);
const rawD = p2.getAttribute("d");
if (!rawD) return;
const strokeWidth = parseFloat(p2.getAttribute("stroke-width") || "0");
const d = strokeWidth > 1 ? this._radialArcToFilledSegment(
rawD,
strokeWidth,
centerX,
centerY
) || rawD : rawD;
captured.push({
realIndex,
j: 0,
d,
fill: p2.getAttribute("stroke")
});
});
} else {
const slices = baseEl.querySelectorAll(
".apexcharts-pie-series .apexcharts-pie-area"
);
slices.forEach(
(p2, i) => {
const d = p2.getAttribute("d");
if (!d) return;
captured.push({
realIndex: i,
j: 0,
d,
fill: p2.getAttribute("fill")
});
}
);
}
}
return { marks: captured, branches, unitDots };
}
/**
* 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 m2 = rawD.match(
/M\s*(-?[\d.]+)\s+(-?[\d.]+)\s+A\s*(-?[\d.]+)\s+(?:-?[\d.]+)\s+(?:-?[\d.]+)\s+(\d)\s+(\d)\s+(-?[\d.]+)\s+(-?[\d.]+)/
);
if (!m2) return null;
const x1 = parseFloat(m2[1]);
const y1 = parseFloat(m2[2]);
const r = parseFloat(m2[3]);
const large = parseInt(m2[4], 10);
const sweep = parseInt(m2[5], 10);
const x2 = parseFloat(m2[6]);
const y2 = parseFloat(m2[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, key?: 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.
* @param {Array<{ key: string, d: string, fill: string|null }>} [branches]
* non-leaf marks, for the key-based partition pairing.
*/
_buildMapping(captured, _fromType, toType, newSeries, branches) {
const map = /* @__PURE__ */ new Map();
const tf = familyOf(toType);
const flat = captured.slice().sort((a, b2) => a.realIndex - b2.realIndex || a.j - b2.j);
if (tf === "partition" && branches && branches.length) {
const keyedMarks = flat.filter((c) => c.key);
if (keyedMarks.length === flat.length) {
keyedMarks.forEach((c) => {
map.set(`key:${c.key}`, { d: c.d, fill: c.fill });
});
branches.forEach((br) => {
map.set(`key:${br.key}`, { d: br.d, fill: br.fill });
});
}
}
if (tf === "radial" || tf === "unit" || tf === "partition") {
flat.forEach((c, i) => {
map.set(`${i}:0`, { d: c.d, fill: c.fill });
});
return map;
}
if (tf === "bar" || tf === "summary") {
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|string} realIndex
* @param {number|string} 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(" ");
}
/**
* The centre point (in the NEW chart's screen space) of the captured shape
* for cluster `i`. Kept for callers that only need a point; the unit renderer
* uses getInitialBBoxFor so its dots fill the shape rather than stack on a
* single point.
* @param {number} i
* @returns {{ x: number, y: number } | null}
*/
getInitialCenterFor(i) {
const box = this.getInitialBBoxFor(i);
if (!box) return null;
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
}
/**
* The `k`-th captured path in draw order, already shifted into the NEW
* chart's coordinate space.
*
* For marks that pair up by position rather than by a (series, point) grid:
* a treemap's tiles and a sunburst's leaves are each one mark per row, laid
* out in the same reading order, so the k-th of one becomes the k-th of the
* other.
*
* @param {number} k
* @returns {string | null}
*/
getInitialPathAt(k) {
return this.getInitialPathFor(k, 0);
}
/**
* The captured shape for a branch identity (charts/common/Hierarchy.morphKey),
* or null when the outgoing chart had no mark for that branch.
*
* This is what lets a partition morph pair at every level: a sector, an
* industry and a company each find the arc or tile that stood for the same
* branch, instead of leaves pairing by draw order while the containers pop.
*
* @param {string} key
* @returns {string | null}
*/
getInitialPathForKey(key) {
if (!this._snapshot || !key) return null;
return this.getInitialPathFor("key", key);
}
/** True when the active snapshot can pair by branch key. */
hasKeyedMarks() {
if (!this._snapshot) return false;
for (const k of this._snapshot.mapping.keys()) {
if (typeof k === "string" && k.startsWith("key:")) return true;
}
return false;
}
/**
* Where the `j`-th of `n` objects in cluster `i` starts, INSIDE the shape it
* came out of.
*
* An aggregate mark stands for a quantity, and its extent is that quantity: a
* bar of height h representing n units gives its k-th unit the height
* fraction (k + 0.5)/n. So a bar does not spray its dots from a single point,
* it comes apart along its own length, bottom-up, and each dot leaves from
* the part of the bar that was standing for it. The reverse direction reads
* the same geometry, so explode and collapse are inverses.
*
* The distribution follows the captured shape's LONGER axis, which is what
* makes one function serve both marks: a bar's box is tall and thin, so the
* dots leave in a column; a wedge's box is squat, so they leave in a row
* across it.
*
* @param {number} i - cluster index
* @param {number} j - the object's rank within its cluster
* @param {number} n - objects in the cluster
* @returns {{ x: number, y: number } | null}
*/
getInitialSlotFor(i, j, n) {
const box = this.getInitialBBoxFor(i);
if (!box) return null;
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
if (!(n > 1) || !(j >= 0)) return { x: cx, y: cy };
const t = (Math.min(j, n - 1) + 0.5) / n;
if (box.height >= box.width) {
return { x: cx, y: box.y + box.height * (1 - t) };
}
return { x: box.x + box.width * t, y: cy };
}
/**
* The bounding box (in the NEW chart's screen space) of the captured shape
* for cluster `i`. `getInitialSlotFor` distributes a cluster's objects across
* this box as their start positions, so a tall bar visibly breaks apart into
* a tall column of dots that then swarm into the cluster.
* @param {number} i
* @returns {{ x: number, y: number, width: number, height: number } | null}
*/
getInitialBBoxFor(i) {
if (!this._snapshot) return null;
const entry = this._snapshot.mapping.get(`${i}:0`);
if (!entry) return null;
const box = this._pathBBox(entry.d);
if (!box) 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 {
x: box.minX + dx,
y: box.minY + dy,
width: box.maxX - box.minX,
height: box.maxY - box.minY
};
}
/**
* Bounding box of an absolute-command SVG path `d`. Good enough as the burst
* footprint (we only need where the shape sat, not exact geometry).
* @param {string} d
* @returns {{ minX:number, minY:number, maxX:number, maxY:number } | null}
*/
_pathBBox(d) {
const commands = parsePath(d);
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let seen = false;
commands.forEach(
/** @param {any[]} c */
(c) => {
const cmd = c[0];
if (cmd === "Z") return;
let pairs = [];
if (cmd === "H") pairs = [[c[1], (minY + maxY) / 2 || c[1]]];
else if (cmd === "V") pairs = [[(minX + maxX) / 2 || c[1], c[1]]];
else if (cmd === "A") pairs = [[c[6], c[7]]];
else {
for (let k = 1; k + 1 < c.length; k += 2) pairs.push([c[k], c[k + 1]]);
}
pairs.forEach(([x, y]) => {
if (!isFinite(x) || !isFinite(y)) return;
seen = true;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
});
}
);
if (!seen) return null;
return { minX, minY, maxX, maxY };
}
/**
* @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;
if (this._snapshot.pieceOut) this._separatePieces();
else if (this._snapshot.pieceIn) this._combinePieces();
else this._mountGhost();
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;
this._removeGhost();
this._cancelPieces();
}
}
_core__default.registerFeatures({ morphTypeChange: MorphTypeChange });
const XHTML$3 = "http://www.w3.org/1999/xhtml";
const BREADCRUMB_HEIGHT = 18;
function breadcrumbConfig(w2, localCfg) {
const shared = w2.config.drilldown && w2.config.drilldown.breadcrumb || {};
return __spreadValues(__spreadValues({
show: true,
position: "top-left",
separator: " / ",
rootLabel: "All",
offsetX: 0,
offsetY: 0,
formatter: void 0
}, shared), localCfg || {});
}
function avoidChromeOverlap(w2, nav) {
const chrome = (
/** @type {Element[]} */
[".apexcharts-title-text", ".apexcharts-subtitle-text"].map((s) => w2.dom.baseEl.querySelector(s)).filter((el) => el !== null)
);
if (!chrome.length) return;
const wrapTop = w2.dom.elWrap.getBoundingClientRect().top;
for (let pass = 0; pass < chrome.length + 1; pass++) {
const nr = nav.getBoundingClientRect();
const hit = chrome.find((el) => {
const r = el.getBoundingClientRect();
return nr.left < r.right && nr.right > r.left && nr.top < r.bottom && nr.bottom > r.top;
});
if (!hit) break;
nav.style.top = `${hit.getBoundingClientRect().bottom - wrapTop + 4}px`;
}
}
function breadcrumbCeiling(w2, nav) {
const gridTop = w2.layout.translateY || 0;
const elWrap = w2.dom.elWrap;
if (!elWrap) return gridTop;
const labels = w2.dom.baseEl.querySelectorAll(".apexcharts-yaxis-label");
if (!labels.length) return gridTop;
const wrapTop = elWrap.getBoundingClientRect().top;
const navRect = nav.getBoundingClientRect();
let ceiling = gridTop;
for (let i = 0; i < labels.length; i++) {
const r = labels[i].getBoundingClientRect();
if (!r.height) continue;
if (r.left >= navRect.right || r.right <= navRect.left) continue;
ceiling = Math.min(ceiling, r.top - wrapTop);
}
return ceiling;
}
function placeInReservedBand(w2, ctx, nav, cfg) {
var _a;
const dimHelpers = (_a = ctx == null ? void 0 : ctx.dimensions) == null ? void 0 : _a.dimHelpers;
const titleArea = dimHelpers ? dimHelpers.getTitleSubtitleCoords("title").height + dimHelpers.getTitleSubtitleCoords("subtitle").height : 0;
const navH = nav.getBoundingClientRect().height || BREADCRUMB_HEIGHT;
const offsetY = cfg && cfg.offsetY || 0;
const ceiling = breadcrumbCeiling(w2, nav);
if (ceiling - titleArea >= navH + 1) {
nav.style.top = `${ceiling - navH - 1 + offsetY}px`;
return true;
}
nav.style.top = `${titleArea + offsetY}px`;
const dark = w2.config.theme.mode === "dark";
nav.style.background = dark ? "rgba(20,24,30,0.82)" : "rgba(255,255,255,0.86)";
nav.style.borderRadius = "4px";
return false;
}
function clearBreadcrumb(w2) {
const elWrap = w2.dom.elWrap;
if (!elWrap) return;
const existing = elWrap.querySelector(".apexcharts-breadcrumb");
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
}
function renderBreadcrumb(w2, opts) {
if (!Environment.isBrowser()) return null;
const elWrap = w2.dom.elWrap;
if (!elWrap) return null;
clearBreadcrumb(w2);
const cfg = opts.config || breadcrumbConfig(w2);
if (cfg.show === false) return null;
const crumbs = opts.crumbs || [];
if (crumbs.length < 2) return null;
const nav = BrowserAPIs.createElementNS(XHTML$3, "nav");
nav.setAttribute("class", "apexcharts-breadcrumb");
nav.setAttribute("aria-label", opts.ariaLabel || "Breadcrumb");
positionBreadcrumb(nav, cfg);
if (opts.compact) {
nav.style.fontSize = "11px";
nav.style.padding = "0 2px";
}
const separator = cfg.separator != null ? cfg.separator : " / ";
crumbs.forEach((crumb, i) => {
var _a;
if (i > 0) {
const sep = BrowserAPIs.createElementNS(XHTML$3, "span");
sep.setAttribute("class", "apexcharts-breadcrumb-separator");
sep.setAttribute("aria-hidden", "true");
sep.textContent = separator;
nav.appendChild(sep);
}
let label = i === 0 ? (_a = cfg.rootLabel) != null ? _a : "All" : crumb.label;
if (typeof cfg.formatter === "function") {
label = cfg.formatter(label, {
index: i,
depth: crumbs.length - 1,
data: crumb.data
});
}
if (i === crumbs.length - 1) {
const cur = BrowserAPIs.createElementNS(XHTML$3, "span");
cur.setAttribute(
"class",
"apexcharts-breadcrumb-item apexcharts-breadcrumb-current"
);
cur.setAttribute("aria-current", "page");
cur.textContent = String(label);
nav.appendChild(cur);
return;
}
const btn = (
/** @type {HTMLButtonElement} */
BrowserAPIs.createElementNS(XHTML$3, "button")
);
btn.setAttribute("type", "button");
btn.setAttribute("class", "apexcharts-breadcrumb-item");
if (i === 0) {
const arrow = BrowserAPIs.createElementNS(XHTML$3, "span");
arrow.setAttribute("class", "apexcharts-breadcrumb-arrow");
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "←";
btn.appendChild(arrow);
}
const text = BrowserAPIs.createElementNS(XHTML$3, "span");
text.setAttribute("class", "apexcharts-breadcrumb-label");
text.textContent = String(label);
btn.appendChild(text);
btn.addEventListener("click", () => opts.onNavigate(i, crumb));
nav.appendChild(btn);
});
elWrap.appendChild(nav);
return nav;
}
function positionBreadcrumb(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 XHTML$2 = "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(w2, ctx, drilldown) {
this.w = w2;
this.ctx = ctx;
this.drilldown = drilldown;
}
/**
* @param {Array<string|number>} path - ['root', id, id, ...]
*/
render(path) {
if (!Environment.isBrowser()) return;
const w2 = this.w;
const elWrap = w2.dom.elWrap;
if (!elWrap) return;
const existing = elWrap.querySelector(".apexcharts-breadcrumb");
if (existing && existing.parentNode) {
existing.parentNode.removeChild(existing);
}
const cfg = w2.config.drilldown && w2.config.drilldown.breadcrumb;
if (!cfg || cfg.show === false) return;
if (this.drilldown.depth === 0) return;
const nav = BrowserAPIs.createElementNS(XHTML$2, "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$2, "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$2, "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$2, "button")
);
btn.setAttribute("type", "button");
btn.setAttribute("class", "apexcharts-breadcrumb-item");
if (i === 0) {
const arrow = BrowserAPIs.createElementNS(XHTML$2, "span");
arrow.setAttribute("class", "apexcharts-breadcrumb-arrow");
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "←";
btn.appendChild(arrow);
}
const text = BrowserAPIs.createElementNS(XHTML$2, "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);
if (this.w.globals.axisCharts) {
placeInReservedBand(this.w, this.ctx, nav, cfg);
}
this._avoidChromeOverlap(nav);
}
/**
* The breadcrumb is an absolute overlay, so at its default top-left it can
* sit on top of a left-aligned title (or subtitle). After mounting, push it
* below any chart chrome it intersects. (Sunburst's self-contained
* breadcrumb applies the same rule.)
* @param {HTMLElement} nav
*/
_avoidChromeOverlap(nav) {
const w2 = this.w;
const chrome = (
/** @type {Element[]} */
[".apexcharts-title-text", ".apexcharts-subtitle-text"].map((s) => w2.dom.baseEl.querySelector(s)).filter((el) => el !== null)
);
if (!chrome.length) return;
const wrapTop = w2.dom.elWrap.getBoundingClientRect().top;
for (let pass = 0; pass < chrome.length + 1; pass++) {
const nr = nav.getBoundingClientRect();
const hit = chrome.find((el) => {
const r = el.getBoundingClientRect();
return nr.left < r.right && nr.right > r.left && nr.top < r.bottom && nr.bottom > r.top;
});
if (!hit) break;
nav.style.top = `${hit.getBoundingClientRect().bottom - wrapTop + 4}px`;
}
}
/**
* @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 XHTML$1 = "http://www.w3.org/1999/xhtml";
const CLASS = "apexcharts-drilldown-loading";
class DrilldownLoading {
/**
* @param {import('../../types/internal').ChartStateW} w
*/
constructor(w2) {
this.w = w2;
this.el = null;
}
/** @returns {any} the drilldown.loading config, normalised. */
_cfg() {
const d = this.w.config.drilldown;
const l = d && d.loading;
if (l === false) return { show: false };
return l || {};
}
/**
* Mount the overlay. No-op when disabled, outside a browser, or already up.
*/
show() {
if (!Environment.isBrowser()) return;
const cfg = this._cfg();
if (cfg.show === false) return;
const elWrap = this.w.dom.elWrap;
if (!elWrap) return;
this.hide();
const box = BrowserAPIs.createElementNS(XHTML$1, "div");
box.setAttribute("class", CLASS);
box.setAttribute("role", "status");
box.setAttribute("aria-live", "polite");
box.setAttribute("aria-label", cfg.text || "Loading");
const spinner = BrowserAPIs.createElementNS(XHTML$1, "div");
spinner.setAttribute("class", `${CLASS}-spinner`);
spinner.setAttribute("aria-hidden", "true");
box.appendChild(spinner);
if (cfg.text) {
const label = BrowserAPIs.createElementNS(XHTML$1, "span");
label.setAttribute("class", `${CLASS}-text`);
label.textContent = cfg.text;
box.appendChild(label);
}
elWrap.appendChild(box);
this.el = box;
}
/** Remove the overlay. Safe to call when it is not mounted. */
hide() {
const elWrap = this.w.dom.elWrap;
if (elWrap) {
const nodes = elWrap.querySelectorAll(`.${CLASS}`);
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
if (n.parentNode) n.parentNode.removeChild(n);
}
} else if (this.el && this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
}
this.el = null;
}
}
const MAX_DEPTH = 32;
const DRILL_MARKER = "__apexDrilldownMarker";
class Drilldown {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
this.stack = [];
this.rootSnapshot = null;
this._wired = false;
this._asyncCache = /* @__PURE__ */ new Map();
this._pending = null;
this._warnedUnreachable = false;
this._warnedNoSliceOffset = false;
this.breadcrumb = new Breadcrumb(w2, ctx, this);
this.loading = new DrilldownLoading(w2);
this._onPointSelect = this._onPointSelect.bind(this);
this._afterRender = this._afterRender.bind(this);
this._onPlotDown = this._onPlotDown.bind(this);
this._onPlotClick = this._onPlotClick.bind(this);
this._downAt = null;
this._plotClickWired = null;
this.init();
}
init() {
const w2 = this.w;
if (!w2.config.drilldown || !w2.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);
if (w2.config.markers) {
w2.config.markers.discrete = this._drillMarkers(w2.config.series);
}
}
// ─── 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 {{ seriesIndex?: number, dataPointIndex?: number }} [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
});
}
/**
* Resolve a level through `onDrillDown` and drill into it.
*
* Failure never changes state: on a throw, a rejection, or a resolver that
* hands back something undrillable, the chart stays exactly where it was and
* `drillDownError` fires. That is what makes this usable against a real
* backend, where a fetch failing is ordinary rather than exceptional.
*
* @param {string|number|null} id
* @param {any} point
* @param {{ seriesIndex?: number, dataPointIndex?: number }} [meta]
* @returns {Promise<any>}
*/
_drillDownAsync(id, point, meta) {
const cfg = this.w.config.drilldown;
const fn = cfg.onDrillDown;
const cached = this._cacheGet(id);
if (cached) return this._drillInto(cached, point, meta);
if (this._pending) return this._pending;
let result;
this.loading.show();
try {
result = fn({
// `id` was missing here, so a resolver could not tell WHICH level was
// asked for without re-deriving it from the point. It is the first
// thing a real implementation needs (`fetch('/levels/' + id)`).
id,
point,
seriesIndex: meta && meta.seriesIndex,
dataPointIndex: meta && meta.dataPointIndex
});
} catch (error) {
this.loading.hide();
this._fire("drillDownError", { id, error });
return Promise.resolve(this.ctx);
}
const settle = () => {
this._pending = null;
this.loading.hide();
};
const p2 = Promise.resolve(result).then(
(child) => {
settle();
if (this._isDead()) return this.ctx;
if (!child || !child.data) {
this._fire("drillDownError", {
id,
error: new Error(
`drilldown: onDrillDown resolved without a drillable level for id "${id}" (expected an object with a \`data\` array).`
)
});
return this.ctx;
}
const level = child.id != null ? child : __spreadProps(__spreadValues({}, child), { id });
this._cacheSet(id, level);
return this._drillInto(level, point, meta);
},
(error) => {
settle();
if (this._isDead()) return this.ctx;
this._fire("drillDownError", { id, error });
return this.ctx;
}
);
this._pending = p2;
return p2;
}
/**
* Whether the chart was torn down while a resolver was in flight.
*
* Clicking to drill and then navigating away is ordinary, not exceptional: a
* component unmounts, `destroy()` runs, and the fetch settles afterwards.
* Without this the resolved level would be applied to a destroyed chart,
* which throws out of `updateOptions` and surfaces in the host app as an
* unhandled rejection from a click the user has already forgotten about.
*
* @returns {boolean}
*/
_isDead() {
const w2 = this.w;
return !w2 || !w2.globals || w2.globals.isDestroyed === true;
}
/** @returns {boolean} whether resolved async levels are cached. */
_cacheEnabled() {
const cfg = this.w.config.drilldown;
return !!(cfg && cfg.cache !== false);
}
/**
* @param {string|number|null} id
* @returns {any|null}
*/
_cacheGet(id) {
if (!this._cacheEnabled() || id == null) return null;
return this._asyncCache.get(id) || null;
}
/**
* @param {string|number|null} id
* @param {any} level
*/
_cacheSet(id, level) {
if (!this._cacheEnabled() || id == null) return;
this._asyncCache.set(id, level);
}
/**
* Drop cached async levels, so the next drill re-runs `onDrillDown`. Call it
* when the underlying data changes behind a chart that has already drilled.
* @param {string|number} [id] a single level, or every level when omitted
* @returns {any} the chart, for chaining
*/
clearCache(id) {
if (id == null) this._asyncCache.clear();
else this._asyncCache.delete(id);
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: this._uncollapseSeries(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;
}
/**
* Restore any legend-collapsed slices/series to their original values in a
* cloned series array, so a drill snapshot captures the pre-collapse data.
* Mirrors legend Helpers' collapse addressing: object-form pie/donut packs
* every slice as a data point inside `series[0].data`; numeric pie stores a
* slice per top-level element; axis series carry a `data` array. No-op when
* nothing is collapsed.
* @param {any[]} series
* @returns {any[]}
*/
_uncollapseSeries(series) {
const w2 = this.w;
const gl = w2.globals;
const entries = [
...gl.collapsedSeries || [],
...gl.ancillaryCollapsedSeries || []
];
if (!entries.length) return series;
const type = w2.config.chart.type;
const objectFormPie = (type === "pie" || type === "donut" || type === "polarArea") && series.length === 1 && series[0] && typeof series[0] === "object" && Array.isArray(series[0].data);
const container = objectFormPie ? series[0].data : series;
for (const entry of entries) {
const i = entry.index;
if (gl.axisCharts) {
if (series[i]) {
series[i].data = Array.isArray(entry.data) ? entry.data.slice() : entry.data;
}
} else if (container[i] && typeof container[i] === "object") {
container[i].y = entry.data;
} else if (container[i] !== void 0) {
container[i] = entry.data;
}
}
return series;
}
/**
* 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;
}
/**
* Copy the optional view fields shared by a drilldown child level and a
* restore snapshot (`xaxis`, `yaxis`, `colors`, `plotOptions`, `fill`,
* `legend`) from `src` onto `view`, only when present.
* @param {Record<string, any>} view @param {Record<string, any>} src
*/
_copyOptionalViewFields(view, src) {
if (src.xaxis) view.xaxis = src.xaxis;
if (src.yaxis) view.yaxis = src.yaxis;
if (src.colors) view.colors = src.colors;
if (src.plotOptions) view.plotOptions = src.plotOptions;
if (src.fill) view.fill = src.fill;
if (src.legend) view.legend = src.legend;
}
/**
* 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;
this._copyOptionalViewFields(view, child);
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;
this._copyOptionalViewFields(view, snap);
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 w2 = this.w;
w2.interact.selectedDataPoints = [];
w2.globals.collapsedSeries = [];
w2.globals.collapsedSeriesIndices = [];
w2.globals.ancillaryCollapsedSeries = [];
w2.globals.ancillaryCollapsedSeriesIndices = [];
w2.globals.allSeriesCollapsed = false;
w2.globals.risingSeries = [];
view.markers = __spreadProps(__spreadValues({}, view.markers || {}), {
discrete: this._drillMarkers(view.series)
});
const animate = (!w2.config.drilldown.animation || w2.config.drilldown.animation.enabled !== false) && w2.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() {
const w2 = this.w;
if (!w2.config.drilldown || !w2.config.drilldown.enabled) return;
this._markDrillableTargets();
this._wirePlotClick();
this.breadcrumb.render(this.path);
if (w2.config.markers) {
w2.config.markers.discrete = this._drillMarkers(w2.config.series);
}
}
/**
* Mark every point that carries a `drilldown` field as an openable target.
*
* Two things have to be true for a point to be drillable, and on line/area
* neither holds by default. It needs a mark to click (with `markers.size: 0`
* there is no element at all), and that mark has to accept the click: core
* gives line/area markers `no-pointer-events` so the shared tooltip can track
* the whole plot, which silently swallows it. `_drillMarkers()` supplies the
* missing dots; this re-enables pointer events on them.
*
* The cursor class only goes on marks that can actually take the click, so we
* never promise an interaction that cannot happen.
*/
_markDrillableTargets() {
if (!Environment.isBrowser()) return;
const w2 = this.w;
const baseEl = w2.dom.baseEl;
const series = w2.config.series;
if (!baseEl || !Array.isArray(series)) return;
let unreachable = 0;
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}"]`);
if (!nodes.length) unreachable++;
nodes.forEach((node) => {
if (this._isClickThroughMark(node)) {
node.classList.remove("no-pointer-events");
}
node.classList.add("apexcharts-drilldown-target");
});
});
});
if (unreachable && !this._warnedUnreachable) {
this._warnedUnreachable = true;
console.warn(
`ApexCharts: ${unreachable} drillable point(s) have no clickable mark, so clicking them cannot do anything. Leave \`drilldown.marker\` on, or give the series markers of its own (\`markers.size > 0\`).`
);
}
}
/**
* Called by Pie when it declines to wire the slice pull-out because this
* chart drills. Warned once per chart (a drill re-renders, and the same
* notice on every navigation is just noise), and from here rather than from
* Pie because this module is the reason it is unavailable.
*/
warnSliceOffsetDisabled() {
if (this._warnedNoSliceOffset) return;
this._warnedNoSliceOffset = true;
console.warn(
"ApexCharts: `plotOptions.pie.expandOnClick` is not available in a drilldown pie/donut, so it was ignored. A slice click navigates, and a slice that slid out would be discarded by the drill it just triggered."
);
}
/**
* Make the whole band a drillable point owns clickable, not just its dot.
*
* A dot is ~6px across, so hitting it takes pixel-precise aim, it is far under
* the ~44px a finger needs, and the tooltip's arrow points AT the point by
* design, which puts a triangle over the very thing you are aiming at. Rather
* than move the tooltip, widen the target: a click anywhere in the plot drills
* whichever point the tooltip is currently reading. The hit area then matches
* the feedback already on screen, so "the tooltip says 2024, I click, I get
* 2024" holds, and the dot goes back to being an affordance rather than a
* target you have to chase.
*
* Only for the point-based types, since a bar, slice or tile is already a
* comfortably large mark and drilling one by clicking the background near it
* would be surprising.
*/
_wirePlotClick() {
if (!Environment.isBrowser()) return;
const baseEl = this.w.dom.baseEl;
if (!baseEl || this._plotClickWired === baseEl) return;
if (this._plotClickWired) {
this._plotClickWired.removeEventListener("mousedown", this._onPlotDown);
this._plotClickWired.removeEventListener("click", this._onPlotClick);
}
baseEl.addEventListener("mousedown", this._onPlotDown);
baseEl.addEventListener("click", this._onPlotClick);
this._plotClickWired = baseEl;
}
/** @param {any} e */
_onPlotDown(e) {
this._downAt = { x: e.clientX, y: e.clientY };
}
/**
* @param {any} e
* @returns {any}
*/
_onPlotClick(e) {
const w2 = this.w;
if (!w2.config.drilldown || !w2.config.drilldown.enabled) return void 0;
const down = this._downAt;
this._downAt = null;
if (down && Math.hypot(e.clientX - down.x, e.clientY - down.y) > 4) {
return void 0;
}
const target = (
/** @type {Element} */
e.target
);
if (!target || typeof target.closest !== "function") return void 0;
if (target.closest(".apexcharts-drilldown-target")) return void 0;
if (target.closest(
".apexcharts-legend, .apexcharts-toolbar, .apexcharts-breadcrumb, .apexcharts-menu, .apexcharts-tooltip"
)) {
return void 0;
}
const i = w2.interact.capturedSeriesIndex;
const j = w2.interact.capturedDataPointIndex;
if (i == null || j == null || i < 0 || j < 0) return void 0;
if (!this._isPointBasedSeries(w2.config.series[i])) return void 0;
const point = this._pointAt(i, j);
if (!point || typeof point !== "object" || point.drilldown == null) {
return void 0;
}
return this.drillDown(point.drilldown, point, {
seriesIndex: i,
dataPointIndex: j
});
}
/**
* A series mark that is deliberately click-through. Restricted to markers
* inside the plot: the tooltip draws its own `no-pointer-events` marker, and
* that one must stay click-through or it would sit under the cursor and eat
* the hover it exists to follow.
* @param {Element} node
* @returns {boolean}
*/
_isClickThroughMark(node) {
if (!node.classList || !node.classList.contains("no-pointer-events")) {
return false;
}
if (!node.classList.contains("apexcharts-marker")) return false;
return !(typeof node.closest === "function" && node.closest(".apexcharts-tooltip"));
}
/**
* Discrete-marker entries that give each drillable point a visible dot.
*
* Only series drawn WITHOUT markers get them, so an author who already shows
* markers keeps their styling untouched, and only drillable points get one, so
* the dots read as "these are the ones you can open" rather than turning every
* point into a dot. Core renders discrete markers even when `markers.size` is
* 0, which is what makes the affordance possible without a core change.
*
* Entries are tagged so a resync replaces ours and leaves the author's alone.
* @param {any[]} series - the series being rendered (a drill applies its
* level's series, which are not yet on `w.config` when this runs)
* @returns {any[]}
*/
_drillMarkers(series) {
const w2 = this.w;
const cfg = w2.config.drilldown;
const authored = Array.isArray(w2.config.markers && w2.config.markers.discrete) ? w2.config.markers.discrete.filter(
(d) => !d || !d[DRILL_MARKER]
) : [];
const mk = cfg && cfg.marker || {};
if (mk.show === false || !Array.isArray(series)) return authored;
const own = [];
series.forEach((s, i) => {
if (!this._seriesNeedsDrillMarker(i, s)) return;
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 entry = { seriesIndex: i, dataPointIndex: j, [DRILL_MARKER]: true };
if (mk.size !== void 0) entry.size = mk.size;
if (mk.shape !== void 0) entry.shape = mk.shape;
if (mk.fillColor !== void 0) entry.fillColor = mk.fillColor;
if (mk.strokeColor !== void 0) entry.strokeColor = mk.strokeColor;
own.push(entry);
});
});
return authored.concat(own);
}
/**
* Whether a series needs drill dots supplied for it: a point-based type whose
* marks are the markers, drawn with markers off. Bar, pie, treemap and heatmap
* marks are already real clickable elements, and a series that already shows
* markers already has its affordance.
* @param {number} i @param {any} s
* @returns {boolean}
*/
_seriesNeedsDrillMarker(i, s) {
if (!this._isPointBasedSeries(s)) return false;
const size = this.w.config.markers && this.w.config.markers.size;
const effective = Array.isArray(size) ? size[i] : size;
return !(Number(effective) > 0);
}
/**
* A series whose marks are markers (a point), rather than a shape big enough
* to aim at on its own.
* @param {any} s
* @returns {boolean}
*/
_isPointBasedSeries(s) {
const type = s && s.type || this.w.config.chart.type;
return type === "line" || type === "area";
}
}
_core__default.registerFeatures({ drilldown: Drilldown });
const REGISTRY_KEY = "__apexcharts_plugins__";
function getRegistry() {
const g2 = (
/** @type {any} */
globalThis
);
if (!g2[REGISTRY_KEY]) g2[REGISTRY_KEY] = {};
return g2[REGISTRY_KEY];
}
function getPlugin(name) {
return getRegistry()[name] || null;
}
const WEAVE_API_VERSION = 1;
const PLUGIN_CHART_METHODS = [
"updateOptions",
"updateSeries",
"appendData",
"appendSeries",
"toggleSeries",
"showSeries",
"hideSeries",
"highlightSeries",
"isSeriesHidden",
"zoomX",
"addXaxisAnnotation",
"addYaxisAnnotation",
"addPointAnnotation",
"clearAnnotations",
"removeAnnotation",
"dataURI",
"exportToCSV"
];
function buildBoundPublicMethods(ctx) {
const out = {};
PLUGIN_CHART_METHODS.forEach((m2) => {
if (typeof ctx[m2] === "function") out[m2] = ctx[m2].bind(ctx);
});
return Object.freeze(out);
}
function makeLayerHandle(g2, graphics) {
const add = (el) => {
if (el) g2.add(el);
return el;
};
const handle = {
get node() {
return g2.node;
},
/** @param {any} opts */
path(opts = {}) {
const {
d = "",
stroke = "#000",
width = 1,
fill = "none",
opacity = 1,
dash = 0,
className = ""
} = opts;
return add(
graphics.drawPath({
d,
stroke,
strokeWidth: width,
fill,
fillOpacity: fill === "none" ? 0 : opacity,
strokeOpacity: opacity,
strokeDashArray: dash,
classes: className
})
);
},
/** @param {any} opts */
line(opts = {}) {
const { x1, y1, x2, y2, stroke = "#000", width = 1, dash = 0 } = opts;
return add(graphics.drawLine(x1, y1, x2, y2, stroke, dash, width));
},
/** @param {any} opts */
rect(opts = {}) {
const {
x = 0,
y = 0,
w: w2 = 0,
h = 0,
r = 0,
fill = "#000",
stroke = null,
opacity = 1
} = opts;
return add(
graphics.drawRect(
x,
y,
w2,
h,
r,
fill,
opacity,
stroke != null ? 1 : null,
stroke
)
);
},
/** @param {any} opts */
circle(opts = {}) {
const { cx = 0, cy = 0, r = 0, fill = "#000", stroke = null } = opts;
return add(
graphics.drawCircle(r, { cx, cy, fill, stroke: stroke || "none" })
);
},
/** @param {any} opts */
text(opts = {}) {
const {
x = 0,
y = 0,
text = "",
color,
size,
anchor = "start",
weight
} = opts;
return add(
graphics.drawText({
x,
y,
text,
textAnchor: anchor,
fontSize: size,
foreColor: color,
fontWeight: weight
})
);
},
clear() {
const node = g2.node;
while (node.firstChild) node.removeChild(node.firstChild);
return handle;
}
};
return handle;
}
function buildPluginAPI(host, record) {
const ctx = host.ctx;
const w2 = host.w;
const api = {
name: record.def.name,
version: WEAVE_API_VERSION,
// Live: reconcile refreshes record.options when the chart's plugins config
// changes, so updateOptions({ plugins: [{ name, options }] }) reconfigures
// an active plugin in place. The returned object is frozen.
get options() {
return record.options;
},
// ── lifecycle subscription ──
/**
* @param {string} hook
* @param {Function} fn
*/
on(hook, fn) {
const m2 = record.handlers;
if (!m2.has(hook)) m2.set(hook, []);
m2.get(hook).push(fn);
return api;
},
/**
* @param {string} hook
* @param {Function} fn
*/
off(hook, fn) {
const a = record.handlers.get(hook);
if (a) {
const i = a.indexOf(fn);
if (i > -1) a.splice(i, 1);
}
return api;
},
// ── per-plugin, per-chart scratch state (survives updates, dropped on
// destroy). The api object is frozen, but this object is mutable. ──
store: {},
// ── drawing (renderer-agnostic) ──
// Call this INSIDE each draw handler: the host wipes plugin layers at the
// start of every draw pass, so a handle cached across draws points at a
// detached node and its writes vanish silently.
/** @param {any} [opts] */
layer(opts) {
return host._layer(record.def.name, opts || {});
},
// ── reads ──
get scales() {
return host._currentScales;
},
// Served from the per-dispatch snapshot when one exists (invalidated at
// every dispatch), so reading api.data in a loop does not rebuild the
// point arrays on each property access.
get data() {
return host._lastData || (host._lastData = host._dataSnapshot());
},
theme: Object.freeze({
get mode() {
return w2.config.theme.mode;
},
get foreColor() {
return w2.config.chart.foreColor;
},
/** @param {number} i */
seriesColor(i) {
return w2.globals.colors[i];
},
/** @param {string} name */
token(name) {
return host._token(name);
}
}),
// ── curated actions (bound public methods only; NEVER raw w) ──
chart: buildBoundPublicMethods(ctx),
// ── custom events out to the host app ──
/**
* Fires as `plugin:<pluginName>:<name>` on the chart's event bus. The
* namespace is not optional: the bus also carries the internal lifecycle
* events ('updated', 'mounted', ...), and an un-namespaced emit could
* trigger every internal subscriber (history capture, re-render hooks).
* Listen with chart.addEventListener('plugin:myplugin:myevent', fn).
* @param {string} name
* @param {any} [detail]
*/
emit(name, detail) {
ctx.events.fireEvent(`plugin:${record.def.name}:${name}`, [ctx, detail]);
},
// ── host element (read; lazy: baseEl is not set until render) ──
get el() {
return w2.dom.baseEl;
}
};
return Object.freeze(api);
}
class WeaveHost {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
this.active = [];
this._layers = /* @__PURE__ */ new Map();
this._currentScales = null;
this._lastPluginsRef = null;
this._lastData = null;
this._updatedWired = false;
this._onUpdated = this._onUpdated.bind(this);
this._init();
}
_init() {
const list = this.w.config.plugins || [];
list.map((entry, i) => ({
entry,
order: entry.order != null ? entry.order : i
})).sort((a, b2) => a.order - b2.order).forEach((o) => this._activate(o.entry));
this._lastPluginsRef = this.w.config.plugins;
this._wireUpdated();
}
_wireUpdated() {
if (this._updatedWired) return;
this.ctx.addEventListener("updated", this._onUpdated);
this._updatedWired = true;
}
_onUpdated() {
this.dispatch("afterUpdate", { pass: "update" });
}
/**
* @param {any} entry { name, options?, order? }
*/
_activate(entry) {
const def = getPlugin(entry.name);
if (!def) {
console.error(`[apexcharts] plugin "${entry.name}" is not registered.`);
return;
}
const v2 = def.apiVersion != null ? def.apiVersion : 1;
if (Math.trunc(v2) !== WEAVE_API_VERSION) {
console.error(
`[apexcharts] plugin "${def.name}" targets Weave API v${v2}, host is v${WEAVE_API_VERSION}; skipped.`
);
return;
}
const record = {
def,
options: Object.freeze(__spreadValues({}, entry.options || {})),
handlers: /* @__PURE__ */ new Map(),
disabled: false,
failures: 0,
api: null
};
record.api = buildPluginAPI(this, record);
this.active.push(record);
this._guard(record, "setup", () => def.setup(record.api));
}
/**
* @param {string} hook
* @param {{ pass?: string, xyRatios?: any }} [extra]
*/
dispatch(hook, extra) {
if (hook === "draw") {
this._reconcile();
this._resetLayers();
}
this._lastData = null;
if (!this.active.length) return;
if (hook === "afterParse") {
this._currentScales = null;
} else if (extra && "xyRatios" in extra) {
this._setScales(extra.xyRatios);
}
const pass = extra && extra.pass || "full";
let data = null;
for (const record of this.active) {
if (record.disabled) continue;
const fns = record.handlers.get(hook);
if (!fns || !fns.length) continue;
if (data === null) {
data = this._dataSnapshot();
this._lastData = data;
}
const payload = {
api: record.api,
scales: this._currentScales,
data,
pass,
hook
};
for (const fn of fns.slice()) {
this._guard(record, hook, () => fn(payload));
}
}
}
/**
* @param {any} record
* @param {string} where
* @param {Function} fn
*/
_guard(record, where, fn) {
if (record.disabled) return;
try {
fn();
} catch (e) {
console.error(
`[apexcharts] plugin "${record.def.name}" threw in "${where}":`,
e
);
record.failures = (record.failures || 0) + 1;
if (record.failures >= 3) {
record.disabled = true;
console.error(
`[apexcharts] plugin "${record.def.name}" disabled after repeated errors.`
);
}
}
}
// ─── Scales facade ──────────────────────────────────────────────────────
/**
* Build api.scales from the SAME xyRatios the series were drawn with, so
* plugin pixels align with series pixels by construction.
* @param {any} xyRatios
*/
_setScales(xyRatios) {
const w2 = this.w;
const gl = w2.globals;
const L2 = w2.layout;
if (!xyRatios || !gl.axisCharts) {
this._currentScales = null;
return;
}
const xRatio = xyRatios.xRatio;
const yRatio = xyRatios.yRatio || [];
const yr = (axis) => yRatio[axis] != null ? yRatio[axis] : yRatio[0];
const maxY = (axis) => gl.maxYArr[axis] != null ? gl.maxYArr[axis] : gl.maxY;
const minY = (axis) => gl.minYArr[axis] != null ? gl.minYArr[axis] : gl.minY;
this._currentScales = {
/** @param {number} v */
x: (v2) => L2.translateX + (v2 - gl.minX) / xRatio,
/**
* @param {number} v
* @param {number} [axis]
*/
y: (v2, axis = 0) => L2.translateY + (maxY(axis) - v2) / yr(axis),
domainX: [gl.minX, gl.maxX],
/** @param {number} [axis] */
domainY: (axis = 0) => [minY(axis), maxY(axis)],
gridWidth: L2.gridWidth,
gridHeight: L2.gridHeight,
ratios: xyRatios
};
}
// ─── Read-only data snapshot ────────────────────────────────────────────
/**
* @returns {any[]} defensive per-series snapshot (never the live slice)
*/
_dataSnapshot() {
const w2 = this.w;
const gl = w2.globals;
const series = w2.seriesData.series || [];
const seriesX = w2.seriesData.seriesX || [];
return series.map((sData, i) => {
const xs = seriesX[i] || [];
const points = (sData || []).map((y, j) => ({
x: xs[j] != null ? xs[j] : j,
y
}));
return {
name: gl.seriesNames ? gl.seriesNames[i] : void 0,
hidden: (gl.collapsedSeriesIndices || []).includes(i),
color: gl.colors ? gl.colors[i] : void 0,
points
};
});
}
// ─── Theme tokens ───────────────────────────────────────────────────────
/**
* @param {string} name
* @returns {any}
*/
_token(name) {
const w2 = this.w;
const gl = w2.globals;
switch (name) {
case "foreColor":
return w2.config.chart.foreColor;
case "background":
return w2.config.chart.background;
case "accent":
case "primary":
return gl.colors ? gl.colors[0] : void 0;
default:
if (/^series-\d+$/.test(name)) {
return gl.colors ? gl.colors[Number(name.split("-")[1])] : void 0;
}
return void 0;
}
}
// ─── Layers ─────────────────────────────────────────────────────────────
/**
* @param {string} name
* @param {{ z?: 'front'|'behind', className?: string }} opts
*/
_layer(name, { z = "front", className = "" } = {}) {
let g2 = this._layers.get(name);
if (!g2) {
g2 = this.ctx.graphics.group({
class: `apexcharts-plugin-${name} ${className}`.trim()
});
const parent = this.w.dom.elGraphical.node;
if (z === "behind") parent.insertBefore(g2.node, parent.firstChild);
else parent.appendChild(g2.node);
g2.node.setAttribute("aria-hidden", "true");
this._layers.set(name, g2);
}
return makeLayerHandle(g2, this.ctx.graphics);
}
/**
* Remove all plugin layers. Run at the start of every `draw` because
* fastUpdate only removes series/data-label groups (not arbitrary plugin
* groups), so without this, fast-path redraws would duplicate plugin output.
*/
_resetLayers() {
const el = this.w.dom.elGraphical;
const parent = el && el.node;
if (parent) {
const groups = parent.querySelectorAll('g[class*="apexcharts-plugin-"]');
Array.prototype.forEach.call(groups, (n) => n.remove());
}
this._layers.clear();
}
// ─── Config-change reconciliation ───────────────────────────────────────
/**
* Diff w.config.plugins by name: teardown removed, activate added; unchanged
* plugins keep their instance + store, but their `options` are refreshed from
* the new entry (api.options is a live getter), so
* updateOptions({ plugins: [{ name, options }] }) reconfigures in place.
* Skipped when the plugins array reference is unchanged (fast redraws), so it
* costs nothing on hover.
*/
_reconcile() {
const plugins = this.w.config.plugins || [];
if (plugins === this._lastPluginsRef) return;
this._lastPluginsRef = plugins;
const desired = new Map(
plugins.map((e, i) => [
e.name,
{ entry: e, order: e.order != null ? e.order : i }
])
);
for (let i = this.active.length - 1; i >= 0; i--) {
const r = this.active[i];
const want = desired.get(r.def.name);
if (!want) {
this._guard(r, "destroy", () => r.def.destroy && r.def.destroy(r.api));
this.active.splice(i, 1);
} else {
r.options = Object.freeze(__spreadValues({}, want.entry.options || {}));
}
}
const activeNames = new Set(this.active.map((r) => r.def.name));
const toAdd = [];
desired.forEach((v2, name) => {
if (!activeNames.has(name)) toAdd.push(v2);
});
toAdd.sort((a, b2) => a.order - b2.order).forEach((v2) => this._activate(v2.entry));
}
/**
* @param {boolean} [isUpdating]
*/
teardown(isUpdating) {
if (!isUpdating) {
this.dispatch("destroy");
for (const record of this.active) {
this._guard(record, "destroy", () => record.def.destroy && record.def.destroy(record.api));
}
this.active = [];
if (this._updatedWired) {
this.ctx.removeEventListener && this.ctx.removeEventListener("updated", this._onUpdated);
this._updatedWired = false;
}
}
this._layers.clear();
}
}
_core__default.registerFeatures({ weave: WeaveHost });
function seriesEmitter(ctx, graphics) {
const r = ctx && ctx.renderer;
return r && r.kind && r.kind !== "svg" ? r : graphics;
}
function makeCustomSeriesClass(name, def) {
const cls = class CustomSeries {
/**
* @param {any} w @param {any} ctx @param {any} xyRatios
*/
constructor(w2, ctx, xyRatios) {
this.w = w2;
this.ctx = ctx;
this.xyRatios = xyRatios;
this._warned = false;
}
/**
* @param {any[]} series parsed y-arrays (one per drawn series)
* @param {string} [_ctype]
* @param {number[]} [seriesIndices] realIndex per entry (combo dispatch)
* @returns {any} the wrap group
*/
draw(series, _ctype, seriesIndices) {
var _a, _b;
const w2 = this.w;
const graphics = new Graphics(w2, this.ctx);
const emit = seriesEmitter(this.ctx, graphics);
const ret = graphics.group({ class: "apexcharts-marks-series" });
series.forEach((_s, idx) => {
var _a2;
const realIndex = Array.isArray(seriesIndices) ? seriesIndices[idx] : idx;
const elSeries = graphics.group({
class: "apexcharts-series",
rel: realIndex + 1,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[realIndex]),
"data:realIndex": realIndex
});
const scales = this._scales(
realIndex,
(w2.seriesData.series[realIndex] || []).length
);
const color = w2.globals.colors[realIndex];
const rawData = (
/** @type {any} */
((_a2 = w2.config.series[realIndex]) == null ? void 0 : _a2.data) || []
);
const xvals = w2.seriesData.seriesX[realIndex] || [];
const yvals = w2.seriesData.series[realIndex] || [];
w2.globals.seriesXvalues[realIndex] = [];
w2.globals.seriesYvalues[realIndex] = [];
if (typeof w2.globals.pointsArray[realIndex] === "undefined") {
w2.globals.pointsArray[realIndex] = [];
}
for (let j = 0; j < yvals.length; j++) {
const yVal = yvals[j];
if (yVal === null || typeof yVal === "undefined") continue;
const xVal = xvals[j];
const xPx = scales.xAt(j, xVal);
const yPx = scales.y(yVal);
const api = this._api(emit, elSeries, realIndex, j);
try {
def.renderItem({
datum: rawData[j],
x: xPx,
y: yPx,
scales,
api,
seriesIndex: realIndex,
dataPointIndex: j,
color
});
} catch (e) {
if (!this._warned) {
console.warn(
`[apexcharts] renderItem for series type "${name}" threw; skipping datum:`,
e
);
this._warned = true;
}
}
w2.globals.seriesXvalues[realIndex][j] = xPx;
w2.globals.seriesYvalues[realIndex][j] = yPx;
w2.globals.pointsArray[realIndex][j] = [xPx, yPx];
}
graphics.setupEventDelegation(elSeries, ".apexcharts-marks-mark");
ret.add(elSeries);
});
(_b = (_a = this.ctx.animations) == null ? void 0 : _a.animationCompleted) == null ? void 0 : _b.call(_a, ret);
return ret;
}
/**
* Series-space (elGraphical-local, translate-free) scales, matching how the
* built-ins compute pixels, so custom marks align with axes and gridlines
* and paint correctly on the elGraphical-local canvas.
* @param {number} realIndex
* @param {number} [nPts] number of data points (for categorical band sizing)
*/
_scales(realIndex, nPts) {
var _a, _b, _c, _d;
const gl = this.w.globals;
const cnf = this.w.config;
const xRatio = this.xyRatios.xRatio;
const yRatioArr = this.xyRatios.yRatio;
const axis = (_b = (_a = gl.seriesYAxisReverseMap) == null ? void 0 : _a[realIndex]) != null ? _b : 0;
const yr = Array.isArray(yRatioArr) ? (_c = yRatioArr[axis]) != null ? _c : yRatioArr[0] : yRatioArr;
const maxYArr = (
/** @type {any} */
gl.maxYArr
);
const maxY = Array.isArray(maxYArr) && maxYArr.length ? (_d = maxYArr[axis]) != null ? _d : gl.maxY : gl.maxY;
const gridWidth = gl.gridWidth;
const gridHeight = gl.gridHeight;
const catMode = !gl.isXNumeric;
const n = nPts || gl.dataPoints || 1;
const bandW = n > 0 ? gridWidth / n : gridWidth;
const tickOn = cnf.xaxis.tickPlacement === "on";
const x = (v2) => xRatio ? (v2 - gl.minX) / xRatio : gridWidth / 2;
const y = (v2) => (maxY - v2) / yr;
const xAt = (index, v2) => {
if (!catMode) return x(v2);
if (tickOn && n > 1) return index / (n - 1) * gridWidth;
return (index + 0.5) * bandW;
};
const step = gl.minXDiff || 1;
const band = catMode ? bandW : xRatio ? step / xRatio : gridWidth;
return {
x,
xAt,
y,
gridWidth,
gridHeight,
band
};
}
/**
* Per-datum primitive API. Each call emits immediately (canvas-aware via
* `emit`), tags the node with the datum's identity, and adds it to the
* series group; on canvas the tag/add are inert (marks live on the canvas,
* events are coordinate-based).
* @param {any} emit @param {any} elSeries @param {number} realIndex @param {number} j
*/
_api(emit, elSeries, realIndex, j) {
const tag = (el) => {
if (el) {
try {
el.node.setAttribute("index", String(realIndex));
el.node.setAttribute("j", String(j));
el.node.classList.add("apexcharts-marks-mark");
} catch (e) {
}
elSeries.add(el);
}
return el;
};
return {
/** @param {any} o */
path: (o = {}) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
return tag(
emit.drawPath({
d: o.d || "",
stroke: (_a = o.stroke) != null ? _a : "#000",
strokeWidth: (_c = (_b = o.width) != null ? _b : o.strokeWidth) != null ? _c : 1,
fill: (_d = o.fill) != null ? _d : "none",
fillOpacity: (_f = o.fillOpacity) != null ? _f : o.fill && o.fill !== "none" ? (_e = o.opacity) != null ? _e : 1 : 0,
strokeOpacity: (_h = (_g = o.strokeOpacity) != null ? _g : o.opacity) != null ? _h : 1,
strokeDashArray: (_i = o.dash) != null ? _i : 0,
strokeLinecap: o.lineCap
})
);
},
/** @param {any} o */
line: (o = {}) => {
var _a, _b, _c, _d;
return tag(
emit.drawLine(
o.x1,
o.y1,
o.x2,
o.y2,
(_a = o.stroke) != null ? _a : "#000",
(_b = o.dash) != null ? _b : 0,
(_d = (_c = o.width) != null ? _c : o.strokeWidth) != null ? _d : 1
)
);
},
/** @param {any} o */
rect: (o = {}) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
return tag(
emit.drawRect(
(_a = o.x) != null ? _a : 0,
(_b = o.y) != null ? _b : 0,
(_c = o.w) != null ? _c : 0,
(_d = o.h) != null ? _d : 0,
(_e = o.r) != null ? _e : 0,
(_f = o.fill) != null ? _f : "#000",
(_g = o.opacity) != null ? _g : 1,
o.stroke != null ? (_h = o.strokeWidth) != null ? _h : 1 : null,
o.stroke
)
);
},
/** @param {any} o */
circle: (o = {}) => {
var _a, _b, _c, _d, _e;
return tag(
emit.drawCircle((_a = o.r) != null ? _a : 0, {
cx: (_b = o.cx) != null ? _b : 0,
cy: (_c = o.cy) != null ? _c : 0,
fill: (_d = o.fill) != null ? _d : "#000",
stroke: o.stroke || "none",
"stroke-width": (_e = o.strokeWidth) != null ? _e : o.stroke ? 1 : 0
})
);
},
/** @param {any} o */
text: (o = {}) => {
var _a, _b, _c, _d;
return tag(
emit.drawText({
x: (_a = o.x) != null ? _a : 0,
y: (_b = o.y) != null ? _b : 0,
text: (_c = o.text) != null ? _c : "",
textAnchor: (_d = o.anchor) != null ? _d : "start",
fontSize: o.size,
foreColor: o.color,
fontWeight: o.weight
})
);
}
};
}
};
cls.dataType = def.dataType || "xy";
cls.yExtent = typeof def.yExtent === "function" ? def.yExtent : null;
return cls;
}
_core__default._customSeriesFactory = makeCustomSeriesClass;
const DARK_QUERY = "(prefers-color-scheme: dark)";
const CONTRAST_QUERY = "(prefers-contrast: more)";
class OSThemeWatcher {
/**
* @param {any} w @param {any} ctx
*/
constructor(w2, ctx) {
this.w = w2;
this.ctx = ctx;
if (w2.config.theme.follow !== "os" || !Environment.isBrowser()) return;
const media = this._ensureMedia();
if (!media) return;
this._applyToConfig(media);
this._ensureListeners(media);
}
/**
* Create (once per instance) the MediaQueryLists and stash them on `ctx` so
* they persist across the re-render that `updateOptions` triggers.
* @returns {{dark: MediaQueryList|null, contrast: MediaQueryList|null, handler: null|(()=>void)}|null}
*/
_ensureMedia() {
if (!this.ctx._osThemeMedia) {
const dark = BrowserAPIs.matchMedia(DARK_QUERY);
const contrast = BrowserAPIs.matchMedia(CONTRAST_QUERY);
if (!dark && !contrast) return null;
this.ctx._osThemeMedia = { dark, contrast, handler: null };
}
return this.ctx._osThemeMedia;
}
/**
* Write the OS-resolved mode / high-contrast onto the live `w.config.theme`.
* @param {{dark: MediaQueryList|null, contrast: MediaQueryList|null}} media
*/
_applyToConfig(media) {
const theme = this.w.config.theme;
if (media.dark) {
theme.mode = media.dark.matches ? "dark" : "light";
}
if (media.contrast && media.contrast.matches) {
theme.accessibility = theme.accessibility || {};
theme.accessibility.colorBlindMode = "highContrast";
}
}
/**
* Attach the `change` listener once. The handler closes over `ctx` + `media`
* (both stable across re-renders), NOT over `this` (a fresh watcher is built
* each create), so it never goes stale.
* @param {{dark: MediaQueryList|null, contrast: MediaQueryList|null, handler: null|(()=>void)}} media
*/
_ensureListeners(media) {
if (media.handler) return;
const ctx = this.ctx;
const handler = () => {
const m2 = ctx._osThemeMedia;
if (!m2) return;
const themeOpt = { mode: m2.dark && m2.dark.matches ? "dark" : "light" };
if (m2.contrast && m2.contrast.matches) {
themeOpt.accessibility = { colorBlindMode: "highContrast" };
} else {
themeOpt.accessibility = { colorBlindMode: "" };
}
ctx.updateOptions({ theme: themeOpt }, false, true, false);
};
OSThemeWatcher._add(media.dark, handler);
OSThemeWatcher._add(media.contrast, handler);
media.handler = handler;
}
/** Remove the listeners and drop the stashed media. Called on full destroy. */
teardown() {
const media = this.ctx._osThemeMedia;
if (!media) return;
if (media.handler) {
OSThemeWatcher._remove(media.dark, media.handler);
OSThemeWatcher._remove(media.contrast, media.handler);
}
this.ctx._osThemeMedia = null;
}
/**
* @param {MediaQueryList|null} mql @param {()=>void} handler
*/
static _add(mql, handler) {
if (!mql) return;
if (typeof mql.addEventListener === "function") {
mql.addEventListener("change", handler);
} else if (typeof /** @type {any} */
mql.addListener === "function") {
mql.addListener(handler);
}
}
/**
* @param {MediaQueryList|null} mql @param {()=>void} handler
*/
static _remove(mql, handler) {
if (!mql) return;
if (typeof mql.removeEventListener === "function") {
mql.removeEventListener("change", handler);
} else if (typeof /** @type {any} */
mql.removeListener === "function") {
mql.removeListener(handler);
}
}
}
_core__default.registerFeatures({ osThemeWatcher: OSThemeWatcher });
const TRANSFORM_KEY = "__apexcharts_series_transforms__";
if (!/** @type {any} */
globalThis[TRANSFORM_KEY]) {
globalThis[TRANSFORM_KEY] = {};
}
function getTransforms() {
return (
/** @type {any} */
globalThis[TRANSFORM_KEY]
);
}
function registerSeriesTransform(name, fn) {
if (!name || typeof name !== "string") {
console.warn(
"ApexCharts: registerSeriesTransform requires a non-empty name."
);
return;
}
if (typeof fn !== "function") {
console.warn(
`ApexCharts: registerSeriesTransform("${name}") expects a function (series, w) => series.`
);
return;
}
getTransforms()[name] = fn;
}
const ROW_SOURCE_KEY = "__apexcharts_row_sources__";
if (!/** @type {any} */
globalThis[ROW_SOURCE_KEY]) {
globalThis[ROW_SOURCE_KEY] = {};
}
function getSources() {
return (
/** @type {any} */
globalThis[ROW_SOURCE_KEY]
);
}
function registerRowSource(name, fn) {
if (!name || typeof name !== "string") {
console.warn("ApexCharts: registerRowSource requires a non-empty name.");
return;
}
if (typeof fn !== "function") {
console.warn(
`ApexCharts: registerRowSource("${name}") expects a function (w, opts) => series.`
);
return;
}
getSources()[name] = fn;
}
const MAX_BINS = 1e3;
function quantileSorted(sorted, q) {
const n = sorted.length;
if (n === 0) return NaN;
if (n === 1) return sorted[0];
const pos = (n - 1) * q;
const lo = Math.floor(pos);
const hi = Math.ceil(pos);
if (lo === hi) return sorted[lo];
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
}
function stdDev(values) {
const n = values.length;
if (n < 2) return 0;
let sum = 0;
for (let i = 0; i < n; i++) sum += values[i];
const mean = sum / n;
let acc = 0;
for (let i = 0; i < n; i++) {
const d = values[i] - mean;
acc += d * d;
}
return Math.sqrt(acc / n);
}
function widthForRule(sorted, span, rule) {
const n = sorted.length;
const byCount = (count) => span / Math.max(1, Math.ceil(count));
switch (rule) {
case "sqrt":
return { width: byCount(Math.sqrt(n)), rule: "sqrt" };
case "rice":
return { width: byCount(2 * Math.cbrt(n)), rule: "rice" };
case "scott": {
const sd = stdDev(sorted);
if (sd > 0) return { width: 3.49 * sd * Math.pow(n, -1 / 3), rule: "scott" };
return { width: byCount(Math.log2(n) + 1), rule: "sturges" };
}
case "fd": {
const iqr = quantileSorted(sorted, 0.75) - quantileSorted(sorted, 0.25);
if (iqr > 0) return { width: 2 * iqr * Math.pow(n, -1 / 3), rule: "fd" };
return { width: byCount(Math.log2(n) + 1), rule: "sturges" };
}
case "auto": {
const sturges = byCount(Math.log2(n) + 1);
const iqr = quantileSorted(sorted, 0.75) - quantileSorted(sorted, 0.25);
if (iqr <= 0) return { width: sturges, rule: "sturges" };
const fd = 2 * iqr * Math.pow(n, -1 / 3);
return fd < sturges ? { width: fd, rule: "fd" } : { width: sturges, rule: "sturges" };
}
case "sturges":
default:
return { width: byCount(Math.log2(n) + 1), rule: "sturges" };
}
}
function computeBinning(values, opts = {}) {
if (!Array.isArray(values) || values.length === 0) return null;
const sorted = values.slice().sort((a, b2) => a - b2);
let lo = sorted[0];
let hi = sorted[sorted.length - 1];
const range = opts.range;
if (Array.isArray(range) && range.length === 2) {
const rLo = Number(range[0]);
const rHi = Number(range[1]);
if (isFinite(rLo) && isFinite(rHi) && rHi > rLo) {
lo = rLo;
hi = rHi;
}
}
if (!(hi > lo)) {
const pad = Math.abs(lo) > 0 ? Math.abs(lo) * 0.05 : 0.5;
return {
edges: [lo - pad, lo + pad],
binWidth: pad * 2,
rule: "single",
capped: false
};
}
const span = hi - lo;
let width;
let rule;
if (typeof opts.binWidth === "number" && opts.binWidth > 0) {
width = opts.binWidth;
rule = "binWidth";
} else if (typeof opts.bins === "number" && opts.bins >= 1) {
width = span / Math.floor(opts.bins);
rule = "count";
} else {
const chosen = widthForRule(
sorted,
span,
typeof opts.bins === "string" ? opts.bins : "auto"
);
width = chosen.width;
rule = chosen.rule;
}
if (!isFinite(width) || width <= 0) width = span;
let count = Math.ceil(span / width);
if (!isFinite(count) || count < 1) count = 1;
let capped = false;
if (count > MAX_BINS) {
count = MAX_BINS;
width = span / count;
capped = true;
}
width = span / count;
const edges = new Array(count + 1);
for (let k = 0; k <= count; k++) edges[k] = lo + k * width;
edges[count] = Math.max(edges[count], hi);
return { edges, binWidth: width, rule, capped };
}
function binIndexOf(v2, edges) {
const last = edges.length - 1;
if (!(v2 >= edges[0]) || v2 > edges[last]) return -1;
if (v2 === edges[last]) return last - 1;
const width = (edges[last] - edges[0]) / last;
if (width > 0) {
let k = Math.floor((v2 - edges[0]) / width);
if (k < 0) k = 0;
if (k > last - 1) k = last - 1;
if (v2 < edges[k]) k--;
else if (v2 >= edges[k + 1]) k++;
if (k < 0 || k > last - 1) return -1;
return k;
}
let lo = 0;
let hi = last - 1;
while (lo <= hi) {
const mid = lo + hi >> 1;
if (v2 < edges[mid]) hi = mid - 1;
else if (v2 >= edges[mid + 1]) lo = mid + 1;
else return mid;
}
return -1;
}
function binCounts(values, edges) {
const counts = new Array(Math.max(0, edges.length - 1)).fill(0);
for (let i = 0; i < values.length; i++) {
const k = binIndexOf(values[i], edges);
if (k >= 0) counts[k]++;
}
return counts;
}
function rowsByBin(values, edges) {
const n = Math.max(0, edges.length - 1);
const buckets = new Array(n);
for (let k = 0; k < n; k++) buckets[k] = [];
for (let i = 0; i < values.length; i++) {
const k = binIndexOf(values[i], edges);
if (k >= 0) buckets[k].push(values[i]);
}
return buckets;
}
function fiveNumberSummary(values, opts = {}) {
if (!Array.isArray(values) || values.length === 0) return null;
const sorted = values.slice().sort((a, b2) => a - b2);
const q1 = quantileSorted(sorted, 0.25);
const median = quantileSorted(sorted, 0.5);
const q3 = quantileSorted(sorted, 0.75);
const iqr = q3 - q1;
let lo = sorted[0];
let hi = sorted[sorted.length - 1];
let outliers = [];
if (opts.whiskers === "tukey" && iqr > 0) {
const loFence = q1 - 1.5 * iqr;
const hiFence = q3 + 1.5 * iqr;
let i = 0;
while (i < sorted.length && sorted[i] < loFence) i++;
let j = sorted.length - 1;
while (j >= 0 && sorted[j] > hiFence) j--;
if (i <= j) {
lo = sorted[i];
hi = sorted[j];
outliers = sorted.slice(0, i).concat(sorted.slice(j + 1));
}
}
return { summary: [lo, q1, median, q3, hi], outliers, iqr };
}
function kernelDensity(values, opts = {}) {
if (!Array.isArray(values) || values.length === 0) return null;
const sorted = values.slice().sort((a, b2) => a - b2);
const n = sorted.length;
let h = opts.bandwidth;
if (!(typeof h === "number" && h > 0)) {
const sd = stdDev(sorted);
const iqr = quantileSorted(sorted, 0.75) - quantileSorted(sorted, 0.25);
const spread = iqr > 0 ? Math.min(sd, iqr / 1.349) : sd;
h = 0.9 * spread * Math.pow(n, -1 / 5);
}
if (!isFinite(h) || h <= 0) {
const v2 = sorted[0];
const eps = Math.abs(v2) > 0 ? Math.abs(v2) * 1e-3 : 1e-3;
return {
density: [
[v2 - eps, 0],
[v2, 1],
[v2 + eps, 0]
],
bandwidth: eps
};
}
const steps = Math.max(8, Math.floor(opts.resolution || 64));
const lo = sorted[0] - 2 * h;
const hi = sorted[n - 1] + 2 * h;
const step = (hi - lo) / (steps - 1);
const norm = 1 / (n * h * Math.sqrt(2 * Math.PI));
const density = [];
for (let g2 = 0; g2 < steps; g2++) {
const x = lo + g2 * step;
let sum = 0;
for (let i = 0; i < n; i++) {
const z = (x - sorted[i]) / h;
sum += Math.exp(-0.5 * z * z);
}
density.push([x, sum * norm]);
}
return { density, bandwidth: h };
}
function normalizeCounts(counts, opts = {}) {
let out = counts.slice();
if (opts.cumulative) {
let acc = 0;
out = out.map((c) => acc += c);
}
const total = counts.reduce((a, b2) => a + b2, 0);
if (total <= 0) return out;
if (opts.normalize === "relative") {
return out.map((c) => c / total * 100);
}
if (opts.normalize === "density") {
const w2 = opts.binWidth;
if (typeof w2 === "number" && w2 > 0) return out.map((c) => c / (total * w2));
}
return out;
}
function histogramValues(data) {
const out = [];
if (!Array.isArray(data)) return out;
for (let i = 0; i < data.length; i++) {
const d = data[i];
let raw = d;
if (Array.isArray(d)) raw = d.length === 1 ? d[0] : d[1];
else if (d && typeof d === "object") raw = d.y !== void 0 ? d.y : d.x;
const v2 = Utils.parseNumber(raw);
if (v2 !== null && isFinite(v2)) out.push(v2);
}
return out;
}
function histogramTransform(ser, w2) {
var _a;
const cnf = w2.config;
const gl = w2.globals;
if (!Array.isArray(ser)) return ser;
if (!gl.histogramRawSeries) {
gl.histogramRawSeries = ser.map((s) => __spreadProps(__spreadValues({}, s), {
data: Array.isArray(s == null ? void 0 : s.data) ? s.data.slice() : s == null ? void 0 : s.data
}));
}
const raw = gl.histogramRawSeries;
const hcfg = ((_a = cnf.plotOptions) == null ? void 0 : _a.histogram) || {};
const perSeries = raw.map((s) => histogramValues(s == null ? void 0 : s.data));
let all = [];
if (perSeries.length === 1) {
all = perSeries[0];
} else {
for (const vals of perSeries) all = all.concat(vals);
}
const binning = computeBinning(all, {
bins: hcfg.bins,
binWidth: hcfg.binWidth,
range: hcfg.range
});
if (!binning) {
w2.histogramData = {
edges: [],
binWidth: 0,
counts: [],
rule: "",
capped: false
};
return raw;
}
const { edges, binWidth } = binning;
const counts = perSeries.map(
(vals) => binCounts(vals, edges)
);
w2.histogramData = {
edges,
binWidth,
counts,
rule: binning.rule,
capped: binning.capped
};
const collapsed = gl.collapsedSeriesIndices || [];
return raw.map((s, i) => {
if (collapsed.indexOf(i) !== -1) return __spreadProps(__spreadValues({}, s), { data: [] });
const ys = normalizeCounts(counts[i], {
normalize: hcfg.normalize,
cumulative: hcfg.cumulative,
binWidth
});
const data = [];
for (let k = 0; k < ys.length; k++) {
data.push({ x: (edges[k] + edges[k + 1]) / 2, y: ys[k] });
}
return __spreadProps(__spreadValues({}, s), { data });
});
}
const derivedData = /* @__PURE__ */ new WeakSet();
function observationsOf(d, allowFlatY) {
var _a;
if (!d || typeof d !== "object" || Array.isArray(d)) return null;
let raw = null;
if (Array.isArray(d.points)) raw = d.points;
else if (Array.isArray((_a = d.y) == null ? void 0 : _a.points)) raw = d.y.points;
else if (allowFlatY && Array.isArray(d.y) && typeof d.y[0] === "number") {
raw = d.y;
}
if (!raw) return null;
const out = [];
for (let i = 0; i < raw.length; i++) {
const v2 = Utils.parseNumber(raw[i]);
if (v2 !== null && isFinite(v2)) out.push(v2);
}
return out.length ? out : null;
}
function boxPlotTransform(ser, w2) {
var _a, _b;
if (!Array.isArray(ser)) return ser;
const whiskers = ((_b = (_a = w2.config.plotOptions) == null ? void 0 : _a.boxPlot) == null ? void 0 : _b.whiskers) || "minmax";
return ser.map((s) => {
if (!Array.isArray(s == null ? void 0 : s.data)) return s;
let touched = false;
const data = s.data.map((d) => {
if (Array.isArray(d == null ? void 0 : d.y) && d.y.length === 5 && !derivedData.has(d)) {
return d;
}
const values = observationsOf(d, false);
if (!values) return d;
const summary = fiveNumberSummary(values, { whiskers });
if (!summary) return d;
touched = true;
const next = __spreadProps(__spreadValues({}, d), { y: summary.summary, points: values });
derivedData.add(next);
return next;
});
return touched ? __spreadProps(__spreadValues({}, s), { data }) : s;
});
}
function violinTransform(ser, w2) {
var _a, _b;
if (!Array.isArray(ser)) return ser;
const kde = ((_b = (_a = w2.config.plotOptions) == null ? void 0 : _a.violin) == null ? void 0 : _b.kde) || {};
return ser.map((s) => {
if (!Array.isArray(s == null ? void 0 : s.data)) return s;
let touched = false;
const data = s.data.map((d) => {
var _a2;
if (Array.isArray((_a2 = d == null ? void 0 : d.y) == null ? void 0 : _a2.density) && d.y.density.length && !derivedData.has(d)) {
return d;
}
const values = observationsOf(d, true);
if (!values) return d;
const est = kernelDensity(values, {
bandwidth: kde.bandwidth,
resolution: kde.resolution
});
if (!est) return d;
touched = true;
const next = __spreadProps(__spreadValues({}, d), { y: { density: est.density, points: values } });
derivedData.add(next);
return next;
});
return touched ? __spreadProps(__spreadValues({}, s), { data }) : s;
});
}
const DEFAULT_MAX_ROWS = 3e3;
function thinClusters(clusters, maxRows) {
let total = 0;
let widest = 0;
for (const c of clusters) {
total += c.length;
if (c.length > widest) widest = c.length;
}
if (total <= maxRows) return { clusters, stride: 1, total, kept: total };
const keptAt = (s) => {
let n = 0;
for (const c of clusters) n += Math.ceil(c.length / s);
return n;
};
let stride = Math.max(2, Math.ceil(total / maxRows));
while (stride < widest && keptAt(stride) > maxRows) stride++;
let kept = 0;
const out = clusters.map((rows) => {
const keepList = [];
for (let i = 0; i < rows.length; i += stride) keepList.push(rows[i]);
kept += keepList.length;
return keepList;
});
return { clusters: out, stride, total, kept };
}
function toUnitSeries(w2, clusters, opts) {
const maxRows = opts && opts.maxRows != null ? opts.maxRows : DEFAULT_MAX_ROWS;
const thinned = thinClusters(
clusters.map((c) => c.rows),
maxRows
);
if (thinned.stride > 1) {
console.warn(
`ApexCharts: rowSeries() thinned ${thinned.total} rows to ${thinned.kept} (every ${thinned.stride}${thinned.stride === 2 ? "nd" : thinned.stride === 3 ? "rd" : "th"} row) to stay under maxRows=${maxRows}. Raise maxRows to draw more.`
);
}
const colors = w2.globals && w2.globals.colors || [];
return clusters.map((c, i) => {
const fillColor = colors[c.realIndex] || colors[0];
return {
name: c.name,
data: thinned.clusters[i].map((v2, q) => __spreadValues({
id: `${c.realIndex}:${i}:${q}`,
x: c.name,
y: v2
}, fillColor ? { fillColor } : {}))
};
});
}
function histogramRows(w2, opts) {
const gl = w2.globals;
const hd = w2.histogramData;
const raw = gl && gl.histogramRawSeries;
if (!hd || !Array.isArray(hd.edges) || hd.edges.length < 2) return null;
if (!Array.isArray(raw) || !raw.length) return null;
const collapsed = gl && gl.collapsedSeriesIndices || [];
const edges = hd.edges;
const clusters = [];
raw.forEach((s, i) => {
var _a;
if (collapsed.indexOf(i) !== -1) return;
const buckets = rowsByBin(histogramValues(s && s.data), edges);
const seriesName = w2.seriesData && ((_a = w2.seriesData.seriesNames) == null ? void 0 : _a[i]) || (s == null ? void 0 : s.name);
buckets.forEach((rows, k) => {
const range = `${formatEdge(edges[k])}-${formatEdge(edges[k + 1])}`;
clusters.push({
// Only qualify by series when there is more than one to tell apart.
name: raw.length > 1 && seriesName ? `${seriesName} ${range}` : range,
realIndex: i,
rows
});
});
});
return clusters.length ? toUnitSeries(w2, clusters, opts) : null;
}
function formatEdge(v2) {
if (!isFinite(v2)) return String(v2);
const r = Math.round(v2);
return Math.abs(v2 - r) < 1e-6 ? String(r) : String(Number(v2.toFixed(2)));
}
function pointsRowSource(pick) {
return (w2, opts) => {
var _a;
const perSeries = pick(w2);
if (!Array.isArray(perSeries) || !perSeries.length) return null;
const collapsed = w2.globals && w2.globals.collapsedSeriesIndices || [];
const labels = w2.globals && (((_a = w2.globals.categoryLabels) == null ? void 0 : _a.length) ? w2.globals.categoryLabels : w2.globals.labels) || [];
const clusters = [];
perSeries.forEach((byCat, i) => {
var _a2;
if (collapsed.indexOf(i) !== -1) return;
if (!Array.isArray(byCat)) return;
const seriesName = w2.seriesData && ((_a2 = w2.seriesData.seriesNames) == null ? void 0 : _a2[i]);
byCat.forEach((pts, j) => {
const label = labels[j] != null ? String(labels[j]) : `#${j + 1}`;
clusters.push({
name: perSeries.length > 1 && seriesName ? `${seriesName} ${label}` : label,
realIndex: i,
rows: Array.isArray(pts) ? pts.slice() : []
});
});
});
return clusters.length ? toUnitSeries(w2, clusters, opts) : null;
};
}
const boxPlotRows = pointsRowSource((w2) => {
var _a;
return (_a = w2.candleData) == null ? void 0 : _a.seriesBoxPoints;
});
const violinRows = pointsRowSource((w2) => {
var _a;
return (_a = w2.violinData) == null ? void 0 : _a.seriesViolinPoints;
});
registerSeriesTransform("histogram", histogramTransform);
registerSeriesTransform("boxPlot", boxPlotTransform);
registerSeriesTransform("violin", violinTransform);
registerRowSource("histogram", histogramRows);
registerRowSource("boxPlot", boxPlotRows);
registerRowSource("violin", violinRows);
const resolveDataLabelOffset = (value, w2, seriesIndex, dataPointIndex) => {
if (typeof value !== "function") return value;
const resolved = value({
series: w2.seriesData.series,
seriesIndex,
dataPointIndex,
w: w2
});
return Number.isFinite(resolved) ? resolved : 0;
};
function easeInOutSine(t) {
return -Math.cos(t * Math.PI) / 2 + 0.5;
}
function cubicBezier(x1, y1, x2, y2) {
x1 = Math.min(Math.max(x1, 0), 1);
x2 = Math.min(Math.max(x2, 0), 1);
const cx = 3 * x1;
const bx = 3 * (x2 - x1) - cx;
const ax = 1 - cx - bx;
const cy = 3 * y1;
const by = 3 * (y2 - y1) - cy;
const ay = 1 - cy - by;
const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
const sampleY = (t) => ((ay * t + by) * t + cy) * t;
const solveT = (x) => {
let lo = 0;
let hi = 1;
let t = x;
if (t < lo) return lo;
if (t > hi) return hi;
while (lo < hi) {
const xt = sampleX(t);
if (Math.abs(xt - x) < 1e-4) return t;
if (x > xt) lo = t;
else hi = t;
t = (lo + hi) / 2;
}
return t;
};
return (t) => t <= 0 ? 0 : t >= 1 ? 1 : sampleY(solveT(t));
}
const REGISTRY = /* @__PURE__ */ new Map();
const linear = (t) => t;
REGISTRY.set("linear", linear);
REGISTRY.set("easeInOutSine", easeInOutSine);
REGISTRY.set("easeInSine", (t) => 1 - Math.cos(t * Math.PI / 2));
REGISTRY.set("easeOutSine", (t) => Math.sin(t * Math.PI / 2));
REGISTRY.set("easeInQuad", (t) => t * t);
REGISTRY.set("easeOutQuad", (t) => 1 - (1 - t) * (1 - t));
REGISTRY.set(
"easeInOutQuad",
(t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
);
REGISTRY.set("easeInCubic", (t) => t * t * t);
REGISTRY.set("easeOutCubic", (t) => 1 - Math.pow(1 - t, 3));
REGISTRY.set(
"easeInOutCubic",
(t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
);
REGISTRY.set("easeOutBack", (t) => {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
});
REGISTRY.set("easeInOutBack", (t) => {
const c1 = 1.70158;
const c2 = c1 * 1.525;
return t < 0.5 ? Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2) / 2 : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
});
function isBezierArray(v2) {
return Array.isArray(v2) && v2.length === 4 && v2.every((n) => typeof n === "number");
}
function resolveEasing(value) {
if (typeof value === "function") return guardEasing(value);
if (isBezierArray(value))
return cubicBezier(value[0], value[1], value[2], value[3]);
if (typeof value === "string" && REGISTRY.has(value)) {
return guardEasing(
/** @type {(t:number)=>number} */
REGISTRY.get(value)
);
}
return easeInOutSine;
}
function guardEasing(fn) {
return (t) => {
const y = fn(t);
return typeof y === "number" && isFinite(y) ? y : t;
};
}
function buildUnionEntries(join, oldN) {
const exitSet = new Set(join.exits);
const entries = [];
let oi = 0;
for (let nj = 0; nj < join.toOld.length; nj++) {
const oj = join.toOld[nj];
if (oj !== -1) {
while (oi < oj) {
if (exitSet.has(oi)) entries.push({ oldJ: oi, newJ: -1 });
oi++;
}
entries.push({ oldJ: oj, newJ: nj });
oi = oj + 1;
} else {
entries.push({ oldJ: -1, newJ: nj });
}
}
while (oi < oldN) {
if (exitSet.has(oi)) entries.push({ oldJ: oi, newJ: -1 });
oi++;
}
return entries;
}
function analyzeSeriesPath(d, expectedAnchors, isArea) {
if (!d || typeof d !== "string") return null;
const cmds = parsePath(d);
if (!cmds.length || cmds[0][0] !== "M") return null;
for (let i = 1; i < cmds.length; i++) {
if (cmds[i][0] === "M") return null;
}
let body = cmds;
let closing = null;
if (isArea) {
if (cmds.length < 5) return null;
closing = cmds.slice(-3);
body = cmds.slice(0, -3);
if (closing[2][0] !== "Z") return null;
if (closing[1][0] !== "L") return null;
if (closing[0][0] !== "L" && closing[0][0] !== "C") return null;
} else if (cmds[cmds.length - 1][0] === "Z") {
return null;
}
if (body.length !== expectedAnchors || body.length < 2) return null;
const segType = body[1][0];
if (segType !== "L" && segType !== "C") return null;
for (let i = 2; i < body.length; i++) {
if (body[i][0] !== segType) return null;
}
const anchors = body.map(
(c) => c[0] === "C" ? [Number(c[5]), Number(c[6])] : [Number(c[1]), Number(c[2])]
);
for (const a of anchors) {
if (!isFinite(a[0]) || !isFinite(a[1])) return null;
}
return { body, closing, segType, anchors };
}
function lerpPt(a, b2, t) {
return [a[0] + (b2[0] - a[0]) * t, a[1] + (b2[1] - a[1]) * t];
}
function splitCubic(s, cmd, t) {
const p0 = s;
const p1 = [cmd[1], cmd[2]];
const p2 = [cmd[3], cmd[4]];
const p3 = [cmd[5], cmd[6]];
const p01 = lerpPt(p0, p1, t);
const p12 = lerpPt(p1, p2, t);
const p23 = lerpPt(p2, p3, t);
const p012 = lerpPt(p01, p12, t);
const p123 = lerpPt(p12, p23, t);
const mid = lerpPt(p012, p123, t);
return {
first: ["C", p01[0], p01[1], p012[0], p012[1], mid[0], mid[1]],
second: ["C", p123[0], p123[1], p23[0], p23[1], p3[0], p3[1]],
mid
};
}
function splitLine(s, cmd, t) {
const e = [cmd[1], cmd[2]];
const mid = lerpPt(s, e, t);
return {
first: ["L", mid[0], mid[1]],
second: ["L", e[0], e[1]],
mid
};
}
function expandPath(analysis, ownIdx) {
const { body, closing, segType, anchors } = analysis;
const n = anchors.length;
const m2 = ownIdx.length;
const degen = (p2) => segType === "C" ? ["C", p2[0], p2[1], p2[0], p2[1], p2[0], p2[1]] : ["L", p2[0], p2[1]];
const out = [["M", anchors[0][0], anchors[0][1]]];
let firstOwn = 0;
while (firstOwn < m2 && ownIdx[firstOwn] !== 0) firstOwn++;
for (let q = 1; q <= firstOwn; q++) out.push(degen(anchors[0]));
let entry = firstOwn + 1;
for (let s = 0; s < n - 1; s++) {
let interior = 0;
while (entry + interior < m2 && ownIdx[entry + interior] === -1) interior++;
const cmd = body[s + 1];
if (!interior) {
out.push(cmd.slice());
} else {
const totalParts = interior + 1;
let start = anchors[s];
let rest = cmd;
for (let q = 0; q < interior; q++) {
const t = 1 / (totalParts - q);
const sp = segType === "C" ? splitCubic(start, rest, t) : splitLine(start, rest, t);
out.push(sp.first);
start = sp.mid;
rest = sp.second;
}
out.push(rest);
}
entry += interior + 1;
}
while (entry < m2) {
out.push(degen(anchors[n - 1]));
entry++;
}
if (closing) closing.forEach((c) => out.push(c.slice()));
return out;
}
function reconcilePathPair(fromD, toD, entries, oldN, newN, isArea) {
const fromAnalysis = analyzeSeriesPath(fromD, oldN, isArea);
if (!fromAnalysis) return null;
const toAnalysis = analyzeSeriesPath(toD, newN, isArea);
if (!toAnalysis) return null;
if (fromAnalysis.segType !== toAnalysis.segType) return null;
if ((fromAnalysis.closing || toAnalysis.closing) && (!fromAnalysis.closing || !toAnalysis.closing || fromAnalysis.closing[0][0] !== toAnalysis.closing[0][0])) {
return null;
}
const fromOwn = entries.map((e) => e.oldJ);
const toOwn = entries.map((e) => e.newJ);
return {
from: arrayToPath(expandPath(fromAnalysis, fromOwn)),
toInterp: arrayToPath(expandPath(toAnalysis, toOwn))
};
}
function lengthTransitionEnabled(w2) {
var _a;
const anim = w2.config.chart.animations;
if (!anim || anim.enabled === false) return false;
if (!anim.dynamicAnimation || anim.dynamicAnimation.enabled === false) {
return false;
}
const largeThreshold = (_a = anim.largeDatasetThreshold) != null ? _a : 0;
if (largeThreshold > 0 && w2.globals.dataPoints > largeThreshold) return false;
return !!(Environment.isBrowser() && w2.globals.dataChanged && w2.globals.shouldAnimate);
}
function datumKey(w2, realIndex, j) {
var _a, _b, _c, _d;
if ((_a = w2.axisFlags) == null ? void 0 : _a.isXNumeric) {
const sx = (_c = (_b = w2.seriesData) == null ? void 0 : _b.seriesX) == null ? void 0 : _c[realIndex];
if (sx && sx.length && sx[j] != null) return "x:" + sx[j];
}
const lbl = (_d = w2.globals.labels) == null ? void 0 : _d[j];
if (lbl != null && String(lbl) !== "") {
return "c:" + (Array.isArray(lbl) ? lbl.join(" ") : String(lbl));
}
return "j:" + j;
}
function frameDatumKey(frame, realIndex, j) {
var _a, _b;
if (frame.isXNumeric) {
const sx = (_a = frame.seriesX) == null ? void 0 : _a[realIndex];
if (sx && sx.length && sx[j] != null) return "x:" + sx[j];
}
const lbl = (_b = frame.labels) == null ? void 0 : _b[j];
if (lbl != null && String(lbl) !== "") {
return "c:" + (Array.isArray(lbl) ? lbl.join(" ") : String(lbl));
}
return "j:" + j;
}
function joinKeys(oldKeys, newKeys) {
const oldIndex = /* @__PURE__ */ new Map();
oldKeys.forEach((k, i) => {
if (!oldIndex.has(k)) oldIndex.set(k, i);
});
const toOld = new Array(newKeys.length);
const usedOld = /* @__PURE__ */ new Set();
let prev = -1;
let ordered = true;
let identity = oldKeys.length === newKeys.length;
newKeys.forEach((k, i) => {
const oi = oldIndex.has(k) && !usedOld.has(oldIndex.get(k)) ? oldIndex.get(k) : -1;
toOld[i] = oi;
if (oi !== -1) {
usedOld.add(oi);
if (oi < prev) ordered = false;
prev = oi;
}
if (oi !== i) identity = false;
});
const exits = [];
for (let i = 0; i < oldKeys.length; i++) {
if (!usedOld.has(i)) exits.push(i);
}
return { toOld, exits, ordered, changed: !identity };
}
function uniquifyKeys(keys) {
const seen = /* @__PURE__ */ new Map();
return keys.map((k) => {
const count = seen.get(k) || 0;
seen.set(k, count + 1);
return count === 0 ? k : `${k}#${count}`;
});
}
function seriesJoin(w2, realIndex, includeIdentity = false, allowReorder = false) {
var _a, _b;
if (!lengthTransitionEnabled(w2)) return null;
const frame = w2.globals.prevStreamFrame;
if (!frame) return null;
const oldY = (_a = frame.seriesY) == null ? void 0 : _a[realIndex];
const newY = (_b = w2.seriesData.series) == null ? void 0 : _b[realIndex];
if (!Array.isArray(oldY) || !Array.isArray(newY)) return null;
if (!oldY.length || !newY.length) return null;
const oldKeys = uniquifyKeys(
oldY.map((_, j) => frameDatumKey(frame, realIndex, j))
);
const newKeys = uniquifyKeys(newY.map((_, j) => datumKey(w2, realIndex, j)));
const join = joinKeys(oldKeys, newKeys);
if (!join.ordered && !allowReorder) return null;
if (!join.changed && !includeIdentity) return null;
return { join, oldKeys, newKeys };
}
function morphEasing(w2) {
var _a, _b;
const anim = w2.config.chart.animations;
return resolveEasing((_b = (_a = anim.dynamicAnimation) == null ? void 0 : _a.easing) != null ? _b : anim.easing);
}
function rafTween(w2, duration, ease, onFrame, onDone) {
const startAt = performance.now();
const step = (now) => {
if (w2.globals.isDestroyed) return;
const raw = Math.max(0, Math.min(1, (now - startAt) / duration));
onFrame(ease(raw), raw);
if (raw < 1) {
BrowserAPIs.requestAnimationFrame(step);
} else if (onDone) {
onDone();
}
};
BrowserAPIs.requestAnimationFrame(step);
}
function tweenSeriesMarkers(w2, { elPointsMain, realIndex, speed }) {
var _a, _b, _c, _d;
if (!(elPointsMain == null ? void 0 : elPointsMain.node)) return false;
const sj = seriesJoin(w2, realIndex, true);
if (!sj) return false;
const frame = w2.globals.prevStreamFrame;
if (!frame) return false;
const oldXP = (_a = frame.xPixels) == null ? void 0 : _a[realIndex];
const oldYP = (_b = frame.yPixels) == null ? void 0 : _b[realIndex];
if (!oldXP || !oldYP) return false;
const markers = elPointsMain.node.querySelectorAll(".apexcharts-marker");
if (!markers.length) return false;
const newXP = ((_c = w2.globals.seriesXvalues) == null ? void 0 : _c[realIndex]) || [];
const newYP = ((_d = w2.globals.seriesYvalues) == null ? void 0 : _d[realIndex]) || [];
const ease = morphEasing(w2);
const duration = Math.max(1, speed || 1);
elPointsMain.node.classList.remove("apexcharts-element-hidden");
markers.forEach((node) => {
var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i;
const j = parseInt(
(_b2 = (_a2 = node.getAttribute("j")) != null ? _a2 : node.getAttribute("rel")) != null ? _b2 : "",
10
);
if (!isFinite(j) || j < 0 || j >= sj.join.toOld.length) return;
const oldJ = sj.join.toOld[j];
const to = newXP[j] != null && newYP[j] != null ? [newXP[j], newYP[j]] : null;
if (oldJ === -1 || !to) {
const style = (
/** @type {any} */
node.style
);
style.opacity = "0";
rafTween(
w2,
duration,
ease,
(eased) => {
style.opacity = String(eased);
},
() => {
style.opacity = "";
}
);
return;
}
const dx = ((_c2 = oldXP[oldJ]) != null ? _c2 : NaN) - to[0];
const dy = ((_d2 = oldYP[oldJ]) != null ? _d2 : NaN) - to[1];
if (!isFinite(dx) || !isFinite(dy)) return;
const rFrom = (_g = (_f = (_e = frame.rPixels) == null ? void 0 : _e[realIndex]) == null ? void 0 : _f[oldJ]) != null ? _g : NaN;
const rTo = parseFloat(
(_i = (_h = node.getAttribute("r")) != null ? _h : node.getAttribute("default-marker-size")) != null ? _i : ""
);
const scales = isFinite(rFrom) && isFinite(rTo) && rTo > 0 && Math.abs(rFrom - rTo) > 0.25;
const moves = Math.abs(dx) >= 0.5 || Math.abs(dy) >= 0.5;
if (!moves && !scales) return;
const apply = (eased) => {
const offX = dx * (1 - eased);
const offY = dy * (1 - eased);
if (scales) {
const s = (rFrom + (rTo - rFrom) * eased) / rTo;
node.setAttribute(
"transform",
`translate(${offX + to[0] * (1 - s)}, ${offY + to[1] * (1 - s)}) scale(${s})`
);
} else {
node.setAttribute("transform", `translate(${offX}, ${offY})`);
}
};
apply(0);
rafTween(w2, duration, ease, apply, () => {
node.removeAttribute("transform");
});
});
return true;
}
function reconcileSeriesPaths(w2, { type, realIndex, pathFromLine, pathFromArea, linePaths, areaPaths }) {
var _a, _b;
const sj = seriesJoin(w2, realIndex);
if (!sj) return null;
const { join, oldKeys, newKeys } = sj;
const frame = w2.globals.prevStreamFrame;
if (!frame) return null;
const oldY = (_a = frame.seriesY) == null ? void 0 : _a[realIndex];
const newY = (_b = w2.seriesData.series) == null ? void 0 : _b[realIndex];
if (oldY.length < 2 || newY.length < 2) return null;
if (oldY.some((v2) => v2 === null) || newY.some((v2) => v2 === null)) return null;
const entries = buildUnionEntries(join, oldKeys.length);
const out = {};
if (Array.isArray(linePaths) && linePaths.length === 1 && pathFromLine) {
out.line = reconcilePathPair(
pathFromLine,
linePaths[0],
entries,
oldKeys.length,
newKeys.length,
false
);
}
if (type === "area" && Array.isArray(areaPaths) && areaPaths.length === 1 && pathFromArea) {
out.area = reconcilePathPair(
pathFromArea,
areaPaths[0],
entries,
oldKeys.length,
newKeys.length,
true
);
}
if (!out.line && !out.area) return null;
return out;
}
function firstMove(d) {
const m2 = /^M\s*([+-]?[\d.eE]+)[\s,]+([+-]?[\d.eE]+)/.exec(d || "");
if (!m2) return null;
const x = parseFloat(m2[1]);
const y = parseFloat(m2[2]);
return isFinite(x) && isFinite(y) ? { x, y } : null;
}
function renderBarExitGhosts({
w: w2,
elSeries,
record,
newKeys,
isHorizontal,
speed
}) {
var _a;
if (!lengthTransitionEnabled(w2)) return;
if (!record || !Array.isArray(record.paths) || !(elSeries == null ? void 0 : elSeries.node)) return;
const newKeySet = new Set(newKeys);
const exits = record.paths.filter(
(p2) => p2 && p2.d && p2.key != null && !newKeySet.has(p2.key)
);
if (!exits.length) return;
const graphics = new Graphics(w2);
const fallbackFill = (_a = w2.globals.colors) == null ? void 0 : _a[parseInt(String(record.realIndex), 10)];
exits.forEach((p2) => {
let fill = p2.fill || fallbackFill || "#c8c8c8";
if (String(fill).indexOf("url(") === 0) fill = fallbackFill || "#c8c8c8";
const ghost = graphics.drawPath({
d: p2.d,
stroke: "none",
strokeWidth: 0,
fill,
fillOpacity: 1,
classes: "apexcharts-bar-ghost"
});
const node = ghost.node;
node.setAttribute("pointer-events", "none");
ghost.attr(
"clip-path",
`url(#gridRectBarMask${w2.globals.cuid})`
);
elSeries.node.insertBefore(node, elSeries.node.firstChild);
const start = firstMove(p2.d);
let origin = isHorizontal ? "left center" : "center bottom";
try {
const bb = node.getBBox();
if (start && bb) {
if (isHorizontal) {
origin = Math.abs(start.x - bb.x) <= Math.abs(start.x - (bb.x + bb.width)) ? "left center" : "right center";
} else {
origin = Math.abs(start.y - (bb.y + bb.height)) <= Math.abs(start.y - bb.y) ? "center bottom" : "center top";
}
}
} catch (_) {
}
const style = node.style;
style.transformBox = "fill-box";
style.transformOrigin = origin;
const duration = Math.max(1, speed || 1);
const startAt = performance.now();
const step = (now) => {
if (w2.globals.isDestroyed || !node.parentNode) return;
const t = Math.max(0, Math.min(1, (now - startAt) / duration));
const eased = 1 - Math.pow(1 - t, 3);
const scale = 1 - eased;
style.transform = isHorizontal ? `scaleX(${scale})` : `scaleY(${scale})`;
style.opacity = String(1 - eased);
if (t < 1) {
BrowserAPIs.requestAnimationFrame(step);
} else {
node.parentNode.removeChild(node);
}
};
BrowserAPIs.requestAnimationFrame(step);
});
}
class BarDataLabels {
/**
* @param {import('../../../charts/Bar').default} barCtx
*/
constructor(barCtx) {
this.w = barCtx.w;
this.barCtx = barCtx;
this.totalFormatter = this.w.config.plotOptions.bar.dataLabels.total.formatter;
if (!this.totalFormatter) {
this.totalFormatter = this.w.config.dataLabels.formatter;
}
}
/** handleBarDataLabels is used to calculate the positions for the data-labels
* It also sets the element's data attr for bars and calls drawCalculatedBarDataLabels()
* After calculating, it also calls the function to draw data labels
* @memberof Bar
* @param {Record<string, any>} opts - bar properties used throughout the bar drawing function
* @return {object} dataLabels node-element which you can append later
**/
handleBarDataLabels(opts) {
const {
x,
y,
y1,
y2,
i,
j,
realIndex,
columnGroupIndex,
series,
barHeight,
barWidth,
barXPosition,
barYPosition,
visibleSeries
} = opts;
const w2 = this.w;
const graphics = new Graphics(this.barCtx.w);
const strokeWidth = Array.isArray(this.barCtx.strokeWidth) ? this.barCtx.strokeWidth[realIndex] : this.barCtx.strokeWidth;
let bcx;
let bcy;
if (w2.axisFlags.isXNumeric && !w2.globals.isBarHorizontal) {
bcx = x + barWidth * (visibleSeries + 1);
bcy = y + barHeight * (visibleSeries + 1) - strokeWidth;
} else {
bcx = x + barWidth * visibleSeries;
bcy = y + barHeight * visibleSeries;
}
let dataLabels = null;
let totalDataLabels = null;
let dataLabelsX = x;
let dataLabelsY = y;
let dataLabelsPos = (
/** @type {any} */
{}
);
const dataLabelsConfig = w2.config.dataLabels;
const barDataLabelsConfig = this.barCtx.barOptions.dataLabels;
const barTotalDataLabelsConfig = this.barCtx.barOptions.dataLabels.total;
if (typeof barYPosition !== "undefined" && (this.barCtx.isRangeBar || this.barCtx.isPyramid)) {
bcy = barYPosition;
dataLabelsY = barYPosition;
}
if (typeof barXPosition !== "undefined" && this.barCtx.isVerticalGroupedRangeBar) {
bcx = barXPosition;
dataLabelsX = barXPosition;
}
const offX = resolveDataLabelOffset(
dataLabelsConfig.offsetX,
w2,
realIndex,
j
);
const offY = resolveDataLabelOffset(
dataLabelsConfig.offsetY,
w2,
realIndex,
j
);
let textRects = {
width: 0,
height: 0
};
if (w2.config.dataLabels.enabled) {
const yLabel = w2.seriesData.series[realIndex][j];
textRects = graphics.getTextRects(
w2.config.dataLabels.formatter ? w2.config.dataLabels.formatter(yLabel, __spreadProps(__spreadValues({}, w2), {
seriesIndex: realIndex,
dataPointIndex: j,
w: w2
})) : w2.formatters.yLabelFormatters[0](yLabel),
parseFloat(dataLabelsConfig.style.fontSize).toString(),
dataLabelsConfig.style.fontFamily,
void 0,
true,
dataLabelsConfig.style.fontWeight
);
}
const params = {
x,
y,
i,
j,
realIndex,
columnGroupIndex,
bcx,
bcy,
barHeight,
barWidth,
textRects,
strokeWidth,
dataLabelsX,
dataLabelsY,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
offX,
offY
};
if (this.barCtx.isHorizontal) {
dataLabelsPos = this.calculateBarsDataLabelsPosition(params);
} else {
dataLabelsPos = this.calculateColumnsDataLabelsPosition(params);
}
dataLabels = this.drawCalculatedDataLabels({
x: dataLabelsPos.dataLabelsX,
y: dataLabelsPos.dataLabelsY,
val: this.barCtx.isRangeBar ? [y1, y2] : w2.config.chart.stackType === "100%" ? series[realIndex][j] : w2.seriesData.series[realIndex][j],
i: realIndex,
j,
barWidth,
barHeight,
textRects,
dataLabelsConfig
});
if (w2.config.chart.stacked && barTotalDataLabelsConfig.enabled) {
totalDataLabels = this.drawTotalDataLabels({
x: dataLabelsPos.totalDataLabelsX,
y: dataLabelsPos.totalDataLabelsY,
barWidth,
barHeight,
realIndex,
j,
textAnchor: dataLabelsPos.totalDataLabelsAnchor,
val: this.getStackedTotalDataLabel({ realIndex, j }),
rawVal: this.getStackedTotalValue({ realIndex, j }),
dataLabelsConfig,
barTotalDataLabelsConfig
});
}
return {
dataLabelsPos,
dataLabels,
totalDataLabels
};
}
/**
* True when this chart stacks in more than one group, so totals have to be
* resolved per group rather than across the whole data point. A single-group
* chart keeps every old code path exactly as it was. See #4173.
*/
hasMultipleSeriesGroups() {
return this.w.labelData.seriesGroups.length > 1;
}
/**
* The series group `realIndex` belongs to, and that group's own stacking
* state, or null when totals are chart-wide (the single-group case).
* @param {number} realIndex
* @returns {{groupIndex: number, group: string[]} | null}
*/
getTotalGroupContext(realIndex) {
if (!this.hasMultipleSeriesGroups()) return null;
const groupIndex = this.barCtx.barHelpers.getSeriesGroupIndex(realIndex);
if (groupIndex < 0) return null;
return { groupIndex, group: this.w.labelData.seriesGroups[groupIndex] };
}
/**
* Whether `realIndex` is the series that should draw the stacked total.
*
* That is the series capping the stack. With grouped stacks each group has
* its own cap, so gating on the chart-wide `lastActiveBarSerieIndex` drew a
* single total for the last group only. See #4173.
* @param {number} realIndex
*/
drawsStackedTotal(realIndex) {
const byGroup = this.barCtx.lastActiveBarSerieIndexByGroup;
const ctx = this.getTotalGroupContext(realIndex);
if (ctx && byGroup && byGroup.length > ctx.groupIndex) {
return byGroup[ctx.groupIndex] === realIndex;
}
return this.barCtx.lastActiveBarSerieIndex === realIndex;
}
/**
* The raw (unformatted) stacked total at this data point. Split out of
* getStackedTotalDataLabel so the label transition can count the total up
* from its previous number and re-run the formatter itself each frame.
* @param {{realIndex: any, j: any}} opts
*/
getStackedTotalValue({ realIndex, j }) {
const w2 = this.w;
const ctx = this.getTotalGroupContext(realIndex);
const byGroups = w2.seriesData.stackedSeriesTotalsByGroups;
return ctx && byGroups && byGroups[ctx.groupIndex] ? byGroups[ctx.groupIndex][j] : this.barCtx.stackedSeriesTotals[j];
}
/** @param {{realIndex: any, j: any}} opts */
getStackedTotalDataLabel({ realIndex, j }) {
const w2 = this.w;
let val = this.getStackedTotalValue({ realIndex, j });
if (this.totalFormatter) {
val = this.totalFormatter(val, __spreadProps(__spreadValues({}, w2), {
seriesIndex: realIndex,
dataPointIndex: j,
w: w2
}));
}
return val;
}
/**
* @param {Record<string, any>} opts
*/
calculateColumnsDataLabelsPosition(opts) {
const w2 = this.w;
let {
i,
j,
realIndex,
y,
bcx,
barWidth,
barHeight,
textRects,
dataLabelsX,
dataLabelsY,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
strokeWidth,
offX,
offY
} = opts;
let totalDataLabelsY;
let totalDataLabelsX;
const totalDataLabelsAnchor = "middle";
const totalDataLabelsBcx = bcx;
barHeight = Math.abs(barHeight);
const vertical = w2.config.plotOptions.bar.dataLabels.orientation === "vertical";
const { zeroEncounters } = this.barCtx.barHelpers.getZeroValueEncounters({
i,
j
});
bcx = bcx - strokeWidth / 2;
const dataPointsDividedWidth = w2.layout.gridWidth / w2.globals.dataPoints;
if (this.barCtx.isVerticalGroupedRangeBar) {
dataLabelsX += barWidth / 2;
} else {
if (w2.axisFlags.isXNumeric) {
dataLabelsX = bcx - barWidth / 2 + offX;
} else {
dataLabelsX = bcx - dataPointsDividedWidth + barWidth / 2 + offX;
}
if (!w2.config.chart.stacked && zeroEncounters > 0 && w2.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
dataLabelsX -= barWidth * zeroEncounters;
}
}
if (vertical) {
const offsetDLX = 2;
dataLabelsX = dataLabelsX + textRects.height / 2 - strokeWidth / 2 - offsetDLX;
}
const valIsNegative = w2.seriesData.series[i][j] < 0;
let newY = y;
if (this.barCtx.isReversed) {
newY = y + (valIsNegative ? barHeight : -barHeight);
}
switch (barDataLabelsConfig.position) {
case "center":
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY - barHeight / 2 + offY;
} else {
dataLabelsY = newY + barHeight / 2 - offY;
}
} else {
if (valIsNegative) {
dataLabelsY = newY - barHeight / 2 + textRects.height / 2 + offY;
} else {
dataLabelsY = newY + barHeight / 2 + textRects.height / 2 - offY;
}
}
break;
case "bottom":
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY - barHeight + offY;
} else {
dataLabelsY = newY + barHeight - offY;
}
} else {
if (valIsNegative) {
dataLabelsY = newY - barHeight + textRects.height + strokeWidth + offY;
} else {
dataLabelsY = newY + barHeight - textRects.height / 2 + strokeWidth - offY;
}
}
break;
case "top":
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY + offY;
} else {
dataLabelsY = newY - offY;
}
} else {
if (valIsNegative) {
dataLabelsY = newY - textRects.height / 2 - offY;
} else {
dataLabelsY = newY + textRects.height + offY;
}
}
break;
}
let lowestPrevY = newY;
const totalGroupCtx = this.getTotalGroupContext(realIndex);
const prevYGroups = totalGroupCtx ? [totalGroupCtx.group] : w2.labelData.seriesGroups;
prevYGroups.forEach((sg) => {
var _a;
(_a = this.barCtx[sg.join(",")]) == null ? void 0 : _a.prevY.forEach(
(arr) => {
if (valIsNegative) {
lowestPrevY = Math.max(arr[j], lowestPrevY);
} else {
lowestPrevY = Math.min(arr[j], lowestPrevY);
}
}
);
});
if (this.drawsStackedTotal(realIndex) && barTotalDataLabelsConfig.enabled) {
const ADDITIONAL_OFFY = 18;
const graphics = new Graphics(this.barCtx.w);
const totalLabeltextRects = graphics.getTextRects(
this.getStackedTotalDataLabel({ realIndex, j }),
dataLabelsConfig.fontSize
);
if (valIsNegative) {
totalDataLabelsY = lowestPrevY - totalLabeltextRects.height / 2 - offY - barTotalDataLabelsConfig.offsetY + ADDITIONAL_OFFY;
} else {
totalDataLabelsY = lowestPrevY + totalLabeltextRects.height + offY + barTotalDataLabelsConfig.offsetY - ADDITIONAL_OFFY;
}
const xDivision = dataPointsDividedWidth;
totalDataLabelsX = totalDataLabelsBcx + (w2.axisFlags.isXNumeric ? -barWidth / 2 : barWidth / 2 - xDivision) + barTotalDataLabelsConfig.offsetX;
}
if (!w2.config.chart.stacked) {
if (dataLabelsY < 0) {
dataLabelsY = 0 + strokeWidth;
} else if (dataLabelsY + textRects.height / 3 > w2.layout.gridHeight) {
dataLabelsY = w2.layout.gridHeight - strokeWidth;
}
}
return {
bcx,
bcy: y,
dataLabelsX,
dataLabelsY,
totalDataLabelsX,
totalDataLabelsY,
totalDataLabelsAnchor
};
}
/**
* @param {Record<string, any>} opts
*/
calculateBarsDataLabelsPosition(opts) {
var _a;
const w2 = this.w;
let {
x,
i,
j,
realIndex,
bcy,
barHeight,
barWidth,
textRects,
dataLabelsX,
strokeWidth,
dataLabelsConfig,
barDataLabelsConfig,
barTotalDataLabelsConfig,
offX,
offY
} = opts;
const dataPointsDividedHeight = w2.layout.gridHeight / w2.globals.dataPoints;
const { zeroEncounters } = this.barCtx.barHelpers.getZeroValueEncounters({
i,
j
});
barWidth = Math.abs(barWidth);
let dataLabelsY;
if (this.barCtx.isPyramid) {
const centerOffset = (_a = textRects.centerOffset) != null ? _a : 0;
dataLabelsY = bcy + barHeight / 2 + offY - centerOffset;
} else {
dataLabelsY = bcy - (this.barCtx.isRangeBar ? 0 : dataPointsDividedHeight) + barHeight / 2 + textRects.height / 2 + offY - 3;
}
if (!w2.config.chart.stacked && zeroEncounters > 0 && w2.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
dataLabelsY -= barHeight * zeroEncounters;
}
let totalDataLabelsX;
let totalDataLabelsY;
let totalDataLabelsAnchor = "start";
const valIsNegative = w2.seriesData.series[i][j] < 0;
let newX = x;
if (this.barCtx.isReversed) {
newX = x + (valIsNegative ? -barWidth : barWidth);
totalDataLabelsAnchor = valIsNegative ? "start" : "end";
}
if (this.barCtx.isPyramid) {
dataLabelsX = w2.layout.gridWidth / 2 + offX;
} else {
switch (barDataLabelsConfig.position) {
case "center":
if (valIsNegative) {
dataLabelsX = newX + barWidth / 2 - offX;
} else {
dataLabelsX = Math.max(textRects.width / 2, newX - barWidth / 2) + offX;
}
break;
case "bottom":
if (valIsNegative) {
dataLabelsX = newX + barWidth - strokeWidth - offX;
} else {
dataLabelsX = newX - barWidth + strokeWidth + offX;
}
break;
case "top":
if (valIsNegative) {
dataLabelsX = newX - strokeWidth - offX;
} else {
dataLabelsX = newX - strokeWidth + offX;
}
break;
}
}
let lowestPrevX = newX;
const totalGroupCtx = this.getTotalGroupContext(realIndex);
const prevXGroups = totalGroupCtx ? [totalGroupCtx.group] : w2.labelData.seriesGroups;
prevXGroups.forEach((sg) => {
var _a2;
(_a2 = this.barCtx[sg.join(",")]) == null ? void 0 : _a2.prevX.forEach(
(arr) => {
if (valIsNegative) {
lowestPrevX = Math.min(arr[j], lowestPrevX);
} else {
lowestPrevX = Math.max(arr[j], lowestPrevX);
}
}
);
});
if (this.drawsStackedTotal(realIndex) && barTotalDataLabelsConfig.enabled) {
const graphics = new Graphics(this.barCtx.w);
const totalLabeltextRects = graphics.getTextRects(
this.getStackedTotalDataLabel({ realIndex, j }),
dataLabelsConfig.fontSize
);
if (valIsNegative) {
totalDataLabelsX = lowestPrevX - strokeWidth - offX - barTotalDataLabelsConfig.offsetX;
totalDataLabelsAnchor = "end";
} else {
totalDataLabelsX = lowestPrevX + offX + barTotalDataLabelsConfig.offsetX + (this.barCtx.isReversed ? -(barWidth + strokeWidth) : strokeWidth);
}
totalDataLabelsY = dataLabelsY - textRects.height / 2 + totalLabeltextRects.height / 2 + barTotalDataLabelsConfig.offsetY + strokeWidth;
if (w2.globals.barGroups.length > 1 && !totalGroupCtx) {
totalDataLabelsY = totalDataLabelsY - w2.globals.barGroups.length / 2 * (barHeight / 2);
}
}
if (!w2.config.chart.stacked) {
const flipped = valIsNegative && dataLabelsConfig.textAnchor !== "middle" ? dataLabelsConfig.textAnchor === "start" ? "end" : "start" : dataLabelsConfig.textAnchor;
let spanLeft;
if (barDataLabelsConfig.orientation === "vertical") {
spanLeft = textRects.height / 2;
} else if (flipped === "end") {
spanLeft = textRects.width;
} else if (flipped === "middle") {
spanLeft = textRects.width / 2;
} else {
spanLeft = 0;
}
const span = barDataLabelsConfig.orientation === "vertical" ? textRects.height : textRects.width;
const spanRight = span - spanLeft;
if (dataLabelsX + spanRight > w2.layout.gridWidth - strokeWidth) {
dataLabelsX = w2.layout.gridWidth - spanRight - strokeWidth;
}
if (dataLabelsX - spanLeft < strokeWidth) {
dataLabelsX = spanLeft + strokeWidth;
}
}
return {
bcx: x,
bcy,
dataLabelsX,
dataLabelsY,
totalDataLabelsX,
totalDataLabelsY,
totalDataLabelsAnchor
};
}
/** @param {{x: any, y: any, val: any, i: any, j: any, textRects: any, barHeight: any, barWidth: any, dataLabelsConfig: any}} opts */
drawCalculatedDataLabels({
x,
y,
val,
i,
// = realIndex
j,
textRects,
barHeight,
barWidth,
dataLabelsConfig
}) {
var _a, _b, _c;
const w2 = this.w;
let rotate = "rotate(0)";
if (w2.config.plotOptions.bar.dataLabels.orientation === "vertical")
rotate = `rotate(-90, ${x}, ${y})`;
const dataLabels = new DataLabels(this.barCtx.w, this.barCtx.ctx);
const graphics = new Graphics(this.barCtx.w);
const formatter = dataLabelsConfig.formatter;
let elDataLabelsWrap = null;
const isSeriesCollapsed = w2.globals.collapsedSeriesIndices.indexOf(i) > -1;
const isSeriesCollapsing = (w2.globals.collapsingSeriesIndices || []).indexOf(i) > -1;
if (isSeriesCollapsing) {
const prev = (_a = w2.globals.prevDataLabels) == null ? void 0 : _a.get(`${i}::${datumKey(w2, i, j)}`);
if (prev && isFinite(prev.val)) val = prev.val;
}
if (dataLabelsConfig.enabled && (!isSeriesCollapsed || isSeriesCollapsing)) {
elDataLabelsWrap = graphics.group({
class: "apexcharts-data-labels",
transform: rotate
});
const dlCfg = w2.config.dataLabels;
if (((_b = dlCfg.animate) == null ? void 0 : _b.enabled) || ((_c = dlCfg.countUp) == null ? void 0 : _c.enabled)) {
elDataLabelsWrap.node.setAttribute(
"data:dlKey",
`${i}::${datumKey(w2, i, j)}`
);
elDataLabelsWrap.node.setAttribute("data:dlJ", String(j));
if (typeof val === "number" && isFinite(val)) {
elDataLabelsWrap.node.setAttribute("data:dlVal", String(val));
}
}
let text = "";
if (typeof val !== "undefined") {
text = formatter(val, __spreadProps(__spreadValues({}, w2), {
seriesIndex: i,
dataPointIndex: j,
w: w2
}));
}
if (!val && w2.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
text = "";
}
const valIsNegative = w2.seriesData.series[i][j] < 0;
const position = w2.config.plotOptions.bar.dataLabels.position;
if (w2.config.plotOptions.bar.dataLabels.orientation === "vertical") {
if (position === "top") {
if (valIsNegative) dataLabelsConfig.textAnchor = "end";
else dataLabelsConfig.textAnchor = "start";
}
if (position === "center") {
dataLabelsConfig.textAnchor = "middle";
}
if (position === "bottom") {
if (valIsNegative) dataLabelsConfig.textAnchor = "end";
else dataLabelsConfig.textAnchor = "start";
}
}
if (this.barCtx.isRangeBar && this.barCtx.barOptions.dataLabels.hideOverflowingLabels) {
const txRect = graphics.getTextRects(
text,
parseFloat(dataLabelsConfig.style.fontSize).toString()
);
if (barWidth < txRect.width) {
text = "";
}
}
if (w2.config.chart.stacked && this.barCtx.barOptions.dataLabels.hideOverflowingLabels && // A collapsing series is measured against its NEW extent, which is
// already zero, so this would blank a label whose bar is still at full
// height on screen. It starts out fitting and fades away with the mark.
!isSeriesCollapsing) {
if (this.barCtx.isHorizontal) {
if (textRects.width / 1.6 > Math.abs(barWidth)) {
text = "";
}
} else {
if (textRects.height / 1.6 > Math.abs(barHeight)) {
text = "";
}
}
}
const modifiedDataLabelsConfig = __spreadValues({}, dataLabelsConfig);
if (this.barCtx.isHorizontal) {
if (val < 0) {
if (dataLabelsConfig.textAnchor === "start") {
modifiedDataLabelsConfig.textAnchor = "end";
} else if (dataLabelsConfig.textAnchor === "end") {
modifiedDataLabelsConfig.textAnchor = "start";
}
}
}
dataLabels.plotDataLabelsText({
x,
y,
text,
i,
j,
parent: elDataLabelsWrap,
dataLabelsConfig: modifiedDataLabelsConfig,
alwaysDrawDataLabel: true,
offsetCorrection: true
});
}
return elDataLabelsWrap;
}
/** @param {{ x?: any, y?: any, val?: any, rawVal?: any, realIndex?: any, j?: any, textAnchor?: any, barWidth?: any, barHeight?: any, dataLabelsConfig?: any, barTotalDataLabelsConfig?: any }} opts */
drawTotalDataLabels({
x,
y,
val,
rawVal,
realIndex,
j,
textAnchor,
barTotalDataLabelsConfig
}) {
var _a, _b;
const graphics = new Graphics(this.barCtx.w);
let totalDataLabelText;
if (barTotalDataLabelsConfig.enabled && typeof x !== "undefined" && typeof y !== "undefined" && this.drawsStackedTotal(realIndex)) {
totalDataLabelText = graphics.drawText({
x,
y,
foreColor: barTotalDataLabelsConfig.style.color,
text: val,
textAnchor,
fontFamily: barTotalDataLabelsConfig.style.fontFamily,
fontSize: barTotalDataLabelsConfig.style.fontSize,
fontWeight: barTotalDataLabelsConfig.style.fontWeight
});
totalDataLabelText.attr({
class: "apexcharts-datalabel-total",
cx: x,
cy: y
});
const dlCfg = this.w.config.dataLabels;
if (((_a = dlCfg.animate) == null ? void 0 : _a.enabled) || ((_b = dlCfg.countUp) == null ? void 0 : _b.enabled)) {
const { groupIndex } = this.barCtx.barHelpers.getGroupIndex(realIndex);
totalDataLabelText.node.setAttribute(
"data:dlTotalKey",
`${groupIndex}::${datumKey(this.w, realIndex, j)}`
);
totalDataLabelText.node.setAttribute(
"data:dlTotalSeries",
String(realIndex)
);
if (typeof rawVal === "number" && isFinite(rawVal)) {
totalDataLabelText.node.setAttribute("data:dlTotalVal", String(rawVal));
}
}
}
return totalDataLabelText;
}
}
function isHistogramOverlay(w2) {
var _a, _b, _c, _d, _e, _f, _g;
if (((_b = (_a = w2 == null ? void 0 : w2.config) == null ? void 0 : _a.chart) == null ? void 0 : _b.requestedType) !== "histogram") return false;
if (((_d = (_c = w2.config.plotOptions) == null ? void 0 : _c.histogram) == null ? void 0 : _d.overlap) === false) return false;
return ((_g = (_f = (_e = w2.seriesData) == null ? void 0 : _e.series) == null ? void 0 : _f.length) != null ? _g : 0) > 1;
}
let Helpers$1 = class Helpers3 {
/**
* @param {Record<string, any>} barCtx
*/
constructor(barCtx) {
this.w = barCtx.w;
this.barCtx = barCtx;
}
/**
* @param {any[]} series
*/
initVariables(series) {
const w2 = this.w;
this.barCtx.series = series;
this.barCtx.totalItems = 0;
this.barCtx.seriesLen = 0;
this.barCtx.visibleI = -1;
this.barCtx.visibleItems = 1;
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length > 0) {
this.barCtx.seriesLen = this.barCtx.seriesLen + 1;
this.barCtx.totalItems += series[sl].length;
}
if (w2.axisFlags.isXNumeric) {
for (let j = 0; j < series[sl].length; j++) {
if (w2.seriesData.seriesX[sl][j] > w2.globals.minX && w2.seriesData.seriesX[sl][j] < w2.globals.maxX) {
this.barCtx.visibleItems++;
}
}
} else {
this.barCtx.visibleItems = w2.globals.dataPoints;
}
}
this.arrBorderRadius = this.createBorderRadiusArr(w2.seriesData.series);
if (Utils.isSafari()) {
this.arrBorderRadius = this.arrBorderRadius.map(
(brArr) => (
/**
* @param {any} _
*/
brArr.map((_) => "none")
)
);
}
if (this.barCtx.seriesLen === 0) {
this.barCtx.seriesLen = 1;
}
}
/**
* @param {number} realIndex
*/
initialPositions(realIndex) {
const w2 = this.w;
let x, y, yDivision, xDivision, barHeight, barWidth, zeroH, zeroW;
let dataPoints = w2.globals.dataPoints;
if (this.barCtx.isRangeBar) {
dataPoints = w2.labelData.labels.length;
}
let seriesLen = this.barCtx.seriesLen;
if (w2.config.plotOptions.bar.rangeBarGroupRows || isHistogramOverlay(w2)) {
seriesLen = 1;
}
if (this.barCtx.isHorizontal) {
yDivision = w2.layout.gridHeight / dataPoints;
barHeight = yDivision / seriesLen;
if (w2.axisFlags.isXNumeric) {
yDivision = w2.layout.gridHeight / this.barCtx.totalItems;
barHeight = yDivision / this.barCtx.seriesLen;
}
barHeight = barHeight * parseInt(this.barCtx.barOptions.barHeight, 10) / 100;
if (String(this.barCtx.barOptions.barHeight).indexOf("%") === -1) {
barHeight = parseInt(this.barCtx.barOptions.barHeight, 10);
}
zeroW = this.barCtx.baseLineInvertedY + w2.globals.padHorizontal + (this.barCtx.isReversed ? w2.layout.gridWidth : 0) - (this.barCtx.isReversed ? this.barCtx.baseLineInvertedY * 2 : 0);
if (this.barCtx.isFunnel) {
zeroW = w2.layout.gridWidth / 2;
}
y = (yDivision - barHeight * this.barCtx.seriesLen) / 2;
} else {
xDivision = w2.layout.gridWidth / this.barCtx.visibleItems;
if (w2.config.xaxis.convertedCatToNumeric) {
xDivision = w2.layout.gridWidth / w2.globals.dataPoints;
}
barWidth = xDivision / seriesLen * parseInt(this.barCtx.barOptions.columnWidth, 10) / 100;
if (w2.axisFlags.isXNumeric) {
const xRatio = this.barCtx.xRatio;
if (w2.globals.minXDiff && w2.globals.minXDiff !== 0.5 && w2.globals.minXDiff / xRatio > 0) {
xDivision = w2.globals.minXDiff / xRatio;
}
barWidth = xDivision / seriesLen * parseInt(this.barCtx.barOptions.columnWidth, 10) / 100;
if (barWidth < 1) {
barWidth = 1;
}
}
if (String(this.barCtx.barOptions.columnWidth).indexOf("%") === -1) {
barWidth = parseInt(this.barCtx.barOptions.columnWidth, 10);
}
zeroH = w2.layout.gridHeight - this.barCtx.baseLineY[this.barCtx.translationsIndex] - (this.barCtx.isReversed ? w2.layout.gridHeight : 0) + (this.barCtx.isReversed ? this.barCtx.baseLineY[this.barCtx.translationsIndex] * 2 : 0);
if (w2.axisFlags.isXNumeric) {
const xForNumericX = this.barCtx.getBarXForNumericXAxis({
x,
j: 0,
realIndex,
barWidth
});
x = xForNumericX.x;
} else {
x = w2.globals.padHorizontal + Utils.noExponents(xDivision - barWidth * this.barCtx.seriesLen) / 2;
}
}
w2.globals.barHeight = barHeight;
w2.globals.barWidth = barWidth;
return {
x,
y,
yDivision,
xDivision,
barHeight,
barWidth,
zeroH,
zeroW
};
}
/**
* @param {Record<string, any>} ctx
*/
initializeStackedPrevVars(ctx) {
const w2 = ctx.w;
w2.labelData.seriesGroups.forEach((group) => {
if (!ctx[group]) ctx[group] = {};
ctx[group].prevY = [];
ctx[group].prevX = [];
ctx[group].prevYF = [];
ctx[group].prevXF = [];
ctx[group].prevYVal = [];
ctx[group].prevXVal = [];
});
}
/**
* @param {Record<string, any>} ctx
*/
initializeStackedXYVars(ctx) {
const w2 = ctx.w;
w2.labelData.seriesGroups.forEach((group) => {
if (!ctx[group]) ctx[group] = {};
ctx[group].xArrj = [];
ctx[group].xArrjF = [];
ctx[group].xArrjVal = [];
ctx[group].yArrj = [];
ctx[group].yArrjF = [];
ctx[group].yArrjVal = [];
});
}
/**
* @param {any[]} series
* @param {number} i
* @param {number} j
* @param {number} realIndex
*/
getPathFillColor(series, i, j, realIndex) {
var _a, _b, _c, _d;
const w2 = this.w;
const fill = new Fill(this.barCtx.w);
let fillColor = null;
const seriesNumber = this.barCtx.barOptions.distributed ? j : i;
let useRangeColor = false;
if (this.barCtx.barOptions.colors.ranges.length > 0) {
const colorRange = this.barCtx.barOptions.colors.ranges;
colorRange.map((range) => {
if (series[i][j] >= range.from && series[i][j] <= range.to) {
fillColor = range.color;
useRangeColor = true;
}
});
}
const pathFill = fill.fillPath({
seriesNumber: this.barCtx.barOptions.distributed ? seriesNumber : realIndex,
dataPointIndex: j,
color: fillColor,
value: series[i][j],
fillConfig: (_a = w2.config.series[i].data[j]) == null ? void 0 : _a.fill,
fillType: ((_c = (_b = w2.config.series[i].data[j]) == null ? void 0 : _b.fill) == null ? void 0 : _c.type) ? (_d = w2.config.series[i].data[j]) == null ? void 0 : _d.fill.type : Array.isArray(w2.config.fill.type) ? w2.config.fill.type[realIndex] : w2.config.fill.type
});
return {
color: pathFill,
useRangeColor
};
}
/**
* @param {number} i
* @param {number} j
* @param {number} realIndex
*/
getStrokeWidth(i, j, realIndex) {
let strokeWidth = 0;
const w2 = this.w;
if (typeof this.barCtx.series[i][j] === "undefined" || this.barCtx.series[i][j] === null || w2.config.chart.type === "bar" && !this.barCtx.series[i][j]) {
this.barCtx.isNullValue = true;
} else {
this.barCtx.isNullValue = false;
}
if (w2.config.stroke.show) {
if (!this.barCtx.isNullValue) {
strokeWidth = Array.isArray(this.barCtx.strokeWidth) ? this.barCtx.strokeWidth[realIndex] : this.barCtx.strokeWidth;
}
}
return strokeWidth;
}
/**
* Series indices bucketed into the stacks they actually draw in: one bucket
* per series group, or a single bucket holding every series when the chart is
* not grouped. Order within a bucket follows series order, which is stacking
* order.
*
* @param {number} numSeries
* @returns {number[][]}
*/
getStackedSeriesIndices(numSeries) {
const groups = this.w.labelData.seriesGroups;
if (!groups || groups.length < 2) {
return [Array.from({ length: numSeries }, (_, i) => i)];
}
const buckets = Array.from({ length: groups.length }, () => []);
const ungrouped = [];
for (let i = 0; i < numSeries; i++) {
const g2 = this.getSeriesGroupIndex(i);
if (g2 > -1) buckets[g2].push(i);
else ungrouped.push(i);
}
if (ungrouped.length) buckets.push(ungrouped);
return buckets.filter((b2) => b2.length > 0);
}
/**
* Which corners each bar rounds, as a [seriesIndex][dataPointIndex] grid of
* 'top' | 'bottom' | 'both' | 'none'.
*
* A rounded corner belongs to the OUTSIDE of a stack, so this resolves, per
* data point, the outermost segment on each side of the baseline; everything
* sandwiched between them stays square.
*
* Crucially a "stack" is a series GROUP, not the whole chart. A grouped
* stacked chart draws one independent stack per group, side by side, and each
* one needs its own outermost segments. Resolving chart-wide instead put the
* radius on the bottom of the first group's lowest series and the top of the
* last group's highest, leaving every stack in between completely square,
* which is exactly how it looked: the first column rounded at the bottom, the
* second at the top, and nothing else touched. Stacked totals already resolve
* per group (see drawsStackedTotal, #4173); corners never got the same fix.
*
* @param {any[]} series
* @returns {string[][]}
*/
createBorderRadiusArr(series) {
var _a;
const w2 = this.w;
const alwaysApplyRadius = !this.w.config.chart.stacked || w2.config.plotOptions.bar.borderRadius <= 0;
const numSeries = series.length;
const numColumns = ((_a = series[0]) == null ? void 0 : _a.length) | 0;
const output = Array.from(
{ length: numSeries },
() => Array(numColumns).fill(alwaysApplyRadius ? "top" : "none")
);
if (alwaysApplyRadius) return output;
const isSoloHorizontal = this.w.config.chart.type === "bar" && numColumns === 1;
const soloCorner = isSoloHorizontal ? "top" : "both";
const baseCorner = isSoloHorizontal ? "top" : "bottom";
for (const stack of this.getStackedSeriesIndices(numSeries)) {
for (let j = 0; j < numColumns; j++) {
const positiveIndices = [];
const negativeIndices = [];
for (const i of stack) {
const value = series[i][j];
if (value > 0) positiveIndices.push(i);
else if (value < 0) negativeIndices.push(i);
}
if (positiveIndices.length > 0 && negativeIndices.length === 0) {
if (positiveIndices.length === 1) {
output[positiveIndices[0]][j] = soloCorner;
} else {
const first = positiveIndices[0];
const last = positiveIndices[positiveIndices.length - 1];
for (const i of positiveIndices) {
output[i][j] = i === first ? baseCorner : i === last ? "top" : "none";
}
}
} else if (negativeIndices.length > 0 && positiveIndices.length === 0) {
if (negativeIndices.length === 1) {
output[negativeIndices[0]][j] = "both";
} else {
const highest = Math.max(...negativeIndices);
const lowest = Math.min(...negativeIndices);
for (const i of negativeIndices) {
output[i][j] = i === highest ? "bottom" : i === lowest ? "top" : "none";
}
}
} else if (positiveIndices.length > 0 && negativeIndices.length > 0) {
const lastPositive = positiveIndices[positiveIndices.length - 1];
for (const i of positiveIndices) {
output[i][j] = i === lastPositive ? "top" : "none";
}
const highestNegative = Math.max(...negativeIndices);
for (const i of negativeIndices) {
output[i][j] = i === highestNegative ? "bottom" : "none";
}
}
}
}
return output;
}
/** @param {{ j?: any, i?: any, x1?: any, x2?: any, y1?: any, y2?: any, bc?: any, elSeries?: any }} opts */
barBackground({ j, i, x1, x2, y1, y2, elSeries }) {
const w2 = this.w;
const graphics = new Graphics(this.barCtx.w);
const sr = new Series(this.barCtx.w);
const activeSeriesIndex = sr.getActiveConfigSeriesIndex();
if (this.barCtx.barOptions.colors.backgroundBarColors.length > 0 && activeSeriesIndex === i) {
if (j >= this.barCtx.barOptions.colors.backgroundBarColors.length) {
j %= this.barCtx.barOptions.colors.backgroundBarColors.length;
}
const bcolor = this.barCtx.barOptions.colors.backgroundBarColors[j];
const rect = graphics.drawRect(
typeof x1 !== "undefined" ? x1 : 0,
typeof y1 !== "undefined" ? y1 : 0,
typeof x2 !== "undefined" ? x2 : w2.layout.gridWidth,
typeof y2 !== "undefined" ? y2 : w2.layout.gridHeight,
this.barCtx.barOptions.colors.backgroundBarRadius,
bcolor,
this.barCtx.barOptions.colors.backgroundBarOpacity
);
elSeries.add(rect);
rect.node.classList.add("apexcharts-backgroundBar");
}
}
/** @param {{ barWidth?: any, barXPosition?: any, y1?: any, y2?: any, yRatio?: any, strokeWidth?: any, isReversed?: any, series?: any, seriesGroup?: any, realIndex?: any, i?: any, j?: any, w?: any }} opts */
getColumnPaths({
barWidth,
barXPosition,
y1,
y2,
strokeWidth,
isReversed,
series,
seriesGroup,
realIndex,
i,
j,
w: w2
}) {
var _a, _b, _c;
const graphics = new Graphics(this.barCtx.w);
strokeWidth = Array.isArray(strokeWidth) ? strokeWidth[realIndex] : strokeWidth;
if (!strokeWidth) strokeWidth = 0;
let bW = barWidth;
let bXP = barXPosition;
if ((_a = w2.config.series[realIndex].data[j]) == null ? void 0 : _a.columnWidthOffset) {
bXP = barXPosition - w2.config.series[realIndex].data[j].columnWidthOffset / 2;
bW = barWidth + w2.config.series[realIndex].data[j].columnWidthOffset;
}
const strokeCenter = strokeWidth / 2;
const x1 = bXP + strokeCenter;
const x2 = bXP + bW - strokeCenter;
const direction = (series[i][j] >= 0 ? 1 : -1) * (isReversed ? -1 : 1);
y1 += 1e-3 - strokeCenter * direction;
y2 += 1e-3 + strokeCenter * direction;
const sl = graphics.line(x2, y1);
const closing = w2.config.plotOptions.bar.borderRadiusApplication === "around" || this.arrBorderRadius[realIndex][j] === "both" ? " Z" : " z";
const squarePathTo = graphics.move(x1, y1) + graphics.line(x1, y2) + graphics.line(x2, y2) + sl + closing;
let pathTo = squarePathTo;
if (this.arrBorderRadius[realIndex][j] !== "none") {
pathTo = graphics.roundPathCorners(
pathTo,
w2.config.plotOptions.bar.borderRadius
);
}
let pathFrom = null;
const morphFrom = (_c = (_b = this.barCtx.ctx) == null ? void 0 : _b.morphTypeChange) == null ? void 0 : _c.getInitialPathFor(
realIndex,
j
);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, pathTo, squarePathTo);
}
if (pathFrom == null) {
pathFrom = graphics.move(x1, y1) + graphics.line(x1, y1) + sl + sl + sl + sl + sl + graphics.line(x1, y1) + closing;
}
if (w2.config.chart.stacked) {
let _ctx = this.barCtx;
_ctx = this.barCtx[seriesGroup];
_ctx.yArrj.push(y2 - strokeCenter * direction);
_ctx.yArrjF.push(Math.abs(y1 - y2 + strokeWidth * direction));
_ctx.yArrjVal.push(this.barCtx.series[i][j]);
}
return {
pathTo,
pathFrom
};
}
/**
* Build a trapezoidal funnel-stage path. Used when
* `plotOptions.funnel.shape === 'trapezoid'` is active alongside `isFunnel`.
*
* Each stage is a 4-corner polygon whose top width matches the current
* stage's value and bottom width matches the next stage's value, producing
* continuous sloped sides between consecutive stages.
*
* For the last stage, the bottom width is configurable:
* - `lastShape: 'flat'` → bottom width = top width (parallel sides)
* - `lastShape: 'taper'` → bottom width = 0 (taper to a point)
*
* @param {{ barYPosition: number, barHeight: number, series: any[][], i: number, j: number, realIndex: number, strokeWidth: number, w: any }} opts
*/
getFunnelTrapezoidPaths({
barYPosition,
barHeight,
series,
i,
j,
realIndex,
strokeWidth,
w: w2
}) {
var _a, _b;
const graphics = new Graphics(this.barCtx.w);
const center = w2.layout.gridWidth / 2;
const halfWidthFor = (v2) => Math.abs(v2 / this.barCtx.invertedYRatio) / 2;
const topHalf = halfWidthFor(series[i][j]);
const lastIdx = series[i].length - 1;
const isLast = j === lastIdx;
const lastShape = w2.config.plotOptions.funnel.lastShape === "taper" ? "taper" : "flat";
let bottomHalf;
if (isLast) {
bottomHalf = lastShape === "taper" ? 0 : topHalf;
} else {
bottomHalf = halfWidthFor(series[i][j + 1]);
}
const strokeCenter = strokeWidth / 2;
const y1 = barYPosition + strokeCenter;
const y2 = barYPosition + barHeight - strokeCenter;
const topLeftX = center - topHalf;
const topRightX = center + topHalf;
const bottomLeftX = center - bottomHalf;
const bottomRightX = center + bottomHalf;
const pathTo = graphics.move(topLeftX, y1) + graphics.line(topRightX, y1) + graphics.line(bottomRightX, y2) + graphics.line(bottomLeftX, y2) + " Z";
let pathFrom = null;
const morphFrom = (_b = (_a = this.barCtx.ctx) == null ? void 0 : _a.morphTypeChange) == null ? void 0 : _b.getInitialPathFor(
realIndex,
j
);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, pathTo);
}
if (pathFrom == null) {
pathFrom = graphics.move(center, y1) + graphics.line(center, y1) + graphics.line(center, y2) + graphics.line(center, y2) + " Z";
}
return {
pathTo,
pathFrom,
// x is the right edge of the wider (top) side — used by dataLabel
// positioning helpers that expect a "right" reference.
x: topRightX,
x1: topLeftX,
barXPosition: center
};
}
/**
* Pre-compute the per-segment layout for a value-proportional pyramid.
*
* Each segment is a horizontal slice of a triangle whose apex sits at the
* top of the plot area (width = 0) and whose base spans `gridWidth` at the
* bottom. The vertical extent of each slice is its share of the total
* series value, so areas track value contribution and segments share
* edges (no gaps). The first data point is the apex, the last is the base.
*
* @param {any[]} seriesData - 1D array of values for a single series row
* @returns {{ y: number, height: number, topHalf: number, bottomHalf: number }[]}
*/
computePyramidLayout(seriesData) {
const w2 = this.w;
const gridHeight = w2.layout.gridHeight;
const gridWidth = w2.layout.gridWidth;
const values = seriesData.map(
/** @param {any} v */
(v2) => Math.abs(Number(v2) || 0)
);
const total = values.reduce(
/** @param {number} a @param {number} b */
(a, b2) => a + b2,
0
);
if (total === 0 || gridHeight <= 0) {
return values.map(() => ({ y: 0, height: 0, topHalf: 0, bottomHalf: 0 }));
}
const halfWidth = gridWidth / 2;
let cumulative = 0;
const layout = [];
for (let j = 0; j < values.length; j++) {
const topRatio = cumulative / total;
cumulative += values[j];
const bottomRatio = cumulative / total;
const topY = topRatio * gridHeight;
const bottomY = bottomRatio * gridHeight;
layout.push({
y: topY,
height: bottomY - topY,
topHalf: topRatio * halfWidth,
bottomHalf: bottomRatio * halfWidth
});
}
return layout;
}
/**
* Build a single pyramid stage path. Geometry is precomputed by
* `computePyramidLayout`; this method only renders that geometry into an
* SVG path string plus a `pathFrom` for entry/morph animations.
*
* @param {{ barYPosition: number, barHeight: number, topHalf: number, bottomHalf: number, realIndex: number, j: number, strokeWidth: number, w: any }} opts
*/
getPyramidPaths({
barYPosition,
barHeight,
topHalf,
bottomHalf,
realIndex,
j,
strokeWidth,
w: w2
}) {
var _a, _b;
const graphics = new Graphics(this.barCtx.w);
const center = w2.layout.gridWidth / 2;
const strokeCenter = strokeWidth / 2;
const y1 = barYPosition + strokeCenter;
const y2 = barYPosition + barHeight - strokeCenter;
const topLeftX = center - topHalf;
const topRightX = center + topHalf;
const bottomLeftX = center - bottomHalf;
const bottomRightX = center + bottomHalf;
const pathTo = graphics.move(topLeftX, y1) + graphics.line(topRightX, y1) + graphics.line(bottomRightX, y2) + graphics.line(bottomLeftX, y2) + " Z";
let pathFrom = null;
const morphFrom = (_b = (_a = this.barCtx.ctx) == null ? void 0 : _a.morphTypeChange) == null ? void 0 : _b.getInitialPathFor(
realIndex,
j
);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, pathTo);
}
if (pathFrom == null) {
pathFrom = graphics.move(center, y1) + graphics.line(center, y1) + graphics.line(center, y2) + graphics.line(center, y2) + " Z";
}
return {
pathTo,
pathFrom,
x: topRightX,
x1: topLeftX,
barXPosition: center
};
}
/** @param {{ barYPosition?: any, barHeight?: any, x1?: any, x2?: any, strokeWidth?: any, isReversed?: any, series?: any, seriesGroup?: any, realIndex?: any, i?: any, j?: any, w?: any }} opts */
getBarpaths({
barYPosition,
barHeight,
x1,
x2,
strokeWidth,
isReversed,
series,
seriesGroup,
realIndex,
i,
j,
w: w2
}) {
var _a, _b, _c;
const graphics = new Graphics(this.barCtx.w);
strokeWidth = Array.isArray(strokeWidth) ? strokeWidth[realIndex] : strokeWidth;
if (!strokeWidth) strokeWidth = 0;
let bYP = barYPosition;
let bH = barHeight;
if ((_a = w2.config.series[realIndex].data[j]) == null ? void 0 : _a.barHeightOffset) {
bYP = barYPosition - w2.config.series[realIndex].data[j].barHeightOffset / 2;
bH = barHeight + w2.config.series[realIndex].data[j].barHeightOffset;
}
const strokeCenter = strokeWidth / 2;
const y1 = bYP + strokeCenter;
const y2 = bYP + bH - strokeCenter;
const direction = (series[i][j] >= 0 ? 1 : -1) * (isReversed ? -1 : 1);
x1 += 1e-3 + strokeCenter * direction;
x2 += 1e-3 - strokeCenter * direction;
const isFunnel = this.barCtx.isFunnel;
const fromX = isFunnel ? (x1 + x2) / 2 : x1;
const sl = graphics.line(x1, y2);
const closing = w2.config.plotOptions.bar.borderRadiusApplication === "around" || this.arrBorderRadius[realIndex][j] === "both" ? " Z" : " z";
const squarePathTo = graphics.move(x1, y1) + graphics.line(x2, y1) + graphics.line(x2, y2) + sl + closing;
let pathTo = squarePathTo;
if (this.arrBorderRadius[realIndex][j] !== "none") {
pathTo = graphics.roundPathCorners(
pathTo,
w2.config.plotOptions.bar.borderRadius
);
}
let pathFrom = null;
const morphFrom = (_c = (_b = this.barCtx.ctx) == null ? void 0 : _b.morphTypeChange) == null ? void 0 : _c.getInitialPathFor(
realIndex,
j
);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, pathTo, squarePathTo);
}
if (pathFrom == null) {
const slFrom = isFunnel ? graphics.line(fromX, y2) : sl;
pathFrom = graphics.move(fromX, y1) + graphics.line(fromX, y1) + slFrom + slFrom + slFrom + slFrom + slFrom + graphics.line(fromX, y1) + closing;
}
if (w2.config.chart.stacked) {
let _ctx = this.barCtx;
_ctx = this.barCtx[seriesGroup];
_ctx.xArrj.push(x2 + strokeCenter * direction);
_ctx.xArrjF.push(Math.abs(x1 - x2 - strokeWidth * direction));
_ctx.xArrjVal.push(this.barCtx.series[i][j]);
}
return {
pathTo,
pathFrom
};
}
/**
* @param {number} value
* @param {number} zeroW
*/
getXForValue(value, zeroW, zeroPositionForNull = true) {
let xForVal = zeroPositionForNull ? zeroW : null;
if (typeof value !== "undefined" && value !== null) {
xForVal = zeroW + value / this.barCtx.invertedYRatio - (this.barCtx.isReversed ? value / this.barCtx.invertedYRatio : 0) * 2;
}
return xForVal;
}
/**
* @param {number} value
* @param {number} zeroH
* @param {number} translationsIndex
*/
getYForValue(value, zeroH, translationsIndex, zeroPositionForNull = true) {
let yForVal = zeroPositionForNull ? zeroH : null;
if (typeof value !== "undefined" && value !== null) {
yForVal = zeroH - value / this.barCtx.yRatio[translationsIndex] + (this.barCtx.isReversed ? value / this.barCtx.yRatio[translationsIndex] : 0) * 2;
}
return yForVal;
}
/**
* @param {string} type
* @param {number} zeroW
* @param {number} zeroH
* @param {number} i
* @param {number} j
* @param {number} translationsIndex
*/
getGoalValues(type, zeroW, zeroH, i, j, translationsIndex) {
const w2 = this.w;
const goals = [];
const pushGoal = (value, attrs) => {
goals.push({
[type]: type === "x" ? this.getXForValue(value, zeroW, false) : this.getYForValue(value, zeroH, translationsIndex, false),
attrs
});
};
if (w2.seriesData.seriesGoals[i] && w2.seriesData.seriesGoals[i][j] && Array.isArray(w2.seriesData.seriesGoals[i][j])) {
w2.seriesData.seriesGoals[i][j].forEach((goal) => {
pushGoal(goal.value, goal);
});
}
if (this.barCtx.barOptions.isDumbbell && w2.rangeData.seriesRange.length) {
const colors = this.barCtx.barOptions.dumbbellColors ? this.barCtx.barOptions.dumbbellColors : w2.globals.colors;
const commonAttrs = {
strokeHeight: type === "x" ? 0 : w2.globals.markers.size[i],
strokeWidth: type === "x" ? w2.globals.markers.size[i] : 0,
strokeDashArray: 0,
strokeLineCap: "round",
strokeColor: Array.isArray(colors[i]) ? colors[i][0] : colors[i]
};
pushGoal(w2.rangeData.seriesRangeStart[i][j], commonAttrs);
pushGoal(w2.rangeData.seriesRangeEnd[i][j], __spreadProps(__spreadValues({}, commonAttrs), {
strokeColor: Array.isArray(colors[i]) ? colors[i][1] : colors[i]
}));
}
return goals;
}
/** @param {{barXPosition: any, barYPosition: any, goalX: any, goalY: any, barWidth: any, barHeight: any}} opts */
drawGoalLine({
barXPosition,
barYPosition,
goalX,
goalY,
barWidth,
barHeight
}) {
const hasGoals = Array.isArray(goalX) && goalX.length > 0 || Array.isArray(goalY) && goalY.length > 0;
if (!hasGoals) {
return null;
}
const graphics = new Graphics(this.barCtx.w);
const lineGroup = graphics.group({
className: "apexcharts-bar-goals-groups"
});
lineGroup.node.classList.add("apexcharts-element-hidden");
this.barCtx.w.globals.delayedElements.push({
el: lineGroup.node
});
lineGroup.attr(
"clip-path",
`url(#gridRectMarkerMask${this.barCtx.w.globals.cuid})`
);
let line = null;
if (this.barCtx.isHorizontal) {
if (Array.isArray(goalX)) {
goalX.forEach((goal) => {
if (goal.x >= -1 && goal.x <= graphics.w.layout.gridWidth + 1) {
const sHeight = typeof goal.attrs.strokeHeight !== "undefined" ? goal.attrs.strokeHeight : barHeight / 2;
const y = barYPosition + sHeight + barHeight / 2;
line = graphics.drawLine(
goal.x,
y - sHeight * 2,
goal.x,
y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : void 0,
goal.attrs.strokeDashArray,
goal.attrs.strokeWidth ? goal.attrs.strokeWidth : 2,
goal.attrs.strokeLineCap
);
lineGroup.add(line);
}
});
}
} else {
if (Array.isArray(goalY)) {
goalY.forEach((goal) => {
if (goal.y >= -1 && goal.y <= graphics.w.layout.gridHeight + 1) {
const sWidth = typeof goal.attrs.strokeWidth !== "undefined" ? goal.attrs.strokeWidth : barWidth / 2;
const x = barXPosition + sWidth + barWidth / 2;
line = graphics.drawLine(
x - sWidth * 2,
goal.y,
x,
goal.y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : void 0,
goal.attrs.strokeDashArray,
goal.attrs.strokeHeight ? goal.attrs.strokeHeight : 2,
goal.attrs.strokeLineCap
);
lineGroup.add(line);
}
});
}
}
return lineGroup;
}
/** @param {{prevPaths: any, currPaths: any, color: any, realIndex: any, j: any}} opts */
drawBarShadow({ prevPaths, currPaths, color, realIndex, j }) {
const w2 = this.w;
const { x: prevX2, x1: prevX1, barYPosition: prevY1 } = prevPaths;
const { x: currX2, x1: currX1, barYPosition: currY1 } = currPaths;
const prevY2 = prevY1 + currPaths.barHeight;
const graphics = new Graphics(this.barCtx.w);
const utils = new Utils();
const shadowPath = graphics.move(prevX1, prevY2) + graphics.line(prevX2, prevY2) + graphics.line(currX2, currY1) + graphics.line(currX1, currY1) + graphics.line(prevX1, prevY2) + (w2.config.plotOptions.bar.borderRadiusApplication === "around" || this.arrBorderRadius[realIndex][j] === "both" ? " Z" : " z");
return graphics.drawPath({
d: shadowPath,
fill: utils.shadeColor(0.5, Utils.rgb2hex(color)),
stroke: "none",
strokeWidth: 0,
fillOpacity: 1,
classes: "apexcharts-bar-shadow apexcharts-decoration-element"
});
}
/** @param {{i: any, j: any}} opts */
getZeroValueEncounters({ i, j }) {
var _a;
const w2 = this.w;
let nonZeroColumns = 0;
let zeroEncounters = 0;
const seriesIndices = w2.config.plotOptions.bar.horizontal ? w2.seriesData.series.map((_, _i) => _i) : ((_a = w2.globals.columnSeries) == null ? void 0 : _a.i.map((_i) => _i)) || [];
seriesIndices.forEach((_si) => {
const val = w2.globals.seriesPercent[_si][j];
if (val) {
nonZeroColumns++;
}
if (_si < i && val === 0) {
zeroEncounters++;
}
});
return {
nonZeroColumns,
zeroEncounters
};
}
/**
* Index of the series group `seriesIndex` belongs to within
* `w.labelData.seriesGroups`, or -1 when the chart has no groups.
*
* Unlike `getGroupIndex` this is a pure lookup: it never appends to
* `columnGroupIndices`, so it is safe to call from positioning/label code
* that must not perturb the draw order bookkeeping.
* @param {number} seriesIndex
* @returns {number}
*/
getSeriesGroupIndex(seriesIndex) {
const w2 = this.w;
return w2.labelData.seriesGroups.findIndex(
(group) => group.indexOf(w2.seriesData.seriesNames[seriesIndex]) > -1
);
}
/**
* @param {number} seriesIndex
*/
getGroupIndex(seriesIndex) {
const w2 = this.w;
const groupIndex = w2.labelData.seriesGroups.findIndex(
(group) => (
// w.config.series[i].name may be undefined, so use
// w.seriesData.seriesNames[i], which has default names for those
// series. w.labelData.seriesGroups[] uses the same default naming.
group.indexOf(w2.seriesData.seriesNames[seriesIndex]) > -1
)
);
const cGI = this.barCtx.columnGroupIndices;
let columnGroupIndex = cGI.indexOf(groupIndex);
if (columnGroupIndex < 0) {
cGI.push(groupIndex);
columnGroupIndex = cGI.length - 1;
}
return { groupIndex, columnGroupIndex };
}
};
class Bar {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
* @param {import('../types/internal').XYRatios} xyRatios
*/
constructor(w2, ctx, xyRatios) {
this.ctx = ctx;
this.w = w2;
this.barOptions = w2.config.plotOptions.bar;
this.isHorizontal = this.barOptions.horizontal;
this.strokeWidth = w2.config.stroke.width;
this.isNullValue = false;
this.isRangeBar = w2.rangeData.seriesRange.length && this.isHorizontal;
this.isVerticalGroupedRangeBar = !w2.globals.isBarHorizontal && w2.rangeData.seriesRange.length && w2.config.plotOptions.bar.rangeBarGroupRows;
this.isFunnel = this.barOptions.isFunnel;
this.isPyramid = this.barOptions.isPyramid;
this.pyramidLayout = null;
this.xyRatios = xyRatios;
this.xRatio = 0;
this.yRatio = [];
this.invertedXRatio = 0;
this.invertedYRatio = 0;
this.baseLineY = [];
this.baseLineInvertedY = 0;
if (this.xyRatios !== null) {
this.xRatio = xyRatios.xRatio;
this.yRatio = xyRatios.yRatio;
this.invertedXRatio = xyRatios.invertedXRatio;
this.invertedYRatio = xyRatios.invertedYRatio;
this.baseLineY = xyRatios.baseLineY;
this.baseLineInvertedY = xyRatios.baseLineInvertedY;
}
this.yaxisIndex = 0;
this.translationsIndex = 0;
this.seriesLen = 0;
this.pathArr = [];
this._prevKeyed = null;
this._ltCache = null;
this._layoutShiftCache = null;
this._pathToInterp = null;
this.series = [];
this.elSeries = null;
this.visibleI = 0;
this.isReversed = false;
const ser = new Series(this.w);
this.lastActiveBarSerieIndex = ser.getActiveConfigSeriesIndex("desc", [
"bar",
"column"
]);
this.lastActiveBarSerieIndexByGroup = ser.getActiveConfigSeriesIndexByGroup(["bar", "column"]);
this.columnGroupIndices = [];
const barSeriesIndices = ser.getBarSeriesIndices();
const coreUtils = new CoreUtils(this.w);
this.stackedSeriesTotals = coreUtils.getStackedSeriesTotals(
this.w.config.series.map((s, i) => {
return barSeriesIndices.indexOf(i) === -1 ? i : -1;
}).filter((s) => {
return s !== -1;
})
);
this.barHelpers = new Helpers$1(this);
}
/** primary draw method which is called on bar object
* @memberof Bar
* @param {any[]} series - user supplied series values
* @param {number} seriesIndex - the index by which series will be drawn on the svg
* @return {Element} element which is supplied to parent chart draw method for appending
**/
draw(series, seriesIndex) {
var _a, _b, _c;
const w2 = this.w;
const graphics = new Graphics(this.w);
const coreUtils = new CoreUtils(this.w);
series = coreUtils.getLogSeries(series);
this.series = series;
this.yRatio = coreUtils.getLogYRatios(this.yRatio);
this.barHelpers.initVariables(series);
const ret = graphics.group({
class: "apexcharts-bar-series apexcharts-plot-series"
});
if (w2.config.dataLabels.enabled) {
if (this.totalItems > this.barOptions.dataLabels.maxItems) {
console.warn(
"WARNING: DataLabels are enabled but there are too many to display. This may cause performance issue when rendering - ApexCharts"
);
}
}
for (let i = 0, bc = 0; i < series.length; i++, bc++) {
let x, y;
const yArrj = [];
const xArrj = [];
const realIndex = w2.globals.comboCharts ? (
/** @type {any} */
seriesIndex[i]
) : i;
const { columnGroupIndex } = this.barHelpers.getGroupIndex(realIndex);
const elSeries = graphics.group({
class: `apexcharts-series`,
rel: i + 1,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[realIndex]),
"data:realIndex": realIndex
});
Series.addCollapsedClassToSeries(this.w, elSeries, realIndex);
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1;
}
if (this.yRatio.length > 1) {
this.yaxisIndex = w2.globals.seriesYAxisReverseMap[realIndex];
this.translationsIndex = realIndex;
}
const translationsIndex = this.translationsIndex;
this.isReversed = w2.config.yaxis[this.yaxisIndex] && w2.config.yaxis[this.yaxisIndex].reversed;
if (this.isPyramid) {
this.pyramidLayout = this.barHelpers.computePyramidLayout(series[i]);
}
const initPositions = this.barHelpers.initialPositions(realIndex);
const {
y: initY,
yDivision,
// yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroW,
// zeroW is the baseline where 0 meets x axis
x: initX,
xDivision,
// xDivision is the GRIDWIDTH divided by number of datapoints (columns)
zeroH
// zeroH is the baseline where 0 meets y axis
} = initPositions;
let barHeight = initPositions.barHeight;
let barWidth = initPositions.barWidth;
y = initY;
x = initX;
if (!this.isHorizontal) {
xArrj.push(x + (barWidth != null ? barWidth : 0) / 2);
}
const elDataLabelsWrap = graphics.group({
class: "apexcharts-datalabels",
"data:realIndex": realIndex
});
w2.globals.delayedElements.push({
el: elDataLabelsWrap.node,
// On a layout-changing update the labels must stay hidden through the
// reflow morph (the updateOptions flow otherwise reveals them at
// frame 0, where they float over sliding bars). When dataLabels.animate
// is on the labels instead RIDE the morph (see DataLabelTransition), so
// keep them visible: holding would hide the very motion we want to show.
holdUntilComplete: !((_a = w2.config.dataLabels.animate) == null ? void 0 : _a.enabled) && this.isLengthTransition(realIndex)
});
elDataLabelsWrap.node.classList.add("apexcharts-element-hidden");
const elGoalsMarkers = graphics.group({
class: "apexcharts-bar-goals-markers"
});
const elBarShadows = graphics.group({
class: "apexcharts-bar-shadows"
});
w2.globals.delayedElements.push({
el: elBarShadows.node
});
elBarShadows.node.classList.add("apexcharts-element-hidden");
for (let j = 0; j < series[i].length; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex);
let paths = (
/** @type {any} */
null
);
const pathsParams = {
indexes: {
i,
j,
realIndex,
translationsIndex,
bc
},
x,
y,
strokeWidth,
elSeries
};
if (this.isHorizontal) {
paths = this.drawBarPaths(__spreadProps(__spreadValues({}, pathsParams), {
barHeight,
zeroW,
yDivision
}));
barWidth = this.series[i][j] / this.invertedYRatio;
} else {
paths = this.drawColumnPaths(__spreadProps(__spreadValues({}, pathsParams), {
xDivision,
barWidth,
zeroH
}));
barHeight = this.series[i][j] / this.yRatio[translationsIndex];
}
const pathFill = this.barHelpers.getPathFillColor(
series,
i,
j,
realIndex
);
if (this.isFunnel && !this.isPyramid && this.barOptions.isFunnel3d && ((_b = w2.config.plotOptions.funnel) == null ? void 0 : _b.shape) !== "trapezoid" && this.pathArr.length && j > 0) {
const barShadow = this.barHelpers.drawBarShadow({
color: typeof pathFill.color === "string" && ((_c = pathFill.color) == null ? void 0 : _c.indexOf("url")) === -1 ? pathFill.color : Utils.hexToRgba(w2.globals.colors[i]),
prevPaths: this.pathArr[this.pathArr.length - 1],
currPaths: paths,
realIndex,
j
});
elBarShadows.add(barShadow);
if (w2.config.chart.dropShadow.enabled) {
const filters = new Filters(this.w);
filters.dropShadow(barShadow, w2.config.chart.dropShadow, realIndex);
}
}
this.pathArr.push(paths);
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition: paths.barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth
});
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine);
}
y = paths.y;
x = paths.x;
if (j > 0) {
xArrj.push(x + (barWidth != null ? barWidth : 0) / 2);
}
yArrj.push(y);
this.renderSeries(__spreadProps(__spreadValues({
realIndex,
pathFill: pathFill.color
}, pathFill.useRangeColor ? { lineFill: pathFill.color } : {}), {
j,
i,
columnGroupIndex,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight: Math.abs(paths.barHeight ? paths.barHeight : barHeight),
barWidth: Math.abs(paths.barWidth ? paths.barWidth : barWidth),
elDataLabelsWrap,
elGoalsMarkers,
elBarShadows,
visibleSeries: this.visibleI,
type: "bar"
}));
}
w2.globals.seriesXvalues[realIndex] = xArrj;
w2.globals.seriesYvalues[realIndex] = yArrj;
if (w2.globals.previousPaths.length > 0) {
const newKeys = [];
for (let j = 0; j < series[i].length; j++) {
newKeys.push(datumKey(w2, realIndex, j));
}
renderBarExitGhosts({
w: w2,
elSeries,
record: this._prevRecord(realIndex),
newKeys,
isHorizontal: this.isHorizontal,
speed: w2.config.chart.animations.dynamicAnimation.speed
});
}
ret.add(elSeries);
}
return ret;
}
/** @param {{ realIndex?: any, pathFill?: any, lineFill?: any, j?: any, i?: any, columnGroupIndex?: any, pathFrom?: any, pathTo?: any, strokeWidth?: any, elSeries?: any, x?: any, y?: any, y1?: any, y2?: any, series?: any, barHeight?: any, barWidth?: any, barXPosition?: any, barYPosition?: any, elDataLabelsWrap?: any, elGoalsMarkers?: any, elBarShadows?: any, visibleSeries?: any, type?: any, classes?: any }} opts */
renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
columnGroupIndex,
pathFrom,
pathTo,
strokeWidth,
elSeries,
x,
// x pos
y,
// y pos
y1,
// absolute value
y2,
// absolute value
series,
barHeight,
barWidth,
barXPosition,
barYPosition,
elDataLabelsWrap,
elGoalsMarkers,
elBarShadows,
visibleSeries,
type,
classes
}) {
var _a, _b, _c, _d, _e, _f, _g;
const w2 = this.w;
const graphics = new Graphics(this.w, this.ctx);
const emit = seriesEmitter(this.ctx, graphics);
let skipDrawing = false;
const pathToInterp = this._pathToInterp;
this._pathToInterp = null;
if (!elSeries._bindingsDelegated) {
elSeries._bindingsDelegated = true;
graphics.setupEventDelegation(elSeries, `.apexcharts-${type}-area`);
}
if (!lineFill) {
let fetchColor = function(i2) {
const exp = w2.config.stroke.colors;
let c;
if (Array.isArray(exp) && exp.length > 0) {
c = exp[i2];
if (!c) c = "";
if (typeof c === "function") {
return c({
value: w2.seriesData.series[i2][j],
dataPointIndex: j,
w: w2
});
}
}
return c;
};
const checkAvailableColor = typeof w2.globals.stroke.colors[realIndex] === "function" ? fetchColor(realIndex) : w2.globals.stroke.colors[realIndex];
lineFill = this.barOptions.distributed ? w2.globals.stroke.colors[j] : checkAvailableColor;
}
const animCfg = w2.config.chart.animations;
const gradCfg = animCfg.animateGradually;
const staggerEnabled = gradCfg && gradCfg.enabled !== false && !(w2.globals.dataChanged && this.isLayoutShift(realIndex));
let delay = 0;
let delayMs = 0;
if (staggerEnabled) {
const totalBars = w2.globals.dataPoints || 1;
const configStep = gradCfg.delay || 0;
const baseDelayMs = Math.min(
configStep,
animCfg.speed * 0.5 / Math.max(1, totalBars)
);
delayMs = computeStagger({
style: "sequential",
index: j,
baseDelay: baseDelayMs
});
if (w2.config.chart.stacked && !w2.globals.dataChanged) {
delayMs += i * baseDelayMs * 0.5;
}
const delayFactor = configStep || 1;
delay = delayMs / delayFactor;
}
const barDataLabels = new BarDataLabels(this);
const dataLabelsObj = (
/** @type {any} */
barDataLabels.handleBarDataLabels({
x,
y,
y1,
y2,
i,
j,
series,
realIndex,
columnGroupIndex,
barHeight,
barWidth,
barXPosition,
barYPosition,
visibleSeries
})
);
if (delayMs > 0) {
const dlAnimCfg = w2.config.dataLabels;
if (((_a = dlAnimCfg.animate) == null ? void 0 : _a.enabled) || ((_b = dlAnimCfg.countUp) == null ? void 0 : _b.enabled)) {
const stampDelay = String(Math.round(delayMs));
(_d = (_c = dataLabelsObj.dataLabels) == null ? void 0 : _c.node) == null ? void 0 : _d.setAttribute("data:dlDelay", stampDelay);
(_f = (_e = dataLabelsObj.totalDataLabels) == null ? void 0 : _e.node) == null ? void 0 : _f.setAttribute(
"data:dlDelay",
stampDelay
);
}
}
if (!w2.globals.isBarHorizontal) {
if (dataLabelsObj.dataLabelsPos.dataLabelsX + Math.max(barWidth, w2.globals.barPadForNumericAxis) < 0 || dataLabelsObj.dataLabelsPos.dataLabelsX - Math.max(barWidth, w2.globals.barPadForNumericAxis) > w2.layout.gridWidth) {
skipDrawing = true;
}
}
if (
/** @type {Record<string,any>} */
w2.config.series[i].data[j] && /** @type {Record<string,any>} */
w2.config.series[i].data[j].strokeColor
) {
lineFill = /** @type {Record<string,any>} */
w2.config.series[i].data[j].strokeColor;
}
if (this.isNullValue && w2.globals.collapsingSeriesIndices.indexOf(realIndex) === -1) {
pathFill = "none";
}
if (!skipDrawing) {
const morphActive = ((_g = this.ctx.morphTypeChange) == null ? void 0 : _g.isActive()) === true;
const dataChangeSpeed = morphActive ? this.ctx.morphTypeChange.getSpeed() : w2.config.chart.animations.dynamicAnimation.speed;
const pieceClaimed = morphActive && this.ctx.morphTypeChange.claimsTargetMark(realIndex, j);
if (pieceClaimed) {
pathFrom = pathTo;
delay = 0;
}
const renderedPath = (
/** @type {any} */
emit.renderPaths({
i,
j,
realIndex,
pathFrom,
pathTo,
stroke: lineFill,
strokeWidth,
strokeLineCap: w2.config.stroke.lineCap,
fill: pathFill,
pathToInterp,
animationDelay: delay,
initialSpeed: w2.config.chart.animations.speed,
dataChangeSpeed,
// `classes` is optional: boxPlot, violin and candlestick call
// renderSeries without it, and interpolating it unguarded stamped a
// literal "undefined" into every one of their marks' class lists.
className: `apexcharts-${type}-area${classes ? ` ${classes}` : ""}`,
chartType: type,
bindEventsOnPaths: false
})
);
renderedPath.attr("clip-path", `url(#gridRectBarMask${w2.globals.cuid})`);
if (pieceClaimed) {
renderedPath.node.setAttribute("opacity", "0");
renderedPath.node.setAttribute("data-piece-hidden", "1");
}
const forecast = w2.config.forecastDataPoints;
if (forecast.count > 0) {
if (j >= w2.globals.dataPoints - forecast.count) {
renderedPath.node.setAttribute("stroke-dasharray", forecast.dashArray);
renderedPath.node.setAttribute("stroke-width", forecast.strokeWidth);
renderedPath.node.setAttribute("fill-opacity", forecast.fillOpacity);
}
}
if (typeof y1 !== "undefined" && typeof y2 !== "undefined") {
renderedPath.attr("data-range-y1", y1);
renderedPath.attr("data-range-y2", y2);
}
const filters = new Filters(this.w);
filters.setSelectionFilter(renderedPath, realIndex, j);
elSeries.add(renderedPath);
renderedPath.attr({
cy: dataLabelsObj.dataLabelsPos.bcy,
cx: dataLabelsObj.dataLabelsPos.bcx,
j,
val: w2.seriesData.series[i][j],
barHeight,
barWidth,
// Datum identity for the next update's keyed join (see
// LengthTransition): survivors match by key, not array position.
"data:pathKey": datumKey(w2, realIndex, j)
});
if (emit.kind === "canvas") {
if (!w2.globals.barCanvasCoords) w2.globals.barCanvasCoords = {};
if (!w2.globals.barCanvasCoords[realIndex]) {
w2.globals.barCanvasCoords[realIndex] = {};
}
w2.globals.barCanvasCoords[realIndex][j] = {
cx: dataLabelsObj.dataLabelsPos.bcx,
cy: dataLabelsObj.dataLabelsPos.bcy,
barWidth
};
}
if (dataLabelsObj.dataLabels !== null) {
elDataLabelsWrap.add(dataLabelsObj.dataLabels);
}
if (dataLabelsObj.totalDataLabels) {
elDataLabelsWrap.add(dataLabelsObj.totalDataLabels);
}
elSeries.add(elDataLabelsWrap);
if (elGoalsMarkers) {
elSeries.add(elGoalsMarkers);
}
if (elBarShadows) {
elSeries.add(elBarShadows);
}
}
return elSeries;
}
/** @param {{indexes: any, barHeight: any, strokeWidth: any, zeroW: any, x: any, y: any, yDivision: any, elSeries: any}} opts */
drawBarPaths({
indexes,
barHeight,
strokeWidth,
zeroW,
x,
y,
yDivision,
elSeries
}) {
const w2 = this.w;
const i = indexes.i;
const j = indexes.j;
let barYPosition;
if (w2.axisFlags.isXNumeric) {
y = (w2.seriesData.seriesX[i][j] - w2.globals.minX) / this.invertedXRatio - barHeight;
barYPosition = y + barHeight * this.visibleI;
} else {
if (w2.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
const { nonZeroColumns, zeroEncounters } = this.barHelpers.getZeroValueEncounters({ i, j });
if (nonZeroColumns > 0) {
barHeight = this.seriesLen * barHeight / nonZeroColumns;
}
barYPosition = y + barHeight * this.visibleI;
barYPosition -= barHeight * zeroEncounters;
} else {
barYPosition = y + barHeight * this.visibleI;
}
}
const useTrapezoid = this.isFunnel && w2.config.plotOptions.funnel.shape === "trapezoid";
const pyramidSeg = this.isPyramid && this.pyramidLayout ? this.pyramidLayout[j] : null;
const usePyramid = !!pyramidSeg;
if (pyramidSeg) {
barYPosition = pyramidSeg.y;
barHeight = pyramidSeg.height;
} else if (this.isFunnel && !useTrapezoid) {
const _zeroW = zeroW != null ? zeroW : 0;
zeroW = _zeroW - /** @type {number} */
/** @type {any} */
(this.barHelpers.getXForValue(
/** @type {any} */
this.series[i][j],
_zeroW
) - _zeroW) / 2;
}
x = this.barHelpers.getXForValue(
/** @type {any} */
this.series[i][j],
zeroW != null ? zeroW : 0
);
let paths;
if (pyramidSeg) {
paths = /** @type {any} */
this.barHelpers.getPyramidPaths({
barYPosition,
barHeight,
topHalf: pyramidSeg.topHalf,
bottomHalf: pyramidSeg.bottomHalf,
realIndex: indexes.realIndex,
j,
strokeWidth,
w: w2
});
} else if (useTrapezoid) {
paths = /** @type {any} */
this.barHelpers.getFunnelTrapezoidPaths({
barYPosition,
barHeight,
series: (
/** @type {any} */
this.series
),
i,
j,
realIndex: indexes.realIndex,
strokeWidth,
w: w2
});
} else {
paths = /** @type {any} */
this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1: zeroW,
x2: x,
strokeWidth,
isReversed: this.isReversed,
series: this.series,
realIndex: indexes.realIndex,
i,
j,
w: w2
});
}
if (useTrapezoid || usePyramid) {
zeroW = paths.x1;
x = paths.x;
}
if (!w2.axisFlags.isXNumeric && !usePyramid) {
y = y + yDivision;
}
if (usePyramid) {
y = barYPosition;
}
this.barHelpers.barBackground({
j,
i,
y1: barYPosition - barHeight * this.visibleI,
y2: barHeight * this.seriesLen,
elSeries
});
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x1: zeroW,
x,
y,
goalX: this.barHelpers.getGoalValues(
"x",
zeroW,
/** @type {any} */
null,
indexes.realIndex,
j,
0
),
barYPosition,
barHeight
};
}
/** @param {{indexes: any, x: any, y: any, xDivision: any, barWidth: any, zeroH: any, strokeWidth: any, elSeries: any}} opts */
drawColumnPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
strokeWidth,
elSeries
}) {
const w2 = this.w;
const realIndex = indexes.realIndex;
const translationsIndex = indexes.translationsIndex;
const i = indexes.i;
const j = indexes.j;
const bc = indexes.bc;
let barXPosition;
if (w2.axisFlags.isXNumeric) {
const xForNumericX = this.getBarXForNumericXAxis({
x,
j,
realIndex,
barWidth
});
x = xForNumericX.x;
barXPosition = xForNumericX.barXPosition;
} else {
if (w2.config.plotOptions.bar.hideZeroBarsWhenGrouped) {
const { nonZeroColumns, zeroEncounters } = this.barHelpers.getZeroValueEncounters({ i, j });
if (nonZeroColumns > 0) {
barWidth = this.seriesLen * barWidth / nonZeroColumns;
}
barXPosition = x + barWidth * this.visibleI;
barXPosition -= barWidth * zeroEncounters;
} else {
barXPosition = x + barWidth * this.visibleI;
}
}
y = this.barHelpers.getYForValue(
/** @type {any} */
this.series[i][j],
zeroH,
translationsIndex
);
const paths = (
/** @type {any} */
this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1: zeroH,
y2: y,
strokeWidth,
isReversed: this.isReversed,
series: this.series,
realIndex,
i,
j,
w: w2
})
);
if (!w2.axisFlags.isXNumeric) {
x = x + xDivision;
}
this.barHelpers.barBackground({
bc,
j,
i,
x1: barXPosition - strokeWidth / 2 - barWidth * this.visibleI,
x2: barWidth * this.seriesLen + strokeWidth / 2,
elSeries
});
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x,
y,
goalY: this.barHelpers.getGoalValues(
"y",
/** @type {any} */
null,
zeroH,
realIndex,
j,
translationsIndex
),
barXPosition,
barWidth
};
}
/** @param {{x: any, barWidth: any, realIndex: any, j: any}} opts */
getBarXForNumericXAxis({ x, barWidth, realIndex, j }) {
const w2 = this.w;
let sxI = realIndex;
if (!w2.seriesData.seriesX[realIndex].length) {
sxI = w2.globals.maxValsInArrayIndex;
}
if (Utils.isNumber(w2.seriesData.seriesX[sxI][j])) {
x = AxisMapping.dataXToPx(w2, w2.seriesData.seriesX[sxI][j]) - barWidth * this.seriesLen / 2;
}
return {
barXPosition: x + (isHistogramOverlay(w2) ? 0 : barWidth * this.visibleI),
x
};
}
/**
* The captured previous-render record for a series (last match wins, same
* as the historical scan order).
*
* @param {number} realIndex
* @returns {any | null}
*/
_prevRecord(realIndex) {
const w2 = this.w;
let record = null;
for (let pp = 0; pp < w2.globals.previousPaths.length; pp++) {
const gpp = w2.globals.previousPaths[pp];
if (gpp.paths && gpp.paths.length > 0 && parseInt(gpp.realIndex, 10) === parseInt(String(realIndex), 10)) {
record = gpp;
}
}
return record;
}
/**
* Previous paths of a series re-keyed by datum key (stamped as
* `data:pathKey` on each bar path and captured by Series.getPreviousPaths).
* Returns null when the previous render carries no keys (so the caller
* falls back to positional matching).
*
* @param {number} realIndex
* @returns {Map<string, {d: string}> | null}
*/
_prevKeyedPaths(realIndex) {
if (!this._prevKeyed) this._prevKeyed = {};
if (this._prevKeyed[realIndex] !== void 0) {
return this._prevKeyed[realIndex];
}
const record = this._prevRecord(realIndex);
let map = null;
if (record && record.paths.every((p2) => p2.key != null)) {
const keyed = /* @__PURE__ */ new Map();
record.paths.forEach((p2) => {
keyed.set(p2.key, p2);
});
map = keyed;
}
this._prevKeyed[realIndex] = map;
return map;
}
/**
* Whether this series' update changes its datum layout (points entered,
* exited, or changed identity). Layout-changing updates run all survivors
* on one shared clock (no per-bar stagger) so the reflow reads as a single
* coordinated motion; pure value updates keep the stagger.
*
* @param {number} realIndex
* @returns {boolean}
*/
isLengthTransition(realIndex) {
var _a, _b;
if (!this._ltCache) this._ltCache = {};
if (this._ltCache[realIndex] !== void 0) return this._ltCache[realIndex];
const w2 = this.w;
let result = false;
if (lengthTransitionEnabled(w2) && w2.globals.previousPaths.length > 0) {
const record = this._prevRecord(realIndex);
const dataLen = (_b = (_a = w2.seriesData.series[realIndex]) == null ? void 0 : _a.length) != null ? _b : 0;
if (!record) {
result = dataLen > 0;
} else if (record.paths.length !== dataLen) {
result = true;
} else {
const keyed = this._prevKeyedPaths(realIndex);
if (keyed) {
for (let j = 0; j < dataLen; j++) {
if (!keyed.has(datumKey(w2, realIndex, j))) {
result = true;
break;
}
}
}
}
}
this._ltCache[realIndex] = result;
return result;
}
/**
* Whether this update moves survivors to new slots: a length change
* (enter/exit, via isLengthTransition) OR a pure reorder (a "bar chart race"
* swap, same datum set in a new order). Used to drop the per-bar stagger so
* all bars slide on one shared clock, staying locked to the axis/data labels.
* Broader than isLengthTransition, which is deliberately enter/exit-only
* (exit ghosts / baseline enters must not fire on a plain reorder).
* @param {number} realIndex
*/
isLayoutShift(realIndex) {
if (this.isLengthTransition(realIndex)) return true;
if (!this._layoutShiftCache) this._layoutShiftCache = {};
if (this._layoutShiftCache[realIndex] !== void 0) {
return this._layoutShiftCache[realIndex];
}
const sj = seriesJoin(this.w, realIndex, true, true);
const result = !!(sj && sj.join.changed);
this._layoutShiftCache[realIndex] = result;
return result;
}
/**
* Resolve `pathFrom` for a bar on data update, joining old and new datums
* by KEY (category label / x value) so survivors keep their identity across
* inserts, prepends, and removals. Three outcomes:
*
* - survivor with stable shape → the previous `d` (smooth reflow morph);
* - survivor whose command count changed (corner state flipped, e.g. bar
* became new top-of-stack) → `pathTo` (pathFrom === pathTo, a snap);
* - genuinely new datum (key absent from the previous render, or the whole
* series is new) → null, telling the path builder to use its
* grow-from-baseline enter path.
*
* Falls back to positional (index j) matching when the previous render
* carries no datum keys.
*
* @param {number} realIndex - stable series index from `data:realIndex`
* @param {number} j - data-point index within the series
* @param {string} pathTo - the freshly-built path for this bar (post-roundPathCorners)
* @param {string} [squarePathTo] - the same bar before roundPathCorners, i.e.
* its new slot with square corners. Supplied by the stacked builders so a
* bar gaining a corner can travel square and round only on arrival.
* @returns {string | null}
**/
getPreviousPath(realIndex, j, pathTo, squarePathTo) {
const w2 = this.w;
this._pathToInterp = null;
const record = this._prevRecord(realIndex);
if (!record) {
return lengthTransitionEnabled(w2) ? null : pathTo;
}
let oldD = null;
let isNewDatum = false;
const keyed = this._prevKeyedPaths(realIndex);
if (keyed) {
const prev = keyed.get(datumKey(w2, realIndex, j));
if (prev) {
oldD = prev.d;
} else {
isNewDatum = true;
}
} else if (typeof record.paths[j] !== "undefined") {
oldD = record.paths[j].d;
} else {
isNewDatum = true;
}
if (oldD) {
const fromCount = Bar.pathCommandCount(oldD);
const toCount = Bar.pathCommandCount(pathTo);
if (fromCount === toCount) {
return oldD;
}
const graphics = new Graphics(w2);
const extentOf = (d) => {
const box = Bar.pathBox(d);
return box ? Math.min(box.maxX - box.minX, box.maxY - box.minY) : 0;
};
const handingOver = fromCount < toCount ? extentOf(oldD) > 1 : extentOf(pathTo) > 1;
if (fromCount < toCount) {
const padded = graphics.roundPathCorners(oldD, 0);
if (Bar.pathCommandCount(padded) === toCount) {
if (handingOver && squarePathTo) {
const squareTarget = graphics.roundPathCorners(squarePathTo, 0);
if (Bar.pathCommandCount(squareTarget) === toCount) {
this._pathToInterp = squareTarget;
}
}
return padded;
}
} else {
const padded = graphics.roundPathCorners(pathTo, 0);
if (Bar.pathCommandCount(padded) === fromCount) {
this._pathToInterp = padded;
if (handingOver) {
const square = Bar.squareLike(oldD);
const squareStart = square ? graphics.roundPathCorners(square, 0) : null;
if (squareStart && Bar.pathCommandCount(squareStart) === fromCount) {
return squareStart;
}
}
return oldD;
}
}
}
if (isNewDatum && lengthTransitionEnabled(w2)) {
return null;
}
return pathTo;
}
/**
* The axis-aligned box a bar path occupies, and the point it starts from.
*
* @param {string} d
* @returns {{minX: number, maxX: number, minY: number, maxY: number, start: [number, number], vertical: boolean} | null}
*/
static pathBox(d) {
if (!d) return null;
const pts = [];
const re = /([MLC])([^MLCZz]*)/g;
let m2;
while ((m2 = re.exec(d)) !== null) {
const nums = m2[2].trim().split(/[\s,]+/).map(Number);
if (nums.length < 2 || nums.some(isNaN)) continue;
pts.push([nums[nums.length - 2], nums[nums.length - 1]]);
}
if (pts.length < 3) return null;
const xs = pts.map((p2) => p2[0]);
const ys = pts.map((p2) => p2[1]);
return {
minX: Math.min(...xs),
maxX: Math.max(...xs),
minY: Math.min(...ys),
maxY: Math.max(...ys),
start: pts[0],
// Column bars run their first leg down a vertical edge, horizontal bars
// run it along a horizontal one. Rounding moves the start point ALONG
// that leg, so the axis it does not move on is the one it shares.
vertical: Math.abs(pts[1][0] - pts[0][0]) < Math.abs(pts[1][1] - pts[0][1])
};
}
/**
* Rebuild a bar path as the plain rectangle it was rounded from, same box,
* same corner order, no radius.
*
* The corner the path starts at is the one nearest its start point, because
* rounding only ever slides that point a radius along the first leg. Knowing
* that corner and the winding is enough to re-emit the rect exactly as the
* builders in common/bar/Helpers do, so the result pairs command-for-command
* with anything built from them.
*
* @param {string} d
* @returns {string | null}
*/
static squareLike(d) {
const box = Bar.pathBox(d);
if (!box) return null;
const { minX, maxX, minY, maxY, start, vertical } = box;
const near = (v2, a, b2) => Math.abs(v2 - a) <= Math.abs(v2 - b2) ? [a, b2] : [b2, a];
let x1, x2, y1, y2;
if (vertical) {
x1 = Math.abs(start[0] - minX) <= Math.abs(start[0] - maxX) ? minX : maxX;
x2 = x1 === minX ? maxX : minX;
[y1, y2] = near(start[1], minY, maxY);
} else {
y1 = Math.abs(start[1] - minY) <= Math.abs(start[1] - maxY) ? minY : maxY;
y2 = y1 === minY ? maxY : minY;
[x1, x2] = near(start[0], minX, maxX);
}
const closing = d.trim().endsWith("Z") ? " Z" : " z";
return vertical ? `M ${x1} ${y1} L ${x1} ${y2} L ${x2} ${y2} L ${x2} ${y1}${closing}` : `M ${x1} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x1} ${y2}${closing}`;
}
/**
* Was this bar MIRRORED in the previous render? Stacked bars carry only
* top-rounded geometry; a bottom radius is produced by the apexcharts-flip-y
* (or -x, horizontal) class, so the mirror is the only record of where the
* radius visually sat. Matched by datum key like getPreviousPath, falling
* back to position when the previous render carries no keys.
*
* @param {number} realIndex
* @param {number} j
* @returns {boolean | null} null when there is no previous record to consult
*/
getPreviousFlip(realIndex, j) {
const w2 = this.w;
const record = this._prevRecord(realIndex);
if (!record) return null;
const keyed = this._prevKeyedPaths(realIndex);
const prev = keyed ? keyed.get(datumKey(w2, realIndex, j)) : record.paths[j];
return prev ? !!prev.flip : null;
}
/**
* Count SVG path commands (M, L, C, Q, Z, etc.). Used to detect whether
* two paths can be morphed safely — SVG.js requires matching command counts.
*
* @param {string} d
* @returns {number}
*/
static pathCommandCount(d) {
if (!d) return 0;
const matches = d.match(/[A-Za-z]/g);
return matches ? matches.length : 0;
}
}
class BarStacked extends Bar {
/**
* @param {any[]} series
* @param {number} seriesIndex
*/
draw(series, seriesIndex) {
const w2 = this.w;
this.graphics = new Graphics(this.w);
this.bar = new Bar(this.w, this.ctx, this.xyRatios);
const coreUtils = new CoreUtils(this.w);
series = coreUtils.getLogSeries(series);
this.yRatio = coreUtils.getLogYRatios(this.yRatio);
this.barHelpers.initVariables(series);
if (w2.config.chart.stackType === "100%") {
series = w2.globals.comboCharts ? (
/** @type {any} */
seriesIndex.map(
(_) => w2.globals.seriesPercent[_]
)
) : w2.globals.seriesPercent.slice();
}
this.series = series;
this.barHelpers.initializeStackedPrevVars(this);
const ret = this.graphics.group({
class: "apexcharts-bar-series apexcharts-plot-series"
});
let x = 0;
let y = 0;
const anim = w2.config.chart.animations;
const holdMirror = anim.enabled && anim.dynamicAnimation.enabled && w2.globals.previousPaths.length > 0;
let heldMirrors = false;
for (let i = 0, bc = 0; i < series.length; i++, bc++) {
const realIndex = w2.globals.comboCharts ? (
/** @type {any} */
seriesIndex[i]
) : i;
const { groupIndex, columnGroupIndex } = this.barHelpers.getGroupIndex(realIndex);
this.groupCtx = /** @type {any} */
this[
/** @type {any} */
w2.labelData.seriesGroups[groupIndex]
];
const xArrValues = [];
const yArrValues = [];
let translationsIndex = 0;
if (this.yRatio.length > 1) {
this.yaxisIndex = /** @type {any} */
w2.globals.seriesYAxisReverseMap[realIndex][0];
translationsIndex = realIndex;
}
this.isReversed = w2.config.yaxis[this.yaxisIndex] && w2.config.yaxis[this.yaxisIndex].reversed;
let elSeries = this.graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[realIndex]),
rel: i + 1,
"data:realIndex": realIndex
});
Series.addCollapsedClassToSeries(this.w, elSeries, realIndex);
const elDataLabelsWrap = this.graphics.group({
class: "apexcharts-datalabels",
"data:realIndex": realIndex
});
Series.addCollapsedClassToSeries(this.w, elDataLabelsWrap, realIndex);
if ((w2.globals.collapsingSeriesIndices || []).indexOf(realIndex) > -1) {
elDataLabelsWrap.node.style.setProperty(
"--apexcharts-dl-exit",
`${w2.config.chart.animations.dynamicAnimation.speed}ms`
);
}
const elGoalsMarkers = this.graphics.group({
class: "apexcharts-bar-goals-markers"
});
const initPositions = this.initialPositions(
x,
y,
void 0,
void 0,
void 0,
void 0,
translationsIndex
);
const {
xDivision,
// xDivision is the GRIDWIDTH divided by number of datapoints (columns)
yDivision,
// yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroH,
// zeroH is the baseline where 0 meets y axis
zeroW
// zeroW is the baseline where 0 meets x axis
} = initPositions;
let barHeight = initPositions.barHeight;
let barWidth = initPositions.barWidth;
y = initPositions.y;
x = initPositions.x;
w2.globals.barHeight = barHeight;
w2.globals.barWidth = barWidth;
this.barHelpers.initializeStackedXYVars(this);
if (this.groupCtx.prevY.length === 1 && /**
* @param {number} val
*/
this.groupCtx.prevY[0].every((val) => isNaN(val))) {
this.groupCtx.prevY[0] = this.groupCtx.prevY[0].map(() => zeroH);
this.groupCtx.prevYF[0] = this.groupCtx.prevYF[0].map(() => 0);
}
for (let j = 0; j < w2.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex);
const commonPathOpts = {
indexes: { i, j, realIndex, translationsIndex, bc },
strokeWidth,
x,
y,
elSeries,
columnGroupIndex,
seriesGroup: w2.labelData.seriesGroups[groupIndex]
};
let paths = (
/** @type {any} */
null
);
if (this.isHorizontal) {
paths = this.drawStackedBarPaths(__spreadProps(__spreadValues({}, commonPathOpts), {
zeroW,
barHeight,
yDivision
}));
barWidth = this.series[i][j] / this.invertedYRatio;
} else {
paths = this.drawStackedColumnPaths(__spreadProps(__spreadValues({}, commonPathOpts), {
xDivision,
barWidth,
zeroH
}));
barHeight = this.series[i][j] / this.yRatio[translationsIndex];
}
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition: paths.barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth
});
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine);
}
y = paths.y;
x = paths.x;
xArrValues.push(x);
yArrValues.push(y);
const pathFill = this.barHelpers.getPathFillColor(
series,
i,
j,
realIndex
);
let classes = "";
const flipClass = w2.globals.isBarHorizontal ? "apexcharts-flip-x" : "apexcharts-flip-y";
const wantsFlip = this.barHelpers.arrBorderRadius[realIndex][j] === "bottom" && w2.seriesData.series[realIndex][j] > 0 || this.barHelpers.arrBorderRadius[realIndex][j] === "top" && w2.seriesData.series[realIndex][j] < 0;
const heldFlip = holdMirror && !wantsFlip && this.getPreviousFlip(realIndex, j);
if (wantsFlip || heldFlip) {
classes = flipClass;
}
if (heldFlip) {
classes += " apexcharts-flip-held";
heldMirrors = true;
}
elSeries = this.renderSeries(__spreadProps(__spreadValues({
realIndex,
pathFill: pathFill.color
}, pathFill.useRangeColor ? { lineFill: pathFill.color } : {}), {
j,
i,
columnGroupIndex,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight,
barWidth,
elDataLabelsWrap,
elGoalsMarkers,
type: "bar",
visibleSeries: columnGroupIndex,
classes
}));
}
w2.globals.seriesXvalues[realIndex] = xArrValues;
w2.globals.seriesYvalues[realIndex] = yArrValues;
this.groupCtx.prevY.push(this.groupCtx.yArrj);
this.groupCtx.prevYF.push(this.groupCtx.yArrjF);
this.groupCtx.prevYVal.push(this.groupCtx.yArrjVal);
this.groupCtx.prevX.push(this.groupCtx.xArrj);
this.groupCtx.prevXF.push(this.groupCtx.xArrjF);
this.groupCtx.prevXVal.push(this.groupCtx.xArrjVal);
ret.add(elSeries);
}
if (heldMirrors) this.settleHeldMirrors();
return ret;
}
/**
* Drop the mirrors held across an animated update once the geometry they
* were covering for has arrived. A no-op for every state except
* 'bottom' → 'top', where the endpoint really is rounded at the other end;
* everywhere else the mirror is an exact identity on the settled shape, so
* removing it changes nothing on screen.
*/
settleHeldMirrors() {
const w2 = this.w;
if (!Environment.isBrowser()) return;
const anim = w2.config.chart.animations;
const hold = (anim.dynamicAnimation.speed || 0) + (anim.speed || 0) + 100;
setTimeout(() => {
if (w2.globals.isDestroyed || !Utils.elementExists(w2.dom.baseEl)) return;
w2.dom.baseEl.querySelectorAll(".apexcharts-flip-held").forEach((el) => {
el.classList.remove(
"apexcharts-flip-held",
"apexcharts-flip-y",
"apexcharts-flip-x"
);
});
}, hold);
}
/**
* @param {number} x
* @param {number} y
* @param {number | undefined} xDivision
* @param {number | undefined} yDivision
* @param {number | undefined} zeroH
* @param {number | undefined} zeroW
* @param {number} translationsIndex
*/
initialPositions(x, y, xDivision, yDivision, zeroH, zeroW, translationsIndex) {
const w2 = this.w;
let barHeight, barWidth;
if (this.isHorizontal) {
yDivision = w2.layout.gridHeight / w2.globals.dataPoints;
const userBarHeight = w2.config.plotOptions.bar.barHeight;
if (String(userBarHeight).indexOf("%") === -1) {
barHeight = parseInt(userBarHeight, 10);
} else {
barHeight = yDivision * parseInt(userBarHeight, 10) / 100;
}
zeroW = w2.globals.padHorizontal + (this.isReversed ? w2.layout.gridWidth - this.baseLineInvertedY : this.baseLineInvertedY);
y = (yDivision - barHeight) / 2;
} else {
xDivision = w2.layout.gridWidth / w2.globals.dataPoints;
barWidth = xDivision;
const userColumnWidth = w2.config.plotOptions.bar.columnWidth;
if (w2.axisFlags.isXNumeric && w2.globals.dataPoints > 1) {
xDivision = w2.globals.minXDiff / this.xRatio;
barWidth = xDivision * parseInt(this.barOptions.columnWidth, 10) / 100;
} else if (String(userColumnWidth).indexOf("%") === -1) {
barWidth = parseInt(userColumnWidth, 10);
} else {
barWidth *= parseInt(userColumnWidth, 10) / 100;
}
if (this.isReversed) {
zeroH = this.baseLineY[translationsIndex];
} else {
zeroH = w2.layout.gridHeight - this.baseLineY[translationsIndex];
}
x = w2.globals.padHorizontal + (xDivision - barWidth) / 2;
}
const subDivisions = w2.globals.barGroups.length || 1;
return {
x,
y,
yDivision,
xDivision,
barHeight: (barHeight != null ? barHeight : 0) / subDivisions,
barWidth: (barWidth != null ? barWidth : 0) / subDivisions,
zeroH,
zeroW
};
}
/** @param {{indexes: any, barHeight: any, strokeWidth: any, zeroW: any, x: any, y: any, columnGroupIndex: any, seriesGroup: any, yDivision: any, elSeries: any}} opts */
drawStackedBarPaths({
indexes,
barHeight,
strokeWidth,
zeroW,
x,
y,
columnGroupIndex,
seriesGroup,
yDivision,
elSeries
}) {
var _a, _b, _c, _d, _e;
const w2 = this.w;
const barYPosition = y + columnGroupIndex * barHeight;
let barXPosition;
const i = indexes.i;
const j = indexes.j;
const realIndex = indexes.realIndex;
const translationsIndex = indexes.translationsIndex;
let prevBarW = 0;
for (let k = 0; k < this.groupCtx.prevXF.length; k++) {
prevBarW = prevBarW + this.groupCtx.prevXF[k][j];
}
let gsi = i;
if (
/** @type {Record<string,any>} */
w2.config.series[realIndex].name
) {
gsi = seriesGroup.indexOf(
/** @type {Record<string,any>} */
w2.config.series[realIndex].name
);
}
if (gsi > 0) {
let bXP = zeroW;
if (this.groupCtx.prevXVal[gsi - 1][j] < 0) {
bXP = /** @type {any} */
((_a = this.series[i]) == null ? void 0 : _a[j]) >= 0 ? this.groupCtx.prevX[gsi - 1][j] + prevBarW - (this.isReversed ? prevBarW : 0) * 2 : this.groupCtx.prevX[gsi - 1][j];
} else if (this.groupCtx.prevXVal[gsi - 1][j] >= 0) {
bXP = /** @type {any} */
((_b = this.series[i]) == null ? void 0 : _b[j]) >= 0 ? this.groupCtx.prevX[gsi - 1][j] : this.groupCtx.prevX[gsi - 1][j] - prevBarW + (this.isReversed ? prevBarW : 0) * 2;
}
barXPosition = bXP;
} else {
barXPosition = zeroW;
}
if (
/** @type {any} */
((_c = this.series[i]) == null ? void 0 : _c[j]) === null
) {
x = barXPosition;
} else {
x = barXPosition + /** @type {any} */
((_d = this.series[i]) == null ? void 0 : _d[j]) / this.invertedYRatio - (this.isReversed ? (
/** @type {any} */
((_e = this.series[i]) == null ? void 0 : _e[j]) / this.invertedYRatio
) : 0) * 2;
}
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1: barXPosition,
x2: x,
strokeWidth,
isReversed: this.isReversed,
series: this.series,
realIndex: indexes.realIndex,
seriesGroup,
i,
j,
w: w2
});
this.barHelpers.barBackground({
j,
i,
y1: barYPosition,
y2: barHeight,
elSeries
});
y = y + yDivision;
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
goalX: this.barHelpers.getGoalValues(
"x",
zeroW,
/** @type {any} */
null,
realIndex,
j,
translationsIndex
),
barXPosition,
barYPosition,
x,
y
};
}
/** @param {{indexes: any, x: any, y: any, xDivision: any, barWidth: any, zeroH: any, columnGroupIndex: any, seriesGroup: any, elSeries: any}} opts */
drawStackedColumnPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
columnGroupIndex,
seriesGroup,
elSeries
}) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
const w2 = this.w;
const i = indexes.i;
const j = indexes.j;
const bc = indexes.bc;
const realIndex = indexes.realIndex;
const translationsIndex = indexes.translationsIndex;
if (w2.axisFlags.isXNumeric) {
let seriesVal = w2.seriesData.seriesX[realIndex][j];
if (!seriesVal) seriesVal = 0;
x = (seriesVal - w2.globals.minX) / this.xRatio - barWidth / 2 * w2.globals.barGroups.length;
}
const barXPosition = x + columnGroupIndex * barWidth;
let barYPosition;
let prevBarH = 0;
for (let k = 0; k < this.groupCtx.prevYF.length; k++) {
prevBarH = prevBarH + (!isNaN(this.groupCtx.prevYF[k][j]) ? this.groupCtx.prevYF[k][j] : 0);
}
let gsi = i;
if (seriesGroup) {
gsi = seriesGroup.indexOf(w2.seriesData.seriesNames[realIndex]);
}
if (gsi > 0 && !w2.axisFlags.isXNumeric || gsi > 0 && w2.axisFlags.isXNumeric && w2.seriesData.seriesX[realIndex - 1][j] === w2.seriesData.seriesX[realIndex][j]) {
let bYP;
let prevYValue;
const p2 = Math.min(this.yRatio.length + 1, realIndex + 1);
if (this.groupCtx.prevY[gsi - 1] !== void 0 && this.groupCtx.prevY[gsi - 1].length) {
for (let ii = 1; ii < p2; ii++) {
if (!isNaN((_a = this.groupCtx.prevY[gsi - ii]) == null ? void 0 : _a[j])) {
prevYValue = this.groupCtx.prevY[gsi - ii][j];
break;
}
}
}
for (let ii = 1; ii < p2; ii++) {
if (((_b = this.groupCtx.prevYVal[gsi - ii]) == null ? void 0 : _b[j]) < 0) {
bYP = /** @type {any} */
((_c = this.series[i]) == null ? void 0 : _c[j]) >= 0 ? prevYValue - prevBarH + (this.isReversed ? prevBarH : 0) * 2 : prevYValue;
break;
} else if (((_d = this.groupCtx.prevYVal[gsi - ii]) == null ? void 0 : _d[j]) >= 0) {
bYP = /** @type {any} */
((_e = this.series[i]) == null ? void 0 : _e[j]) >= 0 ? prevYValue : prevYValue + prevBarH - (this.isReversed ? prevBarH : 0) * 2;
break;
}
}
if (typeof bYP === "undefined") bYP = w2.layout.gridHeight;
if (
/**
* @param {number} val
*/
((_f = this.groupCtx.prevYF[0]) == null ? void 0 : _f.every((val) => val === 0)) && this.groupCtx.prevYF.slice(1, gsi).every(
(arr) => arr.every((val) => isNaN(val))
)
) {
barYPosition = zeroH;
} else {
barYPosition = bYP;
}
} else {
barYPosition = zeroH;
}
if (
/** @type {any} */
(_g = this.series[i]) == null ? void 0 : _g[j]
) {
y = barYPosition - /** @type {any} */
((_h = this.series[i]) == null ? void 0 : _h[j]) / this.yRatio[translationsIndex] + (this.isReversed ? (
/** @type {any} */
((_i = this.series[i]) == null ? void 0 : _i[j]) / this.yRatio[translationsIndex]
) : 0) * 2;
} else {
y = barYPosition;
}
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1: barYPosition,
y2: y,
yRatio: this.yRatio[translationsIndex],
strokeWidth: this.strokeWidth,
isReversed: this.isReversed,
series: this.series,
seriesGroup,
realIndex: indexes.realIndex,
i,
j,
w: w2
});
this.barHelpers.barBackground({
bc,
j,
i,
x1: barXPosition,
x2: barWidth,
elSeries
});
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
goalY: this.barHelpers.getGoalValues(
"y",
/** @type {any} */
null,
zeroH,
realIndex,
j,
0
),
barXPosition,
x: w2.axisFlags.isXNumeric ? x : x + xDivision,
y
};
}
}
function buildJitterGroups({
w: w2,
points,
seedA,
seedB,
center,
halfExtent,
alongFn,
isHorizontal,
options,
clampAt
}) {
const opts = options;
if (!opts || opts.show === false) return [];
if (!points || !points.length) return [];
const maxPoints = opts.maxPoints || 3e3;
const stride = points.length > maxPoints ? Math.ceil(points.length / maxPoints) : 1;
const r = opts.size != null ? opts.size : 2.5;
const jitterFrac = opts.jitter != null ? opts.jitter : 0.5;
const jitterPx = halfExtent * jitterFrac;
const constrain = opts.constrainToViolin !== false && typeof clampAt === "function";
const isSquare = opts.shape === "square";
const scale = opts.colorScale;
const useScale = scale && Array.isArray(scale.colors) && scale.colors.length > 0;
const steps = useScale ? Math.max(2, scale.steps || 24) : 1;
const sMin = useScale && scale.min != null ? scale.min : w2.globals.minY;
const sMax = useScale && scale.max != null ? scale.max : w2.globals.maxY;
const span = sMax - sMin || 1;
const buckets = useScale ? new Array(steps).fill("") : [""];
for (let k = 0; k < points.length; k += stride) {
const v2 = points[k];
const a = alongFn(v2);
let off = (hash01(seedA * 7919 + seedB * 100003 + k) - 0.5) * 2 * jitterPx;
if (constrain) {
const cap = (
/** @type {(v:number)=>number} */
clampAt(v2)
);
if (off > cap) off = cap;
if (off < -cap) off = -cap;
}
const px = isHorizontal ? a : center + off;
const py = isHorizontal ? center + off : a;
const sub = isSquare ? squareSubPath(px, py, r) : circleSubPath(px, py, r);
if (useScale) {
let t = (v2 - sMin) / span;
if (t < 0) t = 0;
if (t > 1) t = 1;
buckets[Math.round(t * (steps - 1))] += sub;
} else {
buckets[0] += sub;
}
}
if (!useScale) {
return buckets[0] ? [{ fill: null, d: buckets[0] }] : [];
}
const groups = [];
for (let b2 = 0; b2 < steps; b2++) {
if (!buckets[b2]) continue;
groups.push({
fill: rampColorAt(scale.colors, b2 / (steps - 1)),
d: buckets[b2]
});
}
return groups;
}
function renderJitter({
graphics,
w: w2,
elSeries,
pointsByCat,
options,
distributed,
realIndex,
wrapClass,
pointClass
}) {
if (!options || options.show === false || !pointsByCat.length) return;
const pOpacity = options.opacity != null ? options.opacity : 0.9;
const strokeColor = options.strokeColor != null ? options.strokeColor : "#fff";
const strokeW = options.strokeWidth != null ? options.strokeWidth : 1;
const willAnimateIn = w2.config.chart.animations.enabled && !w2.globals.resized && !w2.globals.dataChanged;
const elPointsWrap = graphics.group({
class: willAnimateIn ? `${wrapClass} apexcharts-element-hidden` : wrapClass
});
if (willAnimateIn) {
w2.globals.delayedElements.push({ el: elPointsWrap.node });
}
pointsByCat.forEach(({ groups, j }) => {
const catColor = distributed ? w2.globals.colors[j] : w2.globals.colors[realIndex];
const fc = options.fillColor;
const defaultFill = fc === "series" ? catColor : fc === "series-dark" ? darkenColor(catColor, 0.45) : fc || darkenColor(catColor, 0.45);
groups.forEach((g2) => {
const elPoints = graphics.drawPath({
d: g2.d,
fill: g2.fill != null ? g2.fill : defaultFill,
stroke: strokeW > 0 ? strokeColor : "none",
strokeWidth: strokeW,
fillOpacity: pOpacity,
classes: pointClass
});
elPoints.attr("data:realIndex", realIndex);
elPoints.attr("j", j);
elPoints.attr("clip-path", `url(#gridRectBarMask${w2.globals.cuid})`);
elPoints.node.style.pointerEvents = "none";
elPointsWrap.add(elPoints);
});
});
elSeries.add(elPointsWrap);
}
function darkenColor(color, amount) {
const rgb = Utils.parseHex(color);
if (!rgb) return color;
const f = Math.max(0, 1 - amount);
return `rgb(${Math.round(rgb[0] * f)},${Math.round(rgb[1] * f)},${Math.round(rgb[2] * f)})`;
}
function rampColorAt(colors, t) {
if (!colors.length) return "#000";
if (colors.length === 1) return colors[0];
const x = Math.max(0, Math.min(1, t)) * (colors.length - 1);
const i = Math.floor(x);
const frac = x - i;
const c0 = Utils.parseHex(colors[i]) || [0, 0, 0];
const c1 = Utils.parseHex(colors[Math.min(i + 1, colors.length - 1)]) || c0;
const mix = (a, b2) => Math.round(a + (b2 - a) * frac);
return `rgb(${mix(c0[0], c1[0])},${mix(c0[1], c1[1])},${mix(c0[2], c1[2])})`;
}
function hash01(n) {
let h = (n ^ 2654435769) >>> 0;
h = Math.imul(h ^ h >>> 16, 73244475);
h = Math.imul(h ^ h >>> 16, 73244475);
return ((h ^ h >>> 16) >>> 0) / 4294967296;
}
function circleSubPath(px, py, r) {
return `M ${px - r} ${py} a ${r} ${r} 0 1 0 ${2 * r} 0 a ${r} ${r} 0 1 0 ${-2 * r} 0 `;
}
function squareSubPath(px, py, r) {
return `M ${px - r} ${py - r} h ${2 * r} v ${2 * r} h ${-2 * r} z `;
}
class BoxCandleStick extends Bar {
/**
* @param {any[]} series
* @param {string} ctype
* @param {number} seriesIndex
*/
// @ts-ignore -- BoxCandleStick.draw has an extra ctype param compared to Bar.draw
draw(series, ctype, seriesIndex) {
var _a;
const w2 = this.w;
const graphics = new Graphics(this.w);
const type = w2.globals.comboCharts ? ctype : w2.config.chart.type;
const fill = new Fill(this.w);
this.candlestickOptions = this.w.config.plotOptions.candlestick;
this.boxOptions = this.w.config.plotOptions.boxPlot;
this.isHorizontal = w2.config.plotOptions.bar.horizontal;
this.isOHLC = this.candlestickOptions && this.candlestickOptions.type === "ohlc";
this.coreUtils = new CoreUtils(this.w);
series = this.coreUtils.getLogSeries(series);
this.series = series;
this.yRatio = this.coreUtils.getLogYRatios(this.yRatio);
this.barHelpers.initVariables(series);
const ret = graphics.group({
class: `apexcharts-${type}-series apexcharts-plot-series`
});
for (let i = 0; i < series.length; i++) {
this.isBoxPlot = w2.config.chart.type === "boxPlot" || /** @type {Record<string,any>} */
w2.config.series[i].type === "boxPlot";
let x;
let y;
const yArrj = [];
const xArrj = [];
const realIndex = w2.globals.comboCharts ? (
/** @type {any} */
seriesIndex[i]
) : i;
const { columnGroupIndex } = this.barHelpers.getGroupIndex(realIndex);
const elSeries = graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[realIndex]),
rel: i + 1,
"data:realIndex": realIndex
});
Series.addCollapsedClassToSeries(this.w, elSeries, realIndex);
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1;
}
let translationsIndex = 0;
if (this.yRatio.length > 1) {
this.yaxisIndex = /** @type {any} */
w2.globals.seriesYAxisReverseMap[realIndex][0];
translationsIndex = realIndex;
}
const initPositions = this.barHelpers.initialPositions(realIndex);
const {
y: initY,
barHeight,
yDivision,
// yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroW,
// zeroW is the baseline where 0 meets x axis
x: initX,
barWidth,
xDivision,
// xDivision is the GRIDWIDTH divided by number of datapoints (columns)
zeroH
// zeroH is the baseline where 0 meets y axis
} = initPositions;
y = initY;
x = initX;
xArrj.push(x + (barWidth != null ? barWidth : 0) / 2);
const elDataLabelsWrap = graphics.group({
class: "apexcharts-datalabels",
"data:realIndex": realIndex
});
const elGoalsMarkers = graphics.group({
class: "apexcharts-bar-goals-markers"
});
const boxPointsOpts = this.isBoxPlot ? this.boxOptions.points : null;
const pointsByCat = [];
const gridW = w2.layout.gridWidth;
const cullBuffer = barWidth != null ? barWidth : 0;
for (let j = 0; j < w2.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex);
let paths = (
/** @type {any} */
null
);
const pathsParams = {
indexes: {
i,
j,
realIndex,
translationsIndex
},
x,
y,
strokeWidth,
elSeries
};
if (this.isHorizontal) {
paths = this.drawHorizontalBoxPaths(__spreadProps(__spreadValues({}, pathsParams), {
yDivision,
barHeight,
zeroW
}));
} else {
paths = this.drawVerticalBoxPaths(__spreadProps(__spreadValues({}, pathsParams), {
xDivision,
barWidth,
zeroH,
cullBounds: { lo: -cullBuffer, hi: gridW + cullBuffer }
}));
}
y = paths.y;
x = paths.x;
if (j > 0) {
xArrj.push(x + (barWidth != null ? barWidth : 0) / 2);
}
yArrj.push(y);
if (paths.culled) {
continue;
}
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition: paths.barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth
});
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine);
}
paths.pathTo.forEach(
(pathTo, pi) => {
const lineFill = !this.isBoxPlot && this.candlestickOptions.wick.useFillColor ? paths.color[pi] : w2.globals.stroke.colors[i];
const pathFill = fill.fillPath({
seriesNumber: realIndex,
dataPointIndex: j,
color: paths.color[pi],
value: series[i][j]
});
this.renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
pathFrom: paths.pathFrom,
pathTo,
strokeWidth,
elSeries,
x,
y,
series,
columnGroupIndex,
barHeight,
barWidth,
elDataLabelsWrap,
elGoalsMarkers,
visibleSeries: this.visibleI,
type: w2.config.chart.type
});
}
);
if (boxPointsOpts && boxPointsOpts.show !== false) {
const pts = (_a = w2.candleData.seriesBoxPoints[realIndex]) == null ? void 0 : _a[j];
if (pts && pts.length) {
const logVal = (v2) => (
/** @type {any} */
this.coreUtils.getLogValAtSeriesIndex(
v2,
realIndex
)
);
let center, halfExtent, alongFn;
if (this.isHorizontal) {
const yRatio = this.invertedYRatio;
const bh = barHeight != null ? barHeight : 0;
const z = zeroW != null ? zeroW : 0;
center = paths.barYPosition + bh / 2;
halfExtent = bh / 2;
alongFn = (v2) => z + logVal(v2) / yRatio;
} else {
const yRatio = this.yRatio[translationsIndex];
const bw = barWidth != null ? barWidth : 0;
const z = zeroH != null ? zeroH : 0;
center = paths.barXPosition + bw / 2;
halfExtent = bw / 2;
alongFn = (v2) => z - logVal(v2) / yRatio;
}
const groups = buildJitterGroups({
w: w2,
points: pts,
seedA: realIndex,
seedB: j,
center,
halfExtent,
alongFn,
isHorizontal: this.isHorizontal,
options: boxPointsOpts
});
if (groups.length) pointsByCat.push({ groups, j });
}
}
}
if (boxPointsOpts) {
renderJitter({
graphics,
w: w2,
elSeries,
pointsByCat,
options: boxPointsOpts,
distributed: w2.config.plotOptions.bar.distributed,
realIndex,
wrapClass: "apexcharts-boxPlot-points-wrap",
pointClass: "apexcharts-boxPlot-points"
});
}
w2.globals.seriesXvalues[realIndex] = xArrj;
w2.globals.seriesYvalues[realIndex] = yArrj;
ret.add(elSeries);
}
return ret;
}
/** @param {{indexes: any, x: any, xDivision: any, barWidth: any, zeroH: any, strokeWidth: any, cullBounds?: {lo: number, hi: number}|null}} opts */
drawVerticalBoxPaths({
indexes,
x,
xDivision,
barWidth,
zeroH,
strokeWidth,
cullBounds = null
}) {
var _a, _b, _c, _d;
const w2 = this.w;
const graphics = new Graphics(this.w);
const i = indexes.i;
const j = indexes.j;
const { colors: candleColors } = w2.config.plotOptions.candlestick;
const { colors: boxColors } = this.boxOptions;
const realIndex = indexes.realIndex;
const getColor = (color2) => Array.isArray(color2) ? color2[realIndex] : color2;
const colorPos = getColor(candleColors.upward);
const colorNeg = getColor(candleColors.downward);
const yRatio = this.yRatio[indexes.translationsIndex];
const ohlc = this.getOHLCValue(realIndex, j);
let l1 = zeroH;
let l2 = zeroH;
let color = ohlc.o < ohlc.c ? [colorPos] : [colorNeg];
if (this.isBoxPlot) {
color = [getColor(boxColors.lower), getColor(boxColors.upper)];
}
let y1 = Math.min(ohlc.o, ohlc.c);
let y2 = Math.max(ohlc.o, ohlc.c);
let m2 = ohlc.m;
if (w2.axisFlags.isXNumeric) {
x = (w2.seriesData.seriesX[realIndex][j] - w2.globals.minX) / this.xRatio - barWidth / 2;
}
const barXPosition = x + barWidth * this.visibleI;
if (typeof /** @type {any} */
((_a = this.series[i]) == null ? void 0 : _a[j]) === "undefined" || /** @type {any} */
((_b = this.series[i]) == null ? void 0 : _b[j]) === null) {
y1 = zeroH;
y2 = zeroH;
} else {
y1 = zeroH - y1 / yRatio;
y2 = zeroH - y2 / yRatio;
l1 = zeroH - ohlc.h / yRatio;
l2 = zeroH - ohlc.l / yRatio;
m2 = zeroH - ohlc.m / yRatio;
}
if (cullBounds && (barXPosition + barWidth < cullBounds.lo || barXPosition > cullBounds.hi)) {
return {
pathTo: null,
pathFrom: null,
x: w2.axisFlags.isXNumeric ? x : x + xDivision,
y: y2,
barXPosition,
color,
culled: true
};
}
let pathTo;
if (this.isOHLC) {
const centerX = barXPosition + barWidth / 2;
const openY = zeroH - ohlc.o / yRatio;
const closeY = zeroH - ohlc.c / yRatio;
pathTo = [
graphics.move(centerX, l1) + graphics.line(centerX, l2) + graphics.move(centerX, openY) + graphics.line(barXPosition, openY) + graphics.move(centerX, closeY) + graphics.line(barXPosition + barWidth, closeY)
];
} else if (this.isBoxPlot) {
pathTo = [
graphics.move(barXPosition, y1) + graphics.line(barXPosition + barWidth / 2, y1) + graphics.line(barXPosition + barWidth / 2, l1) + graphics.line(barXPosition + barWidth / 4, l1) + graphics.line(barXPosition + barWidth - barWidth / 4, l1) + graphics.line(barXPosition + barWidth / 2, l1) + graphics.line(barXPosition + barWidth / 2, y1) + graphics.line(barXPosition + barWidth, y1) + graphics.line(barXPosition + barWidth, m2) + graphics.line(barXPosition, m2) + graphics.line(barXPosition, y1 + strokeWidth / 2),
graphics.move(barXPosition, m2) + graphics.line(barXPosition + barWidth, m2) + graphics.line(barXPosition + barWidth, y2) + graphics.line(barXPosition + barWidth / 2, y2) + graphics.line(barXPosition + barWidth / 2, l2) + graphics.line(barXPosition + barWidth - barWidth / 4, l2) + graphics.line(barXPosition + barWidth / 4, l2) + graphics.line(barXPosition + barWidth / 2, l2) + graphics.line(barXPosition + barWidth / 2, y2) + graphics.line(barXPosition, y2) + graphics.line(barXPosition, m2) + "z"
];
} else {
pathTo = [
graphics.move(barXPosition, y2) + graphics.line(barXPosition + barWidth / 2, y2) + graphics.line(barXPosition + barWidth / 2, l1) + graphics.line(barXPosition + barWidth / 2, y2) + graphics.line(barXPosition + barWidth, y2) + graphics.line(barXPosition + barWidth, y1) + graphics.line(barXPosition + barWidth / 2, y1) + graphics.line(barXPosition + barWidth / 2, l2) + graphics.line(barXPosition + barWidth / 2, y1) + graphics.line(barXPosition, y1) + graphics.line(barXPosition, y2 - strokeWidth / 2)
];
}
let pathFrom = null;
const morphFrom = (_d = (_c = this.ctx) == null ? void 0 : _c.morphTypeChange) == null ? void 0 : _d.getInitialPathFor(realIndex, j);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, pathTo[0]);
}
if (pathFrom == null) {
pathFrom = graphics.move(barXPosition + barWidth / 2, y1) + graphics.move(barXPosition, y1);
}
if (!w2.axisFlags.isXNumeric) {
x = x + xDivision;
}
return {
pathTo,
pathFrom,
x,
y: y2,
goalY: this.barHelpers.getGoalValues(
"y",
/** @type {any} */
null,
zeroH,
realIndex,
j,
indexes.translationsIndex
),
barXPosition,
color
};
}
/** @param {{indexes: any, y: any, yDivision: any, barHeight: any, zeroW: any, strokeWidth: any}} opts */
drawHorizontalBoxPaths({
indexes,
y,
yDivision,
barHeight,
zeroW,
strokeWidth
}) {
var _a, _b, _c, _d;
const w2 = this.w;
const graphics = new Graphics(this.w);
const i = indexes.i;
const j = indexes.j;
const realIndex = indexes.realIndex;
const { colors: candleColors } = w2.config.plotOptions.candlestick;
const { colors: boxColors } = this.boxOptions;
const getColor = (color2) => Array.isArray(color2) ? color2[realIndex] : color2;
const yRatio = this.invertedYRatio;
const ohlc = this.getOHLCValue(realIndex, j);
let color = ohlc.o < ohlc.c ? [getColor(candleColors.upward)] : [getColor(candleColors.downward)];
if (this.isBoxPlot) {
color = [getColor(boxColors.lower), getColor(boxColors.upper)];
}
let l1 = zeroW;
let l2 = zeroW;
let x1 = Math.min(ohlc.o, ohlc.c);
let x2 = Math.max(ohlc.o, ohlc.c);
let m2 = ohlc.m;
if (w2.axisFlags.isXNumeric) {
y = (w2.seriesData.seriesX[realIndex][j] - w2.globals.minX) / this.invertedXRatio - barHeight / 2;
}
const barYPosition = y + barHeight * this.visibleI;
if (typeof /** @type {any} */
((_a = this.series[i]) == null ? void 0 : _a[j]) === "undefined" || /** @type {any} */
((_b = this.series[i]) == null ? void 0 : _b[j]) === null) {
x1 = zeroW;
x2 = zeroW;
} else {
x1 = zeroW + x1 / yRatio;
x2 = zeroW + x2 / yRatio;
l1 = zeroW + ohlc.h / yRatio;
l2 = zeroW + ohlc.l / yRatio;
m2 = zeroW + ohlc.m / yRatio;
}
const pathTo = [
graphics.move(x1, barYPosition) + graphics.line(x1, barYPosition + barHeight / 2) + graphics.line(l1, barYPosition + barHeight / 2) + graphics.line(l1, barYPosition + barHeight / 2 - barHeight / 4) + graphics.line(l1, barYPosition + barHeight / 2 + barHeight / 4) + graphics.line(l1, barYPosition + barHeight / 2) + graphics.line(x1, barYPosition + barHeight / 2) + graphics.line(x1, barYPosition + barHeight) + graphics.line(m2, barYPosition + barHeight) + graphics.line(m2, barYPosition) + graphics.line(x1 + strokeWidth / 2, barYPosition),
graphics.move(m2, barYPosition) + graphics.line(m2, barYPosition + barHeight) + graphics.line(x2, barYPosition + barHeight) + graphics.line(x2, barYPosition + barHeight / 2) + graphics.line(l2, barYPosition + barHeight / 2) + graphics.line(l2, barYPosition + barHeight - barHeight / 4) + graphics.line(l2, barYPosition + barHeight / 4) + graphics.line(l2, barYPosition + barHeight / 2) + graphics.line(x2, barYPosition + barHeight / 2) + graphics.line(x2, barYPosition) + graphics.line(m2, barYPosition) + "z"
];
let pathFrom = null;
const morphFrom = (_d = (_c = this.ctx) == null ? void 0 : _c.morphTypeChange) == null ? void 0 : _d.getInitialPathFor(realIndex, j);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, pathTo[0]);
}
if (pathFrom == null) {
pathFrom = graphics.move(x1, barYPosition + barHeight / 2) + graphics.move(x1, barYPosition);
}
if (!w2.axisFlags.isXNumeric) {
y = y + yDivision;
}
return {
pathTo,
pathFrom,
x: x2,
y,
goalX: this.barHelpers.getGoalValues(
"x",
zeroW,
/** @type {any} */
null,
realIndex,
j,
0
),
barYPosition,
color
};
}
/**
* @param {number} i
* @param {number} j
*/
getOHLCValue(i, j) {
const w2 = this.w;
const coreUtils = this.coreUtils;
const getCandleVal = (arr) => arr[i] && arr[i][j] != null ? (
/** @type {any} */
coreUtils.getLogValAtSeriesIndex(arr[i][j], i)
) : 0;
const h = getCandleVal(w2.candleData.seriesCandleH);
const o = getCandleVal(w2.candleData.seriesCandleO);
const m2 = getCandleVal(w2.candleData.seriesCandleM);
const c = getCandleVal(w2.candleData.seriesCandleC);
const l = getCandleVal(w2.candleData.seriesCandleL);
return {
o: this.isBoxPlot ? h : o,
h: this.isBoxPlot ? o : h,
m: m2,
l: this.isBoxPlot ? c : l,
c: this.isBoxPlot ? l : c
};
}
}
const tangents = (points) => {
const m2 = finiteDifferences(points);
const n = points.length - 1;
const ε = 1e-6;
const tgts = [];
let a, b2, d, s;
for (let i = 0; i < n; i++) {
d = slope(points[i], points[i + 1]);
if (Math.abs(d) < ε) {
m2[i] = m2[i + 1] = 0;
} else {
a = m2[i] / d;
b2 = m2[i + 1] / d;
s = a * a + b2 * b2;
if (s > 9) {
s = d * 3 / Math.sqrt(s);
m2[i] = s * a;
m2[i + 1] = s * b2;
}
}
}
for (let i = 0; i <= n; i++) {
s = (points[Math.min(n, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m2[i] * m2[i]));
tgts.push([s || 0, m2[i] * s || 0]);
}
return tgts;
};
const svgPath = (points) => {
let p2 = "";
for (let i = 0; i < points.length; i++) {
const point = points[i];
const n = point.length;
if (n > 4) {
p2 += `C${point[0]}, ${point[1]}`;
p2 += `, ${point[2]}, ${point[3]}`;
p2 += `, ${point[4]}, ${point[5]}`;
} else if (n > 2) {
p2 += `S${point[0]}, ${point[1]}`;
p2 += `, ${point[2]}, ${point[3]}`;
}
}
return p2;
};
const spline = {
/**
* Convert 'points' to bezier
* @param {any[]} points
* @returns {any[]}
*/
points(points) {
const tgts = tangents(points);
const p2 = points[1];
const p0 = points[0];
const pts = [];
const t = tgts[1];
const t0 = tgts[0];
pts.push(p0, [
p0[0] + t0[0],
p0[1] + t0[1],
p2[0] - t[0],
p2[1] - t[1],
p2[0],
p2[1]
]);
for (let i = 2, n = tgts.length; i < n; i++) {
const p3 = points[i];
const t2 = tgts[i];
pts.push([p3[0] - t2[0], p3[1] - t2[1], p3[0], p3[1]]);
}
return pts;
},
/**
* Slice out a segment of 'points'
* @param {any[]} points
* @param {Number} start
* @param {Number} end
* @returns {any[]}
*/
slice(points, start, end) {
const pts = points.slice(start, end);
if (start) {
if (end - start > 1 && pts[1].length < 6) {
const n = pts[0].length;
pts[1] = [
pts[0][n - 2] * 2 - pts[0][n - 4],
pts[0][n - 1] * 2 - pts[0][n - 3]
].concat(pts[1]);
}
pts[0] = pts[0].slice(-2);
}
return pts;
}
};
function slope(p0, p1) {
return (p1[1] - p0[1]) / (p1[0] - p0[0]);
}
function finiteDifferences(points) {
const m2 = [];
let p0 = points[0];
let p1 = points[1];
let d = m2[0] = slope(p0, p1);
let i = 1;
for (let n = points.length - 1; i < n; i++) {
p0 = p1;
p1 = points[i + 1];
m2[i] = (d + (d = slope(p0, p1))) * 0.5;
}
m2[i] = d;
return m2;
}
class Violin extends Bar {
/**
* @param {any[]} series
* @param {string} ctype
* @param {number} [seriesIndex]
*/
// @ts-ignore -- Violin.draw has extra ctype param compared to Bar.draw
draw(series, ctype, seriesIndex) {
const w2 = this.w;
const graphics = new Graphics(this.w);
const fill = new Fill(this.w);
this.violinOptions = w2.config.plotOptions.violin;
this.pointsOptions = this.violinOptions.points;
this.bandwidthScale = this.violinOptions.bandwidthScale || 1;
this.normalize = this.violinOptions.normalize || "individual";
this.distributed = w2.config.plotOptions.bar.distributed;
this.isHorizontal = w2.config.plotOptions.bar.horizontal;
this.coreUtils = new CoreUtils(this.w);
series = this.coreUtils.getLogSeries(series);
this.series = series;
this.yRatio = this.coreUtils.getLogYRatios(this.yRatio);
this.barHelpers.initVariables(series);
const ret = graphics.group({
class: "apexcharts-violin-series apexcharts-plot-series"
});
for (let i = 0; i < series.length; i++) {
let x;
let y;
const yArrj = [];
const xArrj = [];
const realIndex = w2.globals.comboCharts ? (
/** @type {any} */
seriesIndex[i]
) : i;
const { columnGroupIndex } = this.barHelpers.getGroupIndex(realIndex);
const elSeries = graphics.group({
class: "apexcharts-series",
seriesName: Utils.escapeString(w2.seriesData.seriesNames[realIndex]),
rel: i + 1,
"data:realIndex": realIndex
});
Series.addCollapsedClassToSeries(this.w, elSeries, realIndex);
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1;
}
let translationsIndex = 0;
if (this.yRatio.length > 1) {
this.yaxisIndex = /** @type {any} */
w2.globals.seriesYAxisReverseMap[realIndex][0];
translationsIndex = realIndex;
}
const initPositions = this.barHelpers.initialPositions(realIndex);
const {
y: initY,
barHeight,
yDivision,
zeroW,
x: initX,
barWidth,
xDivision,
zeroH
} = initPositions;
y = initY;
x = initX;
xArrj.push(x + (barWidth != null ? barWidth : 0) / 2);
const elDataLabelsWrap = graphics.group({
class: "apexcharts-datalabels",
"data:realIndex": realIndex
});
this.seriesMaxWeight = 0;
if (this.normalize === "group") {
const dens = w2.violinData.seriesViolinDensity[realIndex] || [];
dens.forEach((d) => {
if (d && d.maxWeight > this.seriesMaxWeight) {
this.seriesMaxWeight = d.maxWeight;
}
});
}
const pointsByViolin = [];
for (let j = 0; j < w2.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex);
const paths = this.isHorizontal ? this.drawHorizontalViolin({
indexes: { i, j, realIndex, translationsIndex },
y,
yDivision,
barHeight,
zeroW
}) : this.drawVerticalViolin({
indexes: { i, j, realIndex, translationsIndex },
x,
xDivision,
barWidth,
zeroH
});
x = paths.x;
y = paths.y;
if (j > 0) {
xArrj.push(paths.center);
}
yArrj.push(paths.alongRepresentative);
const pointGroups = this.buildPointsSubPath({
realIndex,
j,
center: paths.center,
halfExtent: paths.halfExtent,
alongFn: paths.alongFn,
density: paths.density,
maxWeight: paths.maxWeight
});
if (pointGroups.length) pointsByViolin.push({ groups: pointGroups, j });
const pathFill = fill.fillPath({
// distributed → color per category (data point) instead of per series
seriesNumber: this.distributed ? j : realIndex,
dataPointIndex: j,
color: this.distributed ? w2.globals.colors[j] : void 0,
value: series[i][j]
});
this.renderSeries({
realIndex,
pathFill,
lineFill: w2.globals.stroke.colors[realIndex],
j,
i,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
x,
y,
series,
columnGroupIndex,
barHeight,
barWidth,
elDataLabelsWrap,
visibleSeries: this.visibleI,
type: "violin"
});
const bodyEl = elSeries.node.querySelector(
`path.apexcharts-violin-area[j='${j}']`
);
if (bodyEl && isFinite(paths.alongRepresentative)) {
bodyEl.setAttribute(
this.isHorizontal ? "cx" : "cy",
`${paths.alongRepresentative}`
);
}
}
renderJitter({
graphics,
w: w2,
elSeries,
pointsByCat: pointsByViolin,
options: this.pointsOptions,
distributed: this.distributed,
realIndex,
wrapClass: "apexcharts-violin-points-wrap",
pointClass: "apexcharts-violin-points"
});
w2.globals.seriesXvalues[realIndex] = xArrj;
w2.globals.seriesYvalues[realIndex] = yArrj;
ret.add(elSeries);
}
return ret;
}
/** @param {{indexes: any, x: any, xDivision: any, barWidth: any, zeroH: any}} opts */
drawVerticalViolin({ indexes, x, xDivision, barWidth, zeroH }) {
var _a, _b, _c;
const w2 = this.w;
const { realIndex, j, translationsIndex } = indexes;
const yRatio = this.yRatio[translationsIndex];
if (w2.axisFlags.isXNumeric) {
x = (w2.seriesData.seriesX[realIndex][j] - w2.globals.minX) / this.xRatio - barWidth / 2;
}
const barXPosition = x + barWidth * this.visibleI;
const center = barXPosition + barWidth / 2;
const halfExtent = barWidth / 2;
const density = this.getDensity(realIndex, j);
const maxWeight = this.effectiveMaxWeight(density);
const alongFn = (v2) => zeroH - this.logVal(v2, realIndex) / yRatio;
const pathTo = this.buildBodyPath({
nodes: density.nodes,
center,
halfExtent,
maxWeight,
vertical: true,
alongFn,
collapsed: false
});
let pathFrom = null;
const morphFrom = (_b = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange) == null ? void 0 : _b.getInitialPathFor(realIndex, j);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, pathTo);
}
if (pathFrom == null) {
pathFrom = this.buildBodyPath({
nodes: density.nodes,
center,
halfExtent,
maxWeight,
vertical: true,
alongFn,
collapsed: true
});
}
if (!w2.axisFlags.isXNumeric) {
x = x + xDivision;
}
return {
pathTo,
pathFrom,
x,
y: zeroH,
center,
halfExtent,
alongFn,
density,
maxWeight,
alongRepresentative: alongFn((_c = this.series[indexes.i][j]) != null ? _c : 0)
};
}
/** @param {{indexes: any, y: any, yDivision: any, barHeight: any, zeroW: any}} opts */
drawHorizontalViolin({ indexes, y, yDivision, barHeight, zeroW }) {
var _a, _b, _c;
const w2 = this.w;
const { realIndex, j } = indexes;
const yRatio = this.invertedYRatio;
if (w2.axisFlags.isXNumeric) {
y = (w2.seriesData.seriesX[realIndex][j] - w2.globals.minX) / this.invertedXRatio - barHeight / 2;
}
const barYPosition = y + barHeight * this.visibleI;
const center = barYPosition + barHeight / 2;
const halfExtent = barHeight / 2;
const density = this.getDensity(realIndex, j);
const maxWeight = this.effectiveMaxWeight(density);
const alongFn = (v2) => zeroW + this.logVal(v2, realIndex) / yRatio;
const pathTo = this.buildBodyPath({
nodes: density.nodes,
center,
halfExtent,
maxWeight,
vertical: false,
alongFn,
collapsed: false
});
let pathFrom = null;
const morphFrom = (_b = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange) == null ? void 0 : _b.getInitialPathFor(realIndex, j);
if (morphFrom) {
pathFrom = morphFrom;
} else if (w2.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, pathTo);
}
if (pathFrom == null) {
pathFrom = this.buildBodyPath({
nodes: density.nodes,
center,
halfExtent,
maxWeight,
vertical: false,
alongFn,
collapsed: true
});
}
if (!w2.axisFlags.isXNumeric) {
y = y + yDivision;
}
return {
pathTo,
pathFrom,
x: zeroW,
y,
center,
halfExtent,
alongFn,
maxWeight,
density,
alongRepresentative: alongFn((_c = this.series[indexes.i][j]) != null ? _c : 0)
};
}
/**
* Read the parsed density for one violin and return sorted, de-duplicated
* nodes (strictly increasing value — a hard requirement for the spline).
* @param {number} realIndex
* @param {number} j
*/
getDensity(realIndex, j) {
var _a;
const w2 = this.w;
const d = (_a = w2.violinData.seriesViolinDensity[realIndex]) == null ? void 0 : _a[j];
if (!d || !d.values.length) {
return { nodes: [], maxWeight: 0 };
}
const order = d.values.map(
(_, k) => k
);
order.sort(
(a, b2) => d.values[a] - d.values[b2]
);
const nodes = [];
let prevV = null;
for (const k of order) {
const v2 = d.values[k];
if (prevV !== null && v2 === prevV) continue;
nodes.push({ v: v2, w: d.weights[k] });
prevV = v2;
}
return { nodes, maxWeight: d.maxWeight };
}
/**
* The peak weight used to scale a violin's width: its own ('individual') or
* the densest violin in the series ('group', preserving relative widths).
* @param {{maxWeight:number}} density
*/
effectiveMaxWeight(density) {
return this.normalize === "group" && this.seriesMaxWeight > 0 ? this.seriesMaxWeight : density.maxWeight;
}
/**
* Build the closed, smooth violin outline. The value axis is the monotonic
* parameter for the spline (vertical → Y, horizontal → X); the spline is fed
* with that axis first and the control points swapped back to screen space.
*
* @param {{nodes:{v:number,w:number}[], center:number, halfExtent:number, maxWeight:number, vertical:boolean, alongFn:(v:number)=>number, collapsed:boolean}} opts
*/
buildBodyPath({
nodes,
center,
halfExtent,
maxWeight,
vertical,
alongFn,
collapsed
}) {
const graphics = new Graphics(this.w);
if (nodes.length === 0) {
const a = alongFn(0);
return vertical ? graphics.move(center, a) + graphics.line(center, a) : graphics.move(a, center) + graphics.line(a, center);
}
const wpxOf = (weight) => {
if (collapsed || maxWeight <= 0) return 0;
const wp = weight / maxWeight * halfExtent * this.bandwidthScale;
return Math.min(halfExtent, Math.max(0, wp));
};
const rightPts = [];
const leftPts = [];
for (let k = 0; k < nodes.length; k++) {
const a = alongFn(nodes[k].v);
const wp = wpxOf(nodes[k].w);
if (vertical) {
rightPts.push([center + wp, a]);
leftPts.push([center - wp, a]);
} else {
rightPts.push([a, center + wp]);
leftPts.push([a, center - wp]);
}
}
leftPts.reverse();
return this.smoothSegment(rightPts, vertical, false) + this.smoothSegment(leftPts, vertical, true) + "z";
}
/**
* Emit one edge as a smooth (monotone-cubic) path segment, or a polyline
* when there are too few nodes for a spline.
*
* @param {[number,number][]} screenPts ordered screen points for this edge
* @param {boolean} monotonicIsY true when the value axis is vertical
* @param {boolean} continued false → start with M; true → start with L (joins the previous edge)
*/
smoothSegment(screenPts, monotonicIsY, continued) {
const graphics = new Graphics(this.w);
const first = screenPts[0];
let d = continued ? graphics.line(first[0], first[1]) : graphics.move(first[0], first[1]);
const usePolyline = screenPts.length < 3 || !this.strictlyMonotonic(screenPts, monotonicIsY);
if (usePolyline) {
for (let k = 1; k < screenPts.length; k++) {
d += graphics.line(screenPts[k][0], screenPts[k][1]);
}
return d;
}
const input = screenPts.map(
([px, py]) => monotonicIsY ? [py, px] : [px, py]
);
const bez = spline.points(input);
const out = monotonicIsY ? bez.map(swapPairs) : bez;
d += svgPath(out);
return d;
}
/**
* @param {[number,number][]} screenPts
* @param {boolean} monotonicIsY
*/
strictlyMonotonic(screenPts, monotonicIsY) {
const axis = monotonicIsY ? 1 : 0;
for (let k = 1; k < screenPts.length; k++) {
if (screenPts[k][axis] === screenPts[k - 1][axis]) return false;
}
return true;
}
/**
* Build the jitter sub-paths for one violin, grouped for rendering. Returns
* `[]` when points are hidden or absent. Normally one group (single dot
* colour); with `points.colorScale` the dots are bucketed by value into shade
* groups, each carrying its ramp colour. Offsets are a deterministic index
* hash (SSR-safe); points beyond maxPoints are stride-thinned.
*
* @param {{realIndex:number, j:number, center:number, halfExtent:number, alongFn:(v:number)=>number, density:{nodes:{v:number,w:number}[], maxWeight:number}, maxWeight:number}} opts
* @returns {{fill:string|null, d:string}[]}
*/
buildPointsSubPath({
realIndex,
j,
center,
halfExtent,
alongFn,
density,
maxWeight
}) {
var _a;
return buildJitterGroups({
w: this.w,
points: (_a = this.w.violinData.seriesViolinPoints[realIndex]) == null ? void 0 : _a[j],
seedA: realIndex,
seedB: j,
center,
halfExtent,
alongFn,
isHorizontal: this.isHorizontal,
options: this.pointsOptions,
// Violin clamps jitter to the density half-width at each value so dots
// stay inside the shape.
clampAt: (v2) => this.halfWidthAtValue(v2, density, halfExtent, maxWeight)
});
}
/**
* Density half-width (pixels) at a given value — used to keep jitter inside
* the violin. Linear interpolation between the two nearest density nodes.
* @param {number} value
* @param {{nodes:{v:number,w:number}[], maxWeight:number}} density
* @param {number} halfExtent
* @param {number} [maxWeightOverride] use the group max for 'group' normalize
*/
halfWidthAtValue(value, density, halfExtent, maxWeightOverride) {
const { nodes } = density;
const maxWeight = maxWeightOverride != null ? maxWeightOverride : density.maxWeight;
if (!nodes.length || maxWeight <= 0) return 0;
const toPx = (weight) => Math.min(
halfExtent,
weight / maxWeight * halfExtent * this.bandwidthScale
);
if (value <= nodes[0].v) return toPx(nodes[0].w);
if (value >= nodes[nodes.length - 1].v)
return toPx(nodes[nodes.length - 1].w);
for (let k = 1; k < nodes.length; k++) {
if (value <= nodes[k].v) {
const a = nodes[k - 1];
const b2 = nodes[k];
const t = b2.v === a.v ? 0 : (value - a.v) / (b2.v - a.v);
return toPx(a.w + (b2.w - a.w) * t);
}
}
return 0;
}
/**
* Apply the y-axis log transform to a value when that axis is logarithmic,
* mirroring BoxCandleStick. Linear axes return the value unchanged.
* @param {number} value
* @param {number} realIndex
*/
logVal(value, realIndex) {
return (
/** @type {any} */
this.coreUtils.getLogValAtSeriesIndex(
value,
realIndex
)
);
}
}
function swapPairs(arr) {
const out = [];
for (let k = 0; k < arr.length; k += 2) {
out.push(arr[k + 1], arr[k]);
}
return out;
}
class TreemapHelpers {
/**
* @param {import('../../../types/internal').ChartStateW} w
* @param {import('../../../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.ctx = ctx;
this.w = w2;
}
checkColorRange() {
const w2 = this.w;
let negRange = false;
const chartOpts = w2.config.plotOptions[w2.config.chart.type];
if (chartOpts.colorScale.ranges.length > 0) {
chartOpts.colorScale.ranges.map((range) => {
if (range.from <= 0) {
negRange = true;
}
});
}
return negRange;
}
/**
* @param {string} chartType
* @param {number} i
* @param {number} j
* @param {any} negRange
*/
getShadeColor(chartType, i, j, negRange) {
const w2 = this.w;
let colorShadePercent = 1;
const shadeIntensity = w2.config.plotOptions[chartType].shadeIntensity;
const colorProps = this.determineColor(chartType, i, j);
if (
/** @type {any} */
w2.globals.hasNegs || negRange
) {
if (w2.config.plotOptions[chartType].reverseNegativeShade) {
if (colorProps.percent < 0) {
colorShadePercent = colorProps.percent / 100 * (shadeIntensity * 1.25);
} else {
colorShadePercent = (1 - colorProps.percent / 100) * (shadeIntensity * 1.25);
}
} else {
if (colorProps.percent <= 0) {
colorShadePercent = 1 - (1 + colorProps.percent / 100) * shadeIntensity;
} else {
colorShadePercent = (1 - colorProps.percent / 100) * shadeIntensity;
}
}
} else {
colorShadePercent = 1 - colorProps.percent / 100;
if (chartType === "treemap") {
colorShadePercent = (1 - colorProps.percent / 100) * (shadeIntensity * 1.25);
}
}
let color = colorProps.color;
const utils = new Utils();
if (w2.config.plotOptions[chartType].enableShades) {
if (this.w.config.theme.mode === "dark") {
const shadeColor = utils.shadeColor(
colorShadePercent * -1,
colorProps.color
);
color = Utils.hexToRgba(
Utils.isColorHex(shadeColor) ? shadeColor : Utils.rgb2hex(shadeColor),
w2.config.fill.opacity
);
} else {
const shadeColor = utils.shadeColor(colorShadePercent, colorProps.color);
color = Utils.hexToRgba(
Utils.isColorHex(shadeColor) ? shadeColor : Utils.rgb2hex(shadeColor),
w2.config.fill.opacity
);
}
}
return { color, colorProps };
}
/**
* @param {string} chartType
* @param {number} i
* @param {number} j
*/
determineColor(chartType, i, j) {
const w2 = this.w;
const val = w2.seriesData.series[i][j];
const chartOpts = w2.config.plotOptions[chartType];
let seriesNumber = chartOpts.colorScale.inverse ? j : i;
if (chartOpts.distributed && w2.config.chart.type === "treemap") {
seriesNumber = j;
}
let color = w2.globals.colors[seriesNumber];
let foreColor = null;
let min;
let max;
if (!chartOpts.distributed && chartType === "heatmap") {
min = w2.globals.minY;
max = w2.globals.maxY;
} else {
const row = w2.seriesData.series[i];
min = row.length ? row[0] : 0;
max = min;
for (let k = 1; k < row.length; k++) {
if (row[k] < min) min = row[k];
if (row[k] > max) max = row[k];
}
}
const csMin = chartOpts.colorScale.min;
const csMax = chartOpts.colorScale.max;
if (typeof csMin !== "undefined" && typeof csMax !== "undefined" && csMax > csMin) {
min = csMin;
max = csMax;
} else if (typeof csMin !== "undefined") {
min = csMin < w2.globals.minY ? csMin : w2.globals.minY;
max = csMax > w2.globals.maxY ? csMax : w2.globals.maxY;
}
const total = Math.abs(max) + Math.abs(min);
const clamped = Math.min(Math.max(val, min), max);
let percent = total === 0 ? 0 : 100 * clamped / total;
if (chartOpts.colorScale.ranges.length > 0) {
const colorRange = chartOpts.colorScale.ranges;
colorRange.map((range) => {
if (val >= range.from && val <= range.to) {
color = range.color;
foreColor = range.foreColor ? range.foreColor : null;
min = range.from;
max = range.to;
const rTotal = Math.abs(max) + Math.abs(min);
percent = rTotal === 0 ? 0 : 100 * val / rTotal;
}
});
}
return {
color,
foreColor,
percent
};
}
/** @param {{ text?: any, x?: any, y?: any, i?: any, j?: any, colorProps?: any, fontSize?: any, series?: any }} opts */
calculateDataLabels({ text, x, y, i, j, colorProps, fontSize }) {
const w2 = this.w;
const dataLabelsConfig = w2.config.dataLabels;
const graphics = new Graphics(this.w);
const dataLabels = new DataLabels(this.w, this.ctx);
let elDataLabelsWrap = null;
if (dataLabelsConfig.enabled) {
elDataLabelsWrap = graphics.group({
class: "apexcharts-data-labels"
});
const offX = resolveDataLabelOffset(dataLabelsConfig.offsetX, w2, i, j);
const offY = resolveDataLabelOffset(dataLabelsConfig.offsetY, w2, i, j);
const dataLabelsX = x + offX;
const dataLabelsY = y + parseFloat(dataLabelsConfig.style.fontSize) / 3 + offY;
dataLabels.plotDataLabelsText({
x: dataLabelsX,
y: dataLabelsY,
text,
i,
j,
color: colorProps.foreColor,
parent: elDataLabelsWrap,
fontSize,
dataLabelsConfig
});
}
return elDataLabelsWrap;
}
}
class HeatMap {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
* @param {import('../types/internal').XYRatios} xyRatios
*/
constructor(w2, ctx, xyRatios) {
this.ctx = ctx;
this.w = w2;
this.xRatio = xyRatios.xRatio;
this.yRatio = xyRatios.yRatio;
this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation;
this.helpers = new TreemapHelpers(w2, ctx);
this.rectRadius = this.w.config.plotOptions.heatmap.radius;
this.strokeWidth = this.w.config.stroke.show ? this.w.config.stroke.width : 0;
}
/**
* @param {any[]} series
*/
draw(series) {
var _a, _b;
const w2 = this.w;
const graphics = new Graphics(this.w, this.ctx);
const emit = seriesEmitter(this.ctx, graphics);
const useCanvas = emit !== graphics && typeof emit.drawRectCell === "function";
const ret = graphics.group({
class: "apexcharts-heatmap"
});
ret.attr("clip-path", `url(#gridRectMask${w2.globals.cuid})`);
const xDivision = w2.layout.gridWidth / w2.globals.dataPoints;
const yDivision = w2.layout.gridHeight / w2.seriesData.series.length;
const isContinuousX = (w2.config.xaxis.type === "numeric" || w2.config.xaxis.type === "datetime") && w2.axisFlags.isXNumeric && this.xRatio > 0;
let binPx = xDivision;
if (isContinuousX) {
const diff = w2.globals.minXDiff;
binPx = Number.isFinite(diff) && diff > 0 ? diff / this.xRatio : xDivision;
}
const cellFillOpacity = Array.isArray(w2.config.fill.opacity) ? (_a = w2.config.fill.opacity[0]) != null ? _a : 1 : (_b = w2.config.fill.opacity) != null ? _b : 1;
let y1 = 0;
let rev = false;
this.negRange = this.helpers.checkColorRange();
const heatSeries = series.slice();
if (w2.config.yaxis[0].reversed) {
rev = true;
heatSeries.reverse();
}
for (let i = rev ? 0 : heatSeries.length - 1; rev ? i < heatSeries.length : i >= 0; rev ? i++ : i--) {
const elSeries = graphics.group({
class: `apexcharts-series apexcharts-heatmap-series`,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[i]),
rel: i + 1,
"data:realIndex": i
});
Series.addCollapsedClassToSeries(this.w, elSeries, i);
if (!useCanvas) {
graphics.setupEventDelegation(elSeries, ".apexcharts-heatmap-rect");
}
if (w2.config.chart.dropShadow.enabled) {
const shadow = w2.config.chart.dropShadow;
const filters = new Filters(this.w);
filters.dropShadow(elSeries, shadow, i);
}
let x1 = 0;
const shadeIntensity = w2.config.plotOptions.heatmap.shadeIntensity;
let j = 0;
for (let dIndex = 0; dIndex < w2.globals.dataPoints; dIndex++) {
if (!isContinuousX && w2.seriesData.seriesX.length && !w2.globals.allSeriesHasEqualX) {
if (w2.globals.minX + w2.globals.minXDiff * dIndex < w2.seriesData.seriesX[i][j]) {
x1 = x1 + xDivision;
continue;
}
}
if (j >= heatSeries[i].length) break;
const cellW = isContinuousX ? binPx : xDivision;
if (isContinuousX) {
const xVal = w2.seriesData.seriesX[i] ? w2.seriesData.seriesX[i][j] : null;
if (xVal == null || xVal !== xVal) {
j++;
continue;
}
x1 = (xVal - w2.globals.minX) / this.xRatio - binPx / 2;
}
const heatColor = this.helpers.getShadeColor(
w2.config.chart.type,
i,
j,
this.negRange
);
let color = heatColor.color;
const heatColorProps = heatColor.colorProps;
if (w2.config.fill.type === "image") {
const fill = new Fill(this.w);
color = fill.fillPath({
seriesNumber: i,
dataPointIndex: j,
opacity: (
/** @type {any} */
w2.globals.hasNegs ? heatColorProps.percent < 0 ? 1 - (1 + heatColorProps.percent / 100) : shadeIntensity + heatColorProps.percent / 100 : heatColorProps.percent / 100
),
patternID: Utils.randomId(),
width: w2.config.fill.image.width ? w2.config.fill.image.width : cellW,
height: w2.config.fill.image.height ? w2.config.fill.image.height : yDivision
});
}
const radius = this.rectRadius;
const stroke = w2.config.plotOptions.heatmap.useFillColorAsStroke ? color : w2.globals.stroke.colors[0];
if (useCanvas) {
emit.drawRectCell(x1, y1, cellW, yDivision, {
fill: color,
fillOpacity: cellFillOpacity,
stroke,
strokeWidth: this.strokeWidth,
radius,
seriesIndex: i,
dataPointIndex: j
});
} else {
const rect = graphics.drawRect(x1, y1, cellW, yDivision, radius);
rect.attr({
cx: x1,
cy: y1
});
rect.node.classList.add("apexcharts-heatmap-rect");
elSeries.add(rect);
rect.attr({
fill: color,
i,
index: i,
j,
val: series[i][j],
"stroke-width": this.strokeWidth,
stroke,
color
});
if (w2.config.chart.animations.enabled && !w2.globals.dataChanged) {
let speed = 1;
if (!w2.globals.resized) {
speed = w2.config.chart.animations.speed;
}
this.animateHeatMap(rect, x1, y1, cellW, yDivision, speed, i, j);
}
if (w2.globals.dataChanged) {
let speed = 1;
if (this.dynamicAnim.enabled && w2.globals.shouldAnimate) {
speed = this.dynamicAnim.speed;
let colorFrom = w2.globals.previousPaths[i] && w2.globals.previousPaths[i][j] && w2.globals.previousPaths[i][j].color;
if (!colorFrom) colorFrom = "rgba(255, 255, 255, 0)";
this.animateHeatColor(
rect,
Utils.isColorHex(colorFrom) ? colorFrom : Utils.rgb2hex(colorFrom),
Utils.isColorHex(color) ? color : Utils.rgb2hex(color),
speed
);
}
}
}
const formatter = w2.config.dataLabels.formatter;
const formattedText = formatter(w2.seriesData.series[i][j], {
value: w2.seriesData.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w: w2
});
const dataLabels = this.helpers.calculateDataLabels({
text: formattedText,
x: x1 + cellW / 2,
y: y1 + yDivision / 2,
i,
j,
colorProps: heatColorProps,
series: heatSeries
});
if (dataLabels !== null) {
elSeries.add(dataLabels);
}
if (!isContinuousX) x1 = x1 + xDivision;
j++;
}
y1 = y1 + yDivision;
ret.add(elSeries);
}
const yAxisScale = (
/** @type {any[]} */
w2.globals.yAxisScale[0].result.slice()
);
if (w2.config.yaxis[0].reversed) {
yAxisScale.unshift("");
} else {
yAxisScale.push("");
}
w2.globals.yAxisScale[0].result = yAxisScale;
return ret;
}
/**
* @param {any} el
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {number} speed
* @param {number} [row] - series index (heatmap row)
* @param {number} [col] - data point index (heatmap column)
*/
animateHeatMap(el, x, y, width, height, speed, row = 0, col = 0) {
const w2 = this.w;
const animations = new Animations(this.w);
const animCfg = w2.config.chart.animations;
const gradCfg = animCfg.animateGradually;
const staggerEnabled = gradCfg && gradCfg.enabled !== false;
let delay = 0;
if (staggerEnabled) {
const seriesCount = (w2.seriesData.series || []).length || 1;
const pointsCount = w2.globals.dataPoints || 1;
const maxDiag = seriesCount + pointsCount - 2;
const baseDelay = Math.min(
gradCfg.delay || 0,
speed * 0.5 / Math.max(1, maxDiag)
);
delay = computeStagger({
style: "diagonal",
index: col,
row,
col,
baseDelay
});
}
animations.animateRect(
el,
{
x: x + width / 2,
y: y + height / 2,
width: 0,
height: 0
},
{
x,
y,
width,
height
},
speed,
() => {
animations.animationCompleted(el);
},
delay
);
}
/**
* @param {any} el
* @param {string} colorFrom
* @param {string} colorTo
* @param {number} speed
*/
animateHeatColor(el, colorFrom, colorTo, speed) {
el.attr({
fill: colorFrom
}).animate(speed).attr({
fill: colorTo
});
}
}
class Helpers4 {
/**
* @param {import('../../../charts/Line').default} lineCtx
*/
constructor(lineCtx) {
this.w = lineCtx.w;
this.lineCtx = lineCtx;
}
/**
* @param {number} i
* @param {any[]} series
*/
sameValueSeriesFix(i, series) {
const w2 = this.w;
if (w2.config.fill.type === "gradient" || w2.config.fill.type[i] === "gradient") {
const coreUtils = new CoreUtils(this.lineCtx.w);
if (coreUtils.seriesHaveSameValues(i)) {
const gSeries = series[i].slice();
gSeries[gSeries.length - 1] = gSeries[gSeries.length - 1] + 1e-6;
series[i] = gSeries;
}
}
return series;
}
/** @param {{series: any, realIndex: any, x: any, y: any, i: any, j: any, prevY: any}} opts */
calculatePoints({ series, realIndex, x, y, i, j, prevY }) {
const w2 = this.w;
const ptX = [];
const ptY = [];
let xPT1st = this.lineCtx.categoryAxisCorrection + w2.config.markers.offsetX;
if (w2.axisFlags.isXNumeric) {
xPT1st = (w2.seriesData.seriesX[realIndex][0] - w2.globals.minX) / this.lineCtx.xRatio + w2.config.markers.offsetX;
}
if (j === 0) {
ptX.push(xPT1st);
ptY.push(
Utils.isNumber(series[i][0]) ? prevY + w2.config.markers.offsetY : null
);
}
ptX.push(x + w2.config.markers.offsetX);
ptY.push(
Utils.isNumber(series[i][j + 1]) ? y + w2.config.markers.offsetY : null
);
return {
x: ptX,
y: ptY
};
}
/** @param {{pathFromLine: any, pathFromArea: any, realIndex: any}} opts */
checkPreviousPaths({ pathFromLine, pathFromArea, realIndex }) {
const w2 = this.w;
for (let pp = 0; pp < w2.globals.previousPaths.length; pp++) {
const gpp = w2.globals.previousPaths[pp];
if ((gpp.type === "line" || gpp.type === "area") && gpp.paths.length > 0 && parseInt(gpp.realIndex, 10) === parseInt(realIndex, 10)) {
if (gpp.type === "line") {
this.lineCtx.appendPathFrom = false;
pathFromLine = w2.globals.previousPaths[pp].paths[0].d;
} else if (gpp.type === "area") {
this.lineCtx.appendPathFrom = false;
pathFromArea = w2.globals.previousPaths[pp].paths[0].d;
if (w2.config.stroke.show && w2.globals.previousPaths[pp].paths[1]) {
pathFromLine = w2.globals.previousPaths[pp].paths[1].d;
}
}
}
}
return {
pathFromLine,
pathFromArea
};
}
/** @param {{i: any, realIndex: any, series: any, prevY: any, lineYPosition: any, translationsIndex: any}} opts */
determineFirstPrevY({
i,
realIndex,
series,
prevY,
lineYPosition,
translationsIndex
}) {
var _a, _b, _c;
const w2 = this.w;
const stackSeries = w2.config.chart.stacked && !w2.globals.comboCharts || w2.config.chart.stacked && w2.globals.comboCharts && (!this.w.config.chart.stackOnlyBar || /** @type {any} */
((_a = this.w.config.series[realIndex]) == null ? void 0 : _a.type) === "bar" || /** @type {any} */
((_b = this.w.config.series[realIndex]) == null ? void 0 : _b.type) === "column");
if (typeof ((_c = series[i]) == null ? void 0 : _c[0]) !== "undefined") {
if (stackSeries) {
if (i > 0) {
const top = this.lineCtx.stackTopAt(realIndex, 0);
lineYPosition = top === void 0 ? this.lineCtx.zeroY : top;
} else {
lineYPosition = this.lineCtx.zeroY;
}
} else {
lineYPosition = this.lineCtx.zeroY;
}
prevY = lineYPosition - series[i][0] / this.lineCtx.yRatio[translationsIndex] + (this.lineCtx.isReversed ? series[i][0] / this.lineCtx.yRatio[translationsIndex] : 0) * 2;
} else {
if (stackSeries && i > 0 && typeof series[i][0] === "undefined") {
for (let s = i - 1; s >= 0; s--) {
if (series[s][0] !== null && typeof series[s][0] !== "undefined") {
lineYPosition = this.lineCtx.prevSeriesY[s][0];
prevY = lineYPosition;
break;
}
}
}
}
return {
prevY,
lineYPosition
};
}
}
function detectStreamScroll(w2, realIndex, newXPixels, newYPixels) {
var _a;
const gl = w2.globals;
const frame = gl.prevStreamFrame;
if (!frame || !gl.dataChanged || !((_a = w2.axisFlags) == null ? void 0 : _a.isXNumeric)) return null;
const oldX = frame.seriesX[realIndex];
const oldY = frame.seriesY[realIndex];
const newX = w2.seriesData.seriesX[realIndex];
const newY = w2.seriesData.series[realIndex];
if (!oldX || !oldY || !newX || !newY) return null;
if (oldX.length < 3 || newX.length < 3) return null;
let k = -1;
for (let i = 0; i < oldX.length; i++) {
if (oldX[i] === newX[0]) {
k = i;
break;
}
}
if (k === -1) return null;
const overlap = Math.min(oldX.length - k, newX.length);
if (overlap < 2) return null;
const appended = newX.length - overlap;
if (k === 0 && appended === 0) return null;
for (let i = 0; i < overlap; i++) {
if (oldX[k + i] !== newX[i]) return null;
const oy = oldY[k + i];
const ny = newY[i];
if (oy !== ny && !(oy == null && ny == null)) return null;
}
const oldXP = frame.xPixels[realIndex];
const oldYP = frame.yPixels[realIndex];
if (!oldXP || !oldYP) return null;
let a = -1;
let b2 = -1;
for (let i = 0; i < overlap; i++) {
if (oldXP[k + i] == null || oldYP[k + i] == null || newXPixels[i] == null || newYPixels[i] == null) {
continue;
}
if (a === -1) a = i;
b2 = i;
}
if (a === -1 || b2 <= a) return null;
const nxA = (
/** @type {number} */
newXPixels[a]
);
const nxB = (
/** @type {number} */
newXPixels[b2]
);
const oxA = (
/** @type {number} */
oldXP[k + a]
);
const oxB = (
/** @type {number} */
oldXP[k + b2]
);
if (Math.abs(nxB - nxA) < 1e-6) return null;
const ax = (oxB - oxA) / (nxB - nxA);
const bx = oxA - ax * nxA;
if (!isFinite(ax) || !isFinite(bx)) return null;
if (Math.abs(ax - 1) > 0.02) return null;
if (Math.abs(bx) < 0.5) return null;
let yLo = a;
let yHi = a;
for (let i = a; i <= b2; i++) {
const ny = newYPixels[i];
if (ny == null || oldYP[k + i] == null) continue;
if (ny < /** @type {number} */
newYPixels[yLo]) yLo = i;
if (ny > /** @type {number} */
newYPixels[yHi]) yHi = i;
}
let ay = 1;
let by = 0;
const nyLo = (
/** @type {number} */
newYPixels[yLo]
);
const nyHi = (
/** @type {number} */
newYPixels[yHi]
);
if (Math.abs(nyHi - nyLo) > 1e-6) {
ay = /** @type {number} */
(oldYP[k + yHi] - /** @type {number} */
oldYP[k + yLo]) / (nyHi - nyLo);
by = /** @type {number} */
oldYP[k + yLo] - ay * nyLo;
} else {
by = /** @type {number} */
oldYP[k + yLo] - nyLo;
}
if (!isFinite(ay) || !isFinite(by) || ay < 0.2 || ay > 5) return null;
const m2 = Math.floor((a + b2) / 2);
if (m2 !== a && m2 !== b2 && newXPixels[m2] != null && oldXP[k + m2] != null) {
const predX = ax * /** @type {number} */
newXPixels[m2] + bx;
if (Math.abs(predX - /** @type {number} */
oldXP[k + m2]) > 1.5) {
return null;
}
if (newYPixels[m2] != null && oldYP[k + m2] != null) {
const predY = ay * /** @type {number} */
newYPixels[m2] + by;
if (Math.abs(predY - /** @type {number} */
oldYP[k + m2]) > 1.5) {
return null;
}
}
}
gl.streamScrolled = true;
return { ax, bx, ay, by };
}
const NUM_RE = /[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi;
function projectPathToPrevFrame(d, t) {
const { ax, bx, ay, by } = t;
const out = [];
const re = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
let match;
while ((match = re.exec(d)) !== null) {
const cmd = match[1].toUpperCase();
const nums = (match[2].match(NUM_RE) || []).map(parseFloat);
if (cmd === "Z") {
out.push("z");
continue;
}
if (cmd === "H") {
for (const x of nums) out.push(`H ${ax * x + bx}`);
continue;
}
if (cmd === "V") {
for (const y of nums) out.push(`V ${ay * y + by}`);
continue;
}
if (cmd === "A") {
for (let i = 0; i + 6 < nums.length; i += 7) {
out.push(
`A ${nums[i]} ${nums[i + 1]} ${nums[i + 2]} ${nums[i + 3]} ${nums[i + 4]} ${ax * nums[i + 5] + bx} ${ay * nums[i + 6] + by}`
);
}
continue;
}
const coords = [];
for (let i = 0; i + 1 < nums.length; i += 2) {
coords.push(`${ax * nums[i] + bx} ${ay * nums[i + 1] + by}`);
}
if (coords.length) out.push(`${cmd} ${coords.join(" ")}`);
}
return out.join(" ");
}
class Line {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
* @param {import('../types/internal').XYRatios} xyRatios
* @param {boolean} isPointsChart
*/
constructor(w2, ctx, xyRatios, isPointsChart) {
this.ctx = ctx;
this.w = w2;
this.xyRatios = xyRatios;
this.xRatio = 0;
this.yRatio = [];
this.zRatio = 0;
this.baseLineY = [];
this.pointsChart = !(this.w.config.chart.type !== "bubble" && this.w.config.chart.type !== "scatter") || isPointsChart;
this.scatter = new Scatter(this.w, this.ctx);
this.noNegatives = this.w.globals.minX === Number.MAX_VALUE;
this.lineHelpers = new Helpers4(this);
this.markers = new Markers(this.w, this.ctx);
this.prevSeriesY = [];
this.prevSeriesYByX = /* @__PURE__ */ new Map();
this.categoryAxisCorrection = 0;
this.yaxisIndex = 0;
this.xDivision = 0;
this.zeroY = 0;
this.areaBottomY = 0;
this.strokeWidth = 0;
this.isReversed = false;
this.appendPathFrom = false;
this.elSeries = null;
this.elPointsMain = null;
this.elDataLabelsWrap = null;
this._elLastPointsWrap = null;
}
/**
* @param {any[]} series
* @param {string} ctype
* @param {number} seriesIndex
* @param {any} seriesRangeEnd
*/
draw(series, ctype, seriesIndex, seriesRangeEnd) {
var _a;
const w2 = this.w;
const graphics = new Graphics(this.w);
const type = w2.globals.comboCharts ? ctype : w2.config.chart.type;
const ret = graphics.group({
class: `apexcharts-${type}-series apexcharts-plot-series`
});
const coreUtils = new CoreUtils(this.w);
this.yRatio = this.xyRatios.yRatio;
this.zRatio = this.xyRatios.zRatio;
this.xRatio = this.xyRatios.xRatio;
this.baseLineY = this.xyRatios.baseLineY;
series = coreUtils.getLogSeries(series);
this.yRatio = coreUtils.getLogYRatios(this.yRatio);
this.prevSeriesY = [];
this.prevSeriesYByX = /* @__PURE__ */ new Map();
const allSeries = [];
for (let i = 0; i < series.length; i++) {
series = this.lineHelpers.sameValueSeriesFix(i, series);
const realIndex = w2.globals.comboCharts ? (
/** @type {any} */
seriesIndex[i]
) : i;
const translationsIndex = this.yRatio.length > 1 ? realIndex : 0;
this._initSerieVariables(series, i, realIndex);
const yArrj = [];
const y2Arrj = [];
const xArrj = [];
let x = w2.globals.padHorizontal + this.categoryAxisCorrection;
const y = 1;
const linePaths = [];
const areaPaths = [];
Series.addCollapsedClassToSeries(this.w, this.elSeries, realIndex);
if (w2.axisFlags.isXNumeric && w2.seriesData.seriesX.length > 0) {
x = (w2.seriesData.seriesX[realIndex][0] - w2.globals.minX) / this.xRatio;
}
xArrj.push(x);
const pX = x;
let pY2;
const prevX = pX;
let prevY = this.zeroY;
let prevY2 = this.zeroY;
const lineYPosition = 0;
const firstPrevY = this.lineHelpers.determineFirstPrevY({
i,
realIndex,
series,
prevY,
lineYPosition,
translationsIndex
});
prevY = firstPrevY.prevY;
if (w2.config.stroke.curve === "monotoneCubic" && series[i][0] === null) {
yArrj.push(null);
} else {
yArrj.push(prevY);
}
const pY = prevY;
let firstPrevY2;
if (type === "rangeArea") {
firstPrevY2 = this.lineHelpers.determineFirstPrevY({
i,
realIndex,
series: seriesRangeEnd,
prevY: prevY2,
lineYPosition,
translationsIndex
});
prevY2 = firstPrevY2.prevY;
pY2 = prevY2;
y2Arrj.push(yArrj[0] !== null ? prevY2 : null);
}
const pathsFrom = this._calculatePathsFrom({
type,
series,
i,
realIndex,
translationsIndex,
prevX,
prevY,
prevY2
});
const rYArrj = [yArrj[0]];
const rY2Arrj = [y2Arrj[0]];
const iteratingOpts = {
type,
series,
realIndex,
translationsIndex,
i,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
seriesIndex,
lineYPosition,
xArrj,
yArrj,
y2Arrj,
seriesRangeEnd
};
const paths = this._iterateOverDataPoints(__spreadProps(__spreadValues({}, iteratingOpts), {
iterations: type === "rangeArea" ? series[i].length - 1 : void 0,
isRangeStart: true
}));
if (type === "rangeArea") {
const pathsFrom2 = this._calculatePathsFrom({
series: seriesRangeEnd,
i,
realIndex,
prevX,
prevY: prevY2
});
const rangePaths = this._iterateOverDataPoints(__spreadProps(__spreadValues({}, iteratingOpts), {
series: seriesRangeEnd,
xArrj: [x],
yArrj: rYArrj,
y2Arrj: rY2Arrj,
pY: pY2,
areaPaths: paths.areaPaths,
pathsFrom: pathsFrom2,
iterations: seriesRangeEnd[i].length - 1,
isRangeStart: false
}));
const segments = paths.linePaths.length / 2;
for (let s = 0; s < segments; s++) {
paths.linePaths[s] = rangePaths.linePaths[s + segments] + paths.linePaths[s];
}
paths.linePaths.splice(segments);
paths.pathFromLine = rangePaths.pathFromLine + paths.pathFromLine;
} else if (!/z\s*$/i.test(paths.pathFromArea)) {
paths.pathFromArea += "z";
}
this._handlePaths({ type, realIndex, i, paths });
this.markers.flushBatch(this.elPointsMain, realIndex);
this.elSeries.add(this.elPointsMain);
this.elSeries.add(this.elDataLabelsWrap);
allSeries.push(this.elSeries);
}
if (typeof /** @type {Record<string,any>} */
((_a = w2.config.series[0]) == null ? void 0 : _a.zIndex) !== "undefined") {
allSeries.sort(
(a, b2) => Number(a.node.getAttribute("zIndex")) - Number(b2.node.getAttribute("zIndex"))
);
}
if (w2.config.chart.stacked) {
for (let s = allSeries.length - 1; s >= 0; s--) {
ret.add(allSeries[s]);
}
} else {
for (let s = 0; s < allSeries.length; s++) {
ret.add(allSeries[s]);
}
}
return ret;
}
/**
* @param {any[]} series
* @param {number} i
* @param {number} realIndex
*/
_initSerieVariables(series, i, realIndex) {
const w2 = this.w;
const graphics = new Graphics(this.w);
this.xDivision = w2.layout.gridWidth / (w2.globals.dataPoints - (w2.config.xaxis.tickPlacement === "on" ? 1 : 0));
this.strokeWidth = Array.isArray(w2.config.stroke.width) ? w2.config.stroke.width[realIndex] : w2.config.stroke.width;
let translationsIndex = 0;
if (this.yRatio.length > 1) {
this.yaxisIndex = w2.globals.seriesYAxisReverseMap[realIndex];
translationsIndex = realIndex;
}
this.isReversed = w2.config.yaxis[this.yaxisIndex] && w2.config.yaxis[this.yaxisIndex].reversed;
this.zeroY = w2.layout.gridHeight - this.baseLineY[translationsIndex] - (this.isReversed ? w2.layout.gridHeight : 0) + (this.isReversed ? this.baseLineY[translationsIndex] * 2 : 0);
this.areaBottomY = this.zeroY;
if (this.zeroY > w2.layout.gridHeight || w2.config.plotOptions.area.fillTo === "end") {
this.areaBottomY = w2.layout.gridHeight;
}
this.categoryAxisCorrection = this.xDivision / 2;
const seriesItem = (
/** @type {Record<string,any>} */
w2.config.series[realIndex]
);
this.elSeries = graphics.group({
class: `apexcharts-series`,
zIndex: typeof seriesItem.zIndex !== "undefined" ? seriesItem.zIndex : realIndex,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[realIndex])
});
this.elPointsMain = graphics.group({
class: "apexcharts-series-markers-wrap",
"data:realIndex": realIndex
});
this.markers.resetSeriesWrapCache();
this._elLastPointsWrap = null;
if (w2.globals.hasNullValues) {
const firstPoint = this.markers.plotChartMarkers({
pointsPos: {
x: [0],
y: [w2.layout.gridHeight + w2.globals.markers.largestSize]
},
seriesIndex: i,
j: 0,
pSize: 0.1,
alwaysDrawMarker: true,
isVirtualPoint: true
});
if (firstPoint !== null) {
this.elPointsMain.add(firstPoint);
}
}
this.elDataLabelsWrap = graphics.group({
class: "apexcharts-datalabels",
"data:realIndex": realIndex
});
const longestSeries = series[i].length === w2.globals.dataPoints;
this.elSeries.attr({
"data:longestSeries": longestSeries,
rel: i + 1,
"data:realIndex": realIndex
});
this.appendPathFrom = true;
}
/** @param {{ type?: any, series?: any, i?: any, realIndex?: any, translationsIndex?: any, prevX?: any, prevY?: any, prevY2?: any }} opts */
_calculatePathsFrom({
type,
series,
i,
realIndex,
translationsIndex,
prevX,
prevY,
prevY2
}) {
const w2 = this.w;
const graphics = new Graphics(this.w);
let linePath, areaPath, pathFromLine, pathFromArea;
if (series[i][0] === null) {
for (let s = 0; s < series[i].length; s++) {
if (series[i][s] !== null) {
prevX = this.xDivision * s;
prevY = this.zeroY - series[i][s] / this.yRatio[translationsIndex];
linePath = graphics.move(prevX, prevY);
areaPath = graphics.move(prevX, this.areaBottomY);
break;
}
}
} else {
linePath = graphics.move(prevX, prevY);
if (type === "rangeArea") {
linePath = graphics.move(prevX, prevY2) + graphics.line(prevX, prevY);
}
areaPath = graphics.move(prevX, this.areaBottomY) + graphics.line(prevX, prevY);
}
pathFromLine = graphics.move(0, this.areaBottomY) + graphics.line(0, this.areaBottomY);
pathFromArea = graphics.move(0, this.areaBottomY) + graphics.line(0, this.areaBottomY);
if (w2.globals.previousPaths.length > 0) {
const pathFrom = this.lineHelpers.checkPreviousPaths({
pathFromLine,
pathFromArea,
realIndex
});
pathFromLine = pathFrom.pathFromLine;
pathFromArea = pathFrom.pathFromArea;
}
return {
prevX,
prevY,
linePath,
areaPath,
pathFromLine,
pathFromArea
};
}
/**
* The identity a stacked baseline is looked up by.
*
* On a numeric or datetime axis each series carries its own x array, and two
* series' Nth points are not the same x when one of them is missing an entry.
* So the key is the x VALUE there. On a category axis every series is indexed
* against the shared category list, so the ordinal already IS the identity
* (and the pixel x is a running sum, unsafe to compare as a float).
*
* Returns undefined when this ordinal has no x, which happens on every series
* shorter than the longest one: the loop runs `dataPoints - 1` times for all
* of them. Those iterations must not write to or read from the map.
* @param {number} realIndex
* @param {number} ordinal
* @returns {any}
*/
_stackKey(realIndex, ordinal) {
if (!this.w.axisFlags.isXNumeric) return ordinal;
const xs = this.w.seriesData.seriesX[realIndex];
return xs ? xs[ordinal] : void 0;
}
/**
* Pixel y of the top of the stack at one point of the series being drawn, or
* undefined when nothing has been stacked there yet (so the caller starts
* from the axis baseline).
* @param {number} realIndex
* @param {number} ordinal
* @returns {number | undefined}
*/
stackTopAt(realIndex, ordinal) {
const key = this._stackKey(realIndex, ordinal);
if (key === void 0 || key === null) return void 0;
return this.prevSeriesYByX.get(key);
}
/**
* Fold a drawn series into the running stack top, so the next series can find
* its baseline by x (#4886).
*
* A point the series does not have simply leaves the previous top in place,
* which is the same thing as contributing 0 there. That is exactly what the
* workaround posted on the issue does by hand (pad every series onto the union
* of all x with zeros), and it is the behaviour the reporter expected.
*
* Collapsed series are skipped rather than folded in. Today a collapsed series
* renders a full-length yArrj sitting on the running baseline, so folding it
* would be a no-op anyway, but skipping states the intent and keeps this
* correct if that representation ever changes.
* @param {number} realIndex
* @param {any[]} yArrj
*/
_recordStackTops(realIndex, yArrj) {
const w2 = this.w;
if (!Array.isArray(yArrj)) return;
if (w2.globals.collapsedSeriesIndices.indexOf(realIndex) !== -1 || w2.globals.ancillaryCollapsedSeriesIndices.indexOf(realIndex) !== -1) {
return;
}
for (let j = 0; j < yArrj.length; j++) {
const key = this._stackKey(realIndex, j);
if (key === void 0 || key === null) continue;
const y = yArrj[j];
if (!Utils.isNumber(y)) continue;
this.prevSeriesYByX.set(key, y);
}
}
/** @param {{type: any, realIndex: any, i: any, paths: any}} opts */
_handlePaths({ type, realIndex, i, paths }) {
var _a, _b, _c, _d, _e, _f, _g;
const w2 = this.w;
const graphics = new Graphics(this.w);
const emit = seriesEmitter(this.ctx, graphics);
const fill = new Fill(this.w);
this.prevSeriesY.push(paths.yArrj);
this._recordStackTops(realIndex, paths.yArrj);
let streamScroll = null;
if ((type === "line" || type === "area") && w2.globals.dataChanged) {
streamScroll = detectStreamScroll(w2, realIndex, paths.xArrj, paths.yArrj);
}
let reconcile = null;
if (!streamScroll && (type === "line" || type === "area")) {
reconcile = reconcileSeriesPaths(w2, {
type,
realIndex,
pathFromLine: paths.pathFromLine,
pathFromArea: paths.pathFromArea,
linePaths: paths.linePaths,
areaPaths: paths.areaPaths
});
}
w2.globals.seriesXvalues[realIndex] = paths.xArrj;
w2.globals.seriesYvalues[realIndex] = paths.yArrj;
const forecast = w2.config.forecastDataPoints;
if (forecast.count > 0 && type !== "rangeArea") {
const forecastCutoff = w2.globals.seriesXvalues[realIndex][w2.globals.seriesXvalues[realIndex].length - forecast.count - 1];
const elForecastMask = graphics.drawRect(
forecastCutoff,
0,
w2.layout.gridWidth,
w2.layout.gridHeight,
0
);
w2.dom.elForecastMask.appendChild(elForecastMask.node);
const elNonForecastMask = graphics.drawRect(
0,
0,
forecastCutoff,
w2.layout.gridHeight,
0
);
w2.dom.elNonForecastMask.appendChild(elNonForecastMask.node);
}
if (!this.pointsChart) {
w2.globals.delayedElements.push({
el: this.elPointsMain.node,
index: realIndex
});
tweenSeriesMarkers(w2, {
elPointsMain: this.elPointsMain,
realIndex,
speed: w2.config.chart.animations.dynamicAnimation.speed
});
if (seriesJoin(w2, realIndex) && ((_a = this.elDataLabelsWrap) == null ? void 0 : _a.node)) {
this.elDataLabelsWrap.node.classList.add("apexcharts-element-hidden");
w2.globals.delayedElements.push({
el: this.elDataLabelsWrap.node,
holdUntilComplete: true
});
}
} else {
tweenSeriesMarkers(w2, {
elPointsMain: this.elPointsMain,
realIndex,
speed: w2.config.chart.animations.dynamicAnimation.speed
});
}
const defaultRenderedPathOptions = {
i,
realIndex,
animationDelay: i,
initialSpeed: w2.config.chart.animations.speed,
dataChangeSpeed: w2.config.chart.animations.dynamicAnimation.speed,
className: `apexcharts-${type}`
};
const numericXY = paths.numericXY;
const mergeSegments = type === "line" || type === "area";
const linePathsToDraw = mergeSegments && paths.linePaths.length > 1 ? [paths.linePaths.join(" ")] : paths.linePaths;
const areaPathsToDraw = mergeSegments && paths.areaPaths.length > 1 ? [paths.areaPaths.join(" ")] : paths.areaPaths;
if (type === "area") {
const pathFill = fill.fillPath({
seriesNumber: realIndex
});
for (let p2 = 0; p2 < areaPathsToDraw.length; p2++) {
const renderedPath = emit.renderPaths(__spreadProps(__spreadValues({}, defaultRenderedPathOptions), {
pathFrom: streamScroll ? projectPathToPrevFrame(paths.areaPaths[p2], streamScroll) : (_c = (_b = reconcile == null ? void 0 : reconcile.area) == null ? void 0 : _b.from) != null ? _c : paths.pathFromArea,
pathTo: areaPathsToDraw[p2],
pathToNumeric: numericXY ? {
xs: numericXY.xs,
ys: numericXY.ys,
closeY: numericXY.areaCloseY
} : void 0,
pathToInterp: (_d = reconcile == null ? void 0 : reconcile.area) == null ? void 0 : _d.toInterp,
scrollMorph: !!streamScroll,
stroke: "none",
strokeWidth: 0,
strokeLineCap: null,
fill: pathFill
}));
this.elSeries.add(renderedPath);
}
}
if (w2.config.stroke.show && !this.pointsChart) {
let lineFill = null;
if (type === "line") {
lineFill = fill.fillPath({
seriesNumber: realIndex,
i
});
} else {
if (w2.config.stroke.fill.type === "solid") {
lineFill = w2.globals.stroke.colors[realIndex];
} else {
const prevFill = w2.config.fill;
w2.config.fill = w2.config.stroke.fill;
lineFill = fill.fillPath({
seriesNumber: realIndex,
i
});
w2.config.fill = prevFill;
}
}
for (let p2 = 0; p2 < linePathsToDraw.length; p2++) {
let pathFill = lineFill;
if (type === "rangeArea") {
pathFill = fill.fillPath({
seriesNumber: realIndex
});
}
const linePathCommonOpts = __spreadProps(__spreadValues({}, defaultRenderedPathOptions), {
pathFrom: streamScroll ? projectPathToPrevFrame(paths.linePaths[p2], streamScroll) : (_f = (_e = reconcile == null ? void 0 : reconcile.line) == null ? void 0 : _e.from) != null ? _f : paths.pathFromLine,
pathTo: linePathsToDraw[p2],
pathToNumeric: numericXY ? { xs: numericXY.xs, ys: numericXY.ys } : void 0,
pathToInterp: (_g = reconcile == null ? void 0 : reconcile.line) == null ? void 0 : _g.toInterp,
scrollMorph: !!streamScroll,
stroke: lineFill,
strokeWidth: this.strokeWidth,
strokeLineCap: w2.config.stroke.lineCap,
fill: type === "rangeArea" ? pathFill : "none"
});
const renderedPath = emit.renderPaths(linePathCommonOpts);
this.elSeries.add(renderedPath);
renderedPath.attr("fill-rule", `evenodd`);
if (forecast.count > 0 && type !== "rangeArea") {
const renderedForecastPath = emit.renderPaths(linePathCommonOpts);
renderedForecastPath.node.setAttribute(
"stroke-dasharray",
forecast.dashArray
);
if (forecast.strokeWidth) {
renderedForecastPath.node.setAttribute(
"stroke-width",
forecast.strokeWidth
);
}
this.elSeries.add(renderedForecastPath);
renderedForecastPath.attr(
"clip-path",
`url(#forecastMask${w2.globals.cuid})`
);
renderedPath.attr(
"clip-path",
`url(#nonForecastMask${w2.globals.cuid})`
);
}
}
}
}
_iterateOverDataPoints({
type,
series,
iterations,
realIndex,
translationsIndex,
i,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
seriesIndex,
lineYPosition,
xArrj,
yArrj,
y2Arrj,
isRangeStart,
seriesRangeEnd
}) {
var _a, _b;
const w2 = this.w;
const graphics = new Graphics(this.w);
const yRatio = this.yRatio;
let { prevY, linePath, areaPath, pathFromLine, pathFromArea } = pathsFrom;
const minY = Utils.isNumber(w2.globals.minYArr[realIndex]) ? w2.globals.minYArr[realIndex] : w2.globals.minY;
if (!iterations) {
iterations = w2.globals.dataPoints > 1 ? w2.globals.dataPoints - 1 : w2.globals.dataPoints;
}
const getY = (_y, lineYPos) => {
return lineYPos - _y / yRatio[translationsIndex] + (this.isReversed ? _y / yRatio[translationsIndex] : 0) * 2;
};
let y2 = y;
const stackSeries = w2.config.chart.stacked && !w2.globals.comboCharts || w2.config.chart.stacked && w2.globals.comboCharts && (!this.w.config.chart.stackOnlyBar || /** @type {Record<string,any>} */
((_a = this.w.config.series[realIndex]) == null ? void 0 : _a.type) === "bar" || /** @type {Record<string,any>} */
((_b = this.w.config.series[realIndex]) == null ? void 0 : _b.type) === "column");
let curve = w2.config.stroke.curve;
if (Array.isArray(curve)) {
if (Array.isArray(seriesIndex)) {
curve = curve[seriesIndex[i]];
} else {
curve = curve[i];
}
}
let pathState = 0;
let segmentStartX;
const jitterPx = this.pointsChart ? this._scatterJitterPx(realIndex) : null;
if (curve === "straight" && !this.pointsChart) {
const fast = this._fastStraightPath({
type,
series,
i,
realIndex,
translationsIndex,
iterations,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
xArrj,
yArrj,
y2Arrj,
stackSeries
});
if (fast) return fast;
}
for (let j = 0; j < iterations; j++) {
if (series[i].length === 0) break;
const isNull = typeof series[i][j + 1] === "undefined" || series[i][j + 1] === null;
if (w2.axisFlags.isXNumeric) {
let sX = w2.seriesData.seriesX[realIndex][j + 1];
if (typeof w2.seriesData.seriesX[realIndex][j + 1] === "undefined") {
sX = w2.seriesData.seriesX[realIndex][iterations - 1];
}
x = (sX - w2.globals.minX) / this.xRatio;
} else {
x = x + this.xDivision;
}
if (stackSeries) {
if (i > 0 && w2.globals.collapsedSeries.length < w2.config.series.length - 1) {
const prevIndex = (pi) => {
var _a2;
for (let pii = pi; pii >= 0; pii--) {
const ri = (_a2 = seriesIndex == null ? void 0 : seriesIndex[pii]) != null ? _a2 : pii;
if (w2.globals.collapsedSeriesIndices.indexOf(ri) === -1 && w2.globals.ancillaryCollapsedSeriesIndices.indexOf(ri) === -1) {
return pii;
}
}
return -1;
};
const pIdx = prevIndex(i - 1);
if (pIdx < 0) {
lineYPosition = this.zeroY;
} else {
const top = this.stackTopAt(realIndex, j + 1);
lineYPosition = top === void 0 ? this.zeroY : top;
}
} else {
lineYPosition = this.zeroY;
}
} else {
lineYPosition = this.zeroY;
}
if (isNull) {
y = getY(minY, lineYPosition);
} else {
y = getY(series[i][j + 1], lineYPosition);
if (type === "rangeArea") {
y2 = getY(seriesRangeEnd[i][j + 1], lineYPosition);
}
}
let xj = x;
let yj = y;
if (jitterPx) {
const seed = realIndex * 100003 + (j + 1);
if (jitterPx.x) xj = x + (hash01(seed * 7919 + 13) - 0.5) * 2 * jitterPx.x;
if (jitterPx.y) yj = y + (hash01(seed * 6271 + 97) - 0.5) * 2 * jitterPx.y;
}
xArrj.push(series[i][j + 1] === null ? null : xj);
if (isNull && (w2.config.stroke.curve === "smooth" || w2.config.stroke.curve === "monotoneCubic")) {
yArrj.push(null);
y2Arrj.push(null);
} else {
yArrj.push(yj);
y2Arrj.push(y2);
}
const pointsPos = this.lineHelpers.calculatePoints({
series,
x: xj,
y: yj,
realIndex,
i,
j,
prevY
});
const calculatedPaths = this._createPaths({
type,
series,
i,
j,
x,
y,
y2,
xArrj,
yArrj,
y2Arrj,
pX,
pY,
pathState,
segmentStartX,
linePath,
areaPath,
linePaths,
areaPaths,
curve,
isRangeStart
});
areaPaths = calculatedPaths.areaPaths;
linePaths = calculatedPaths.linePaths;
pX = calculatedPaths.pX;
pY = calculatedPaths.pY;
pathState = calculatedPaths.pathState;
segmentStartX = calculatedPaths.segmentStartX;
areaPath = calculatedPaths.areaPath;
linePath = calculatedPaths.linePath;
if (this.appendPathFrom && !w2.globals.hasNullValues && !(curve === "monotoneCubic" && type === "rangeArea")) {
pathFromLine += graphics.line(x, this.areaBottomY);
pathFromArea += graphics.line(x, this.areaBottomY);
}
this.handleNullDataPoints(series, pointsPos, i, j, realIndex);
this._handleMarkersAndLabels({
type,
pointsPos,
i,
j,
realIndex,
isRangeStart
});
}
return {
yArrj,
xArrj,
pathFromArea,
areaPaths,
pathFromLine,
linePaths,
linePath,
areaPath
};
}
/** @param {{type: any, pointsPos: any, isRangeStart: any, i: any, j: any, realIndex: any}} opts */
_handleMarkersAndLabels({ type, pointsPos, isRangeStart, i, j, realIndex }) {
const w2 = this.w;
const dataLabels = new DataLabels(this.w, this.ctx);
if (!this.pointsChart) {
const useProgressive = !w2.globals.dataChanged && !w2.globals.resized && !w2.globals.markers.batched;
if (!useProgressive && w2.seriesData.series[i].length > 1) {
this.elPointsMain.node.classList.add("apexcharts-element-hidden");
}
const elPointsWrap = this.markers.plotChartMarkers({
pointsPos,
seriesIndex: realIndex,
j: j + 1
});
if (elPointsWrap !== null && elPointsWrap !== this._elLastPointsWrap) {
this.elPointsMain.add(elPointsWrap);
this._elLastPointsWrap = elPointsWrap;
}
} else {
this.scatter.draw(this.elSeries, j, {
realIndex,
pointsPos,
zRatio: this.zRatio,
elParent: this.elPointsMain
});
}
const drawnLabels = dataLabels.drawDataLabel({
type,
isRangeStart,
pos: pointsPos,
i: realIndex,
j: j + 1
});
if (drawnLabels !== null) {
this.elDataLabelsWrap.add(drawnLabels);
}
}
/**
* Max scatter-jitter offsets in pixels for this series, or null when jitter is
* off. The config offsets are in axis units (x: 1 = one category step / x-data
* unit, y: 1 = one y-data unit); convert each to pixels using the chart's
* ratios. The actual per-point offset is a deterministic fraction of these
* (see Scatter.drawPoint).
* @param {number} realIndex
* @returns {{ x: number, y: number } | null}
*/
_scatterJitterPx(realIndex) {
var _a;
const w2 = this.w;
const jt = (_a = w2.config.plotOptions.scatter) == null ? void 0 : _a.jitter;
if (!jt || !jt.enabled || !jt.x && !jt.y) return null;
const xUnitPx = w2.axisFlags.isXNumeric && this.xRatio ? 1 / this.xRatio : this.xDivision;
const ti = this.yRatio.length > 1 ? realIndex : 0;
const yUnitPx = this.yRatio[ti] ? 1 / this.yRatio[ti] : 0;
return {
x: (jt.x || 0) * xUnitPx,
y: (jt.y || 0) * yUnitPx
};
}
/**
* Numeric geometry fast path for plain straight line/area series (the
* render-2026 perf work). Eligibility is strict: everything the per-point
* slow loop can do beyond plain geometry (null gaps, markers, data labels,
* discrete markers, stacking, combos, range areas) bails to the state
* machine. When eligible it produces the SAME outputs as the slow loop
* (byte-identical d strings via join, the same xArrj/yArrj/y2Arrj
* pushes, and the same pointsArray tooltip cache) in one tight loop.
* In canvas mode it additionally emits typed-array coordinates so the
* renderer can paint via moveTo/lineTo without a Path2D d-string parse.
*
* @param {{type: any, series: any, i: number, realIndex: number,
* translationsIndex: number, iterations: number, x: number, y: number,
* pX: number, pY: number, pathsFrom: any, linePaths: any[],
* areaPaths: any[], xArrj: any[], yArrj: any[], y2Arrj: any[],
* stackSeries: boolean}} opts
* @returns {any} the _iterateOverDataPoints result, or null when ineligible
*/
_fastStraightPath({
type,
series,
i,
realIndex,
translationsIndex,
iterations,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
xArrj,
yArrj,
y2Arrj,
stackSeries
}) {
const w2 = this.w;
if (type !== "line" && type !== "area") return null;
if (w2.globals.comboCharts || stackSeries) return null;
if (w2.config.dataLabels.enabled) return null;
if (w2.config.markers.discrete.length) return null;
if (w2.globals.markers.size[realIndex] > 0) return null;
const s = series[i];
const n = s.length;
if (!iterations || n < 2 || n - 1 !== iterations) return null;
for (let k = 0; k <= iterations; k++) {
const v2 = s[k];
if (v2 === null || typeof v2 === "undefined") return null;
}
const isXNumeric = w2.axisFlags.isXNumeric;
const sx = isXNumeric ? w2.seriesData.seriesX[realIndex] : null;
if (isXNumeric && (!sx || sx.length < n)) return null;
const yR = this.yRatio[translationsIndex];
const isReversed = this.isReversed;
const zeroY = this.zeroY;
const bottomY = this.areaBottomY;
const xRatio = this.xRatio;
const minX = w2.globals.minX;
const xDivision = this.xDivision;
const offX = w2.config.markers.offsetX;
const offY = w2.config.markers.offsetY;
const appendFrom = this.appendPathFrom && !w2.globals.hasNullValues;
let { pathFromLine, pathFromArea } = pathsFrom;
const r = this.ctx && this.ctx.renderer;
const canvasMode = !!(r && r.kind && r.kind !== "svg");
const buildStrings = !canvasMode || w2.globals.dataChanged && !!w2.globals.prevStreamFrame;
const nxs = canvasMode ? new Float64Array(n) : null;
const nys = canvasMode ? new Float64Array(n) : null;
if (nxs && nys) {
nxs[0] = pX;
nys[0] = pY;
}
if (typeof w2.globals.pointsArray[realIndex] === "undefined") {
w2.globals.pointsArray[realIndex] = [];
}
const pts = w2.globals.pointsArray[realIndex];
const xPT1st = sx ? (sx[0] - minX) / xRatio + offX : this.categoryAxisCorrection + offX;
pts.push([xPT1st, pathsFrom.prevY + offY]);
const parts = buildStrings ? new Array(iterations + 1) : [];
if (buildStrings) parts[0] = "M " + pX + " " + pY;
const fromParts = buildStrings && appendFrom ? new Array(iterations) : null;
let xv = x;
let xj = pX;
let yj = pY;
for (let j = 0; j < iterations; j++) {
if (sx) {
xj = (sx[j + 1] - minX) / xRatio;
} else {
xv = xv + xDivision;
xj = xv;
}
const v2 = s[j + 1];
yj = zeroY - v2 / yR + (isReversed ? v2 / yR : 0) * 2;
xArrj.push(xj);
yArrj.push(yj);
y2Arrj.push(y);
pts.push([xj + offX, yj + offY]);
if (buildStrings) {
parts[j + 1] = " L " + xj + " " + yj;
if (fromParts) fromParts[j] = " L " + xj + " " + bottomY;
}
if (nxs && nys) {
nxs[j + 1] = xj;
nys[j + 1] = yj;
}
}
let linePath = "";
let areaPath = "";
if (buildStrings) {
linePath = parts.join("");
areaPath = linePath + " L " + xj + " " + bottomY + " L " + pX + " " + bottomY + "z";
}
if (fromParts) {
const fromAppend = fromParts.join("");
pathFromLine += fromAppend;
pathFromArea += fromAppend;
} else if (!buildStrings && appendFrom) {
pathFromLine += " L " + xj + " " + bottomY;
pathFromArea += " L " + xj + " " + bottomY;
}
linePaths.push(linePath);
areaPaths.push(areaPath);
return {
yArrj,
xArrj,
pathFromArea,
areaPaths,
pathFromLine,
linePaths,
linePath,
areaPath,
numericXY: nxs ? { xs: nxs, ys: nys, areaCloseY: bottomY } : void 0
};
}
/** @param {{type: any, series: any, i: any, j: any, x: any, y: any, xArrj: any, yArrj: any, y2: any, y2Arrj: any, pX: any, pY: any, pathState: any, segmentStartX: any, linePath: any, areaPath: any, linePaths: any, areaPaths: any, curve: any, isRangeStart: any}} opts */
_createPaths({
type,
series,
i,
j,
x,
y,
xArrj,
yArrj,
y2,
y2Arrj,
pX,
pY,
pathState,
segmentStartX,
linePath,
areaPath,
linePaths,
areaPaths,
curve,
isRangeStart
}) {
const graphics = new Graphics(this.w);
const areaBottomY = this.areaBottomY;
const rangeArea = type === "rangeArea";
const isLowerRangeAreaPath = type === "rangeArea" && isRangeStart;
switch (curve) {
case "monotoneCubic": {
const yAj = isRangeStart ? yArrj : y2Arrj;
const getSmoothInputs = (xArr, yArr) => {
return xArr.map((_, i2) => {
return [_, yArr[i2]];
}).filter((_) => _[1] !== null);
};
const getSegmentLengths = (yArr) => {
const segLens = [];
let count = 0;
yArr.forEach((_) => {
if (_ !== null) {
count++;
} else if (count > 0) {
segLens.push(count);
count = 0;
}
});
if (count > 0) {
segLens.push(count);
}
return segLens;
};
const getSegments = (yArr, points) => {
const segLens = getSegmentLengths(yArr);
const segments = [];
for (let i2 = 0, len = 0; i2 < segLens.length; len += segLens[i2++]) {
segments[i2] = spline.slice(points, len, len + segLens[i2]);
}
return segments;
};
switch (pathState) {
case 0:
if (yAj[j + 1] === null) {
break;
}
pathState = 1;
// falls through
case 1:
if (!(rangeArea ? xArrj.length === series[i].length : j === series[i].length - 2)) {
break;
}
// falls through
case 2: {
const _xAj = isRangeStart ? xArrj : xArrj.slice().reverse();
const _yAj = isRangeStart ? yAj : yAj.slice().reverse();
const smoothInputs = getSmoothInputs(_xAj, _yAj);
const points = smoothInputs.length > 1 ? spline.points(smoothInputs) : smoothInputs;
let smoothInputsLower = [];
if (rangeArea) {
if (isLowerRangeAreaPath) {
areaPaths = smoothInputs;
} else {
smoothInputsLower = areaPaths.reverse();
}
}
let segmentCount = 0;
let smoothInputsIndex = 0;
getSegments(_yAj, points).forEach((_) => {
segmentCount++;
const svgPoints = svgPath(_);
const _start = smoothInputsIndex;
smoothInputsIndex += _.length;
const _end = smoothInputsIndex - 1;
if (isLowerRangeAreaPath) {
linePath = graphics.move(
smoothInputs[_start][0],
smoothInputs[_start][1]
) + svgPoints;
} else if (rangeArea) {
linePath = graphics.move(
smoothInputsLower[_start][0],
smoothInputsLower[_start][1]
) + graphics.line(
smoothInputs[_start][0],
smoothInputs[_start][1]
) + svgPoints + graphics.line(
smoothInputsLower[_end][0],
smoothInputsLower[_end][1]
);
} else {
linePath = graphics.move(
smoothInputs[_start][0],
smoothInputs[_start][1]
) + svgPoints;
areaPath = linePath + graphics.line(smoothInputs[_end][0], areaBottomY) + graphics.line(smoothInputs[_start][0], areaBottomY) + "z";
areaPaths.push(areaPath);
}
linePaths.push(linePath);
});
if (rangeArea && segmentCount > 1 && !isLowerRangeAreaPath) {
const upperLinePaths = linePaths.slice(segmentCount).reverse();
linePaths.splice(segmentCount);
upperLinePaths.forEach(
(u) => linePaths.push(u)
);
}
pathState = 0;
break;
}
}
break;
}
case "smooth": {
const length = (x - pX) * 0.35;
if (series[i][j] === null) {
pathState = 0;
} else {
switch (pathState) {
case 0:
segmentStartX = pX;
if (isLowerRangeAreaPath) {
linePath = graphics.move(pX, y2Arrj[j]) + graphics.line(pX, pY);
} else {
linePath = graphics.move(pX, pY);
}
areaPath = graphics.move(pX, pY);
if (series[i][j + 1] === null || typeof series[i][j + 1] === "undefined") {
linePaths.push(linePath);
areaPaths.push(areaPath);
break;
}
pathState = 1;
if (j < series[i].length - 2) {
const p2 = graphics.curve(pX + length, pY, x - length, y, x, y);
linePath += p2;
areaPath += p2;
break;
}
// falls through
case 1:
if (series[i][j + 1] === null) {
if (isLowerRangeAreaPath) {
linePath += graphics.line(pX, y2);
} else {
linePath += graphics.move(pX, pY);
}
areaPath += graphics.line(pX, areaBottomY) + graphics.line(segmentStartX, areaBottomY) + "z";
linePaths.push(linePath);
areaPaths.push(areaPath);
pathState = -1;
} else {
const p2 = graphics.curve(pX + length, pY, x - length, y, x, y);
linePath += p2;
areaPath += p2;
if (j >= series[i].length - 2) {
if (isLowerRangeAreaPath) {
linePath += graphics.curve(x, y, x, y, x, y2) + graphics.move(x, y2);
}
areaPath += graphics.curve(x, y, x, y, x, areaBottomY) + graphics.line(segmentStartX, areaBottomY) + "z";
linePaths.push(linePath);
areaPaths.push(areaPath);
pathState = -1;
}
}
break;
}
}
pX = x;
pY = y;
break;
}
default: {
const pathToPoint = (curve2, x2, y3) => {
let path = "";
switch (curve2) {
case "stepline":
path = graphics.line(x2, null, "H") + graphics.line(null, y3, "V");
break;
case "linestep":
path = graphics.line(null, y3, "V") + graphics.line(x2, null, "H");
break;
case "straight":
path = graphics.line(x2, y3);
break;
}
return path;
};
if (series[i][j] === null) {
pathState = 0;
} else {
switch (pathState) {
case 0:
segmentStartX = pX;
if (isLowerRangeAreaPath) {
linePath = graphics.move(pX, y2Arrj[j]) + graphics.line(pX, pY);
} else {
linePath = graphics.move(pX, pY);
}
areaPath = graphics.move(pX, pY);
if (series[i][j + 1] === null || typeof series[i][j + 1] === "undefined") {
linePaths.push(linePath);
areaPaths.push(areaPath);
break;
}
pathState = 1;
if (j < series[i].length - 2) {
const p2 = pathToPoint(curve, x, y);
linePath += p2;
areaPath += p2;
break;
}
// falls through
case 1:
if (series[i][j + 1] === null) {
if (isLowerRangeAreaPath) {
linePath += graphics.line(pX, y2);
} else {
linePath += graphics.move(pX, pY);
}
areaPath += graphics.line(pX, areaBottomY) + graphics.line(segmentStartX, areaBottomY) + "z";
linePaths.push(linePath);
areaPaths.push(areaPath);
pathState = -1;
} else {
const p2 = pathToPoint(curve, x, y);
linePath += p2;
areaPath += p2;
if (j >= series[i].length - 2) {
if (isLowerRangeAreaPath) {
linePath += graphics.line(x, y2);
}
areaPath += graphics.line(x, areaBottomY) + graphics.line(segmentStartX, areaBottomY) + "z";
linePaths.push(linePath);
areaPaths.push(areaPath);
pathState = -1;
}
}
break;
}
}
pX = x;
pY = y;
break;
}
}
return {
linePaths,
areaPaths,
pX,
pY,
pathState,
segmentStartX,
linePath,
areaPath
};
}
/**
* @param {any[]} series
* @param {any} pointsPos
* @param {number} i
* @param {number} j
* @param {number} realIndex
*/
handleNullDataPoints(series, pointsPos, i, j, realIndex) {
const w2 = this.w;
if (series[i][j] === null && w2.config.markers.showNullDataPoints || series[i].length === 1) {
let pSize = this.strokeWidth - w2.config.markers.strokeWidth / 2;
if (!(pSize > 0)) {
pSize = 0;
}
const elPointsWrap = this.markers.plotChartMarkers({
pointsPos,
seriesIndex: realIndex,
j: j + 1,
pSize,
alwaysDrawMarker: true
});
if (elPointsWrap !== null) {
this.elPointsMain.add(elPointsWrap);
}
}
}
}
function drawOuterLabel(w2, spec) {
const {
lines,
lineHeight,
anchor,
elbow,
labelX,
labelY,
side,
connector,
style,
foreColor
} = spec;
const graphics = new Graphics(w2);
const group = graphics.group({
class: spec.groupClass || "apexcharts-outer-label-group"
});
if (connector.show) {
const d = `M ${anchor.x} ${anchor.y} L ${elbow.x} ${elbow.y} L ${labelX} ${labelY}`;
const line = graphics.drawPath({
d,
stroke: connector.color,
strokeWidth: connector.width,
fill: "none",
strokeLinecap: "round"
});
line.node.classList.add(spec.connectorClass || "apexcharts-outer-label-connector");
group.add(line);
}
const textX = side === "right" ? labelX + 4 : labelX - 4;
const n = lines.length;
const startY = labelY - (n - 1) * lineHeight / 2;
const elText = graphics.drawText({
x: textX,
y: startY,
text: n === 1 ? lines[0] : lines,
textAnchor: side === "right" ? "start" : "end",
fontSize: style.fontSize,
fontFamily: style.fontFamily,
fontWeight: style.fontWeight,
foreColor,
dominantBaseline: "central",
cssClass: spec.textClass || "apexcharts-outer-label"
});
if (n > 1) {
const tspans = elText.node.getElementsByTagName("tspan");
for (let li = 0; li < tspans.length; li++) {
tspans[li].setAttribute("x", `${textX}`);
tspans[li].setAttribute("dy", li === 0 ? "0" : `${lineHeight}`);
}
}
group.add(elText);
return group;
}
function measureLabelWidth(w2, labels, style = {}) {
const graphics = new Graphics(w2);
const fontSize = style.fontSize || "12px";
const px = parseFloat(fontSize) || 12;
let max = 0;
labels.forEach((text) => {
if (text == null || text === "") return;
const str = `${text}`;
const measured = graphics.getTextRects(str, fontSize, style.fontFamily, "").width;
max = Math.max(max, measured > 0 ? measured : str.length * px * 0.58);
});
return max;
}
function spaceOutLabels(items, minGap, maxY, minY) {
const col = items.slice().sort((a, b2) => a.idealY - b2.idealY);
col.forEach((l) => {
l.labelY = l.idealY;
});
for (let k = 1; k < col.length; k++) {
if (col[k].labelY - col[k - 1].labelY < minGap) {
col[k].labelY = col[k - 1].labelY + minGap;
}
}
const last = col[col.length - 1];
const overflow = last ? last.labelY - maxY : 0;
if (overflow > 0) {
for (let k = col.length - 1; k >= 0; k--) {
col[k].labelY -= overflow;
if (k < col.length - 1 && col[k + 1].labelY - col[k].labelY < minGap) {
col[k].labelY = col[k + 1].labelY - minGap;
}
}
}
if (minY != null && col.length && col[0].labelY < minY) {
const shift = minY - col[0].labelY;
for (let k = 0; k < col.length; k++) {
col[k].labelY += shift;
}
}
}
class CircularChartsHelpers {
/**
* @param {import('../../../types/internal').ChartStateW} w
*/
constructor(w2) {
this.w = w2;
}
/**
* @param {number} x
* @param {number} y
* @param {number} i
* @param {string | number} text
*/
drawYAxisTexts(x, y, i, text) {
const w2 = this.w;
const yaxisConfig = w2.config.yaxis[0];
const formatter = w2.formatters.yLabelFormatters[0];
const graphics = new Graphics(this.w);
const yaxisLabel = graphics.drawText({
x: x + yaxisConfig.labels.offsetX,
y: y + yaxisConfig.labels.offsetY,
text: formatter(text, i),
textAnchor: "middle",
fontSize: yaxisConfig.labels.style.fontSize,
fontFamily: yaxisConfig.labels.style.fontFamily,
foreColor: Array.isArray(yaxisConfig.labels.style.colors) ? yaxisConfig.labels.style.colors[i] : yaxisConfig.labels.style.colors
});
return yaxisLabel;
}
/**
* Widest rendered width among the given label strings. Used to reserve
* horizontal room for outer (name) labels so the pie can shrink to fit them.
* @param {string[]} labels
* @param {{ fontSize?: string, fontFamily?: string }} style
* @returns {number}
*/
getMaxLabelWidth(labels, { fontSize, fontFamily } = {}) {
const graphics = new Graphics(this.w);
let maxWidth = 0;
labels.forEach((text) => {
if (text === null || typeof text === "undefined" || text === "") return;
const rect = graphics.getTextRects(
`${text}`,
fontSize || "12px",
fontFamily,
""
);
maxWidth = Math.max(maxWidth, rect.width);
});
return maxWidth;
}
/**
* Draw a single outer (name) label: an optional leader line from the slice
* edge (anchor -> radial elbow -> label) plus the name text (one or more
* lines, e.g. name + percent). Geometry is computed by the caller (Pie.js)
* so it can run a de-overlap pass first. The text block is vertically
* centered on `labelY`, which is where the connector terminates.
* @param {{
* lines: string[],
* lineHeight: number,
* anchor: { x: number, y: number },
* elbow: { x: number, y: number },
* labelX: number,
* labelY: number,
* side: 'left' | 'right',
* connector: { show: boolean, width: number, color: string },
* style: { fontSize?: string, fontFamily?: string, fontWeight?: string | number },
* foreColor: string,
* }} opts
*/
drawExternalLabel(opts) {
return drawOuterLabel(this.w, __spreadProps(__spreadValues({}, opts), {
groupClass: "apexcharts-pie-name-label-group",
textClass: "apexcharts-pie-name-label",
connectorClass: "apexcharts-pie-label-connector"
}));
}
}
const D2R$1 = Math.PI / 180;
const R2D$1 = 180 / Math.PI;
function arcPoint(cx, cy, radius, deg) {
return {
x: cx + radius * Math.cos((deg - 90) * D2R$1),
y: cy + radius * Math.sin((deg - 90) * D2R$1)
};
}
const xy = (p2) => `${p2.x} ${p2.y}`;
function roundedDonutSegmentPath({ cx, cy, rIn, rOut, a0, a1, r, spanDeg }) {
const ptAt = (radius, deg) => arcPoint(cx, cy, radius, deg);
const degOut = r / rOut * R2D$1;
const degIn = r / rIn * R2D$1;
const oStart = ptAt(rOut, a0 + degOut);
const oEnd = ptAt(rOut, a1 - degOut);
const largeOut = spanDeg - 2 * degOut > 180 ? 1 : 0;
const ocEnd = ptAt(rOut, a1);
const rEndOut = ptAt(rOut - r, a1);
const ocStart = ptAt(rOut, a0);
const rStartOut = ptAt(rOut - r, a0);
const iEnd = ptAt(rIn, a1 - degIn);
const iStart = ptAt(rIn, a0 + degIn);
const largeIn = spanDeg - 2 * degIn > 180 ? 1 : 0;
const icEnd = ptAt(rIn, a1);
const rEndIn = ptAt(rIn + r, a1);
const icStart = ptAt(rIn, a0);
const rStartIn = ptAt(rIn + r, a0);
return [
"M",
xy(oStart),
"A",
rOut,
rOut,
0,
largeOut,
1,
xy(oEnd),
"Q",
xy(ocEnd),
xy(rEndOut),
"L",
xy(rEndIn),
"Q",
xy(icEnd),
xy(iEnd),
"A",
rIn,
rIn,
0,
largeIn,
0,
xy(iStart),
"Q",
xy(icStart),
xy(rStartIn),
"L",
xy(rStartOut),
"Q",
xy(ocStart),
xy(oStart),
"Z"
].join(" ");
}
function roundedPieSegmentPath({ cx, cy, rOut, a0, a1, r, spanDeg }) {
const ptAt = (radius, deg) => arcPoint(cx, cy, radius, deg);
const degOut = r / rOut * R2D$1;
const oStart = ptAt(rOut, a0 + degOut);
const oEnd = ptAt(rOut, a1 - degOut);
const largeOut = spanDeg - 2 * degOut > 180 ? 1 : 0;
const ocEnd = ptAt(rOut, a1);
const rEndOut = ptAt(rOut - r, a1);
const ocStart = ptAt(rOut, a0);
const rStartOut = ptAt(rOut - r, a0);
return [
"M",
xy(oStart),
"A",
rOut,
rOut,
0,
largeOut,
1,
xy(oEnd),
"Q",
xy(ocEnd),
xy(rEndOut),
"L",
`${cx} ${cy}`,
"L",
xy(rStartOut),
"Q",
xy(ocStart),
xy(oStart),
"Z"
].join(" ");
}
function sharpDonutSegmentPath({ cx, cy, rIn, rOut, a0, a1, spanDeg }) {
const ptAt = (radius, deg) => arcPoint(cx, cy, radius, deg);
const largeArc = spanDeg > 180 ? 1 : 0;
const A = ptAt(rOut, a0);
const B = ptAt(rOut, a1);
const C = ptAt(rIn, a1);
const Din = ptAt(rIn, a0);
return [
"M",
xy(A),
"A",
rOut,
rOut,
0,
largeArc,
1,
xy(B),
"L",
xy(C),
"A",
rIn,
rIn,
0,
largeArc,
0,
xy(Din),
"Z"
].join(" ");
}
const SLICE_OFFSET_TRANSITION = "transform 320ms cubic-bezier(0.25, 0.8, 0.3, 1)";
const HOVER_OUTLINE_TRANSITION = "opacity 180ms ease-out";
class Pie {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.ctx = ctx;
this.w = w2;
this.chartType = this.w.config.chart.type;
this.initialAnim = this.w.config.chart.animations.enabled;
this.dynamicAnim = this.initialAnim && this.w.config.chart.animations.dynamicAnimation.enabled;
this.animBeginArr = [0];
this.animDur = 0;
this.donutDataLabels = this.w.config.plotOptions.pie.donut.labels;
this.lineColorArr = w2.globals.stroke.colors !== void 0 ? w2.globals.stroke.colors : w2.globals.colors;
this.defaultSize = Math.min(w2.layout.gridWidth, w2.layout.gridHeight);
this.centerY = this.defaultSize / 2;
this.centerX = w2.layout.gridWidth / 2;
if (w2.config.chart.type === "radialBar") {
this.fullAngle = 360;
} else {
this.fullAngle = Math.abs(
w2.config.plotOptions.pie.endAngle - w2.config.plotOptions.pie.startAngle
);
}
this.initialAngle = w2.config.plotOptions.pie.startAngle % this.fullAngle;
w2.globals.radialSize = this.defaultSize / 2.05 - w2.config.stroke.width - (!w2.config.chart.sparkline.enabled ? w2.config.chart.dropShadow.blur : 0);
this.externalCfg = w2.config.plotOptions.pie.dataLabels.external;
const dlStyle = w2.config.dataLabels.style;
this.externalLabelStyle = {
fontSize: this.externalCfg.fontSize || dlStyle.fontSize,
fontFamily: this.externalCfg.fontFamily || dlStyle.fontFamily,
fontWeight: this.externalCfg.fontWeight || dlStyle.fontWeight
};
this.externalLabels = [];
this.externalLabelMaxLines = 1;
this.externalLabelLineH = parseFloat(this.externalLabelStyle.fontSize) || 12;
w2.globals.pieExternalLabelMarginY = 0;
this.showExternalLabels = this.externalCfg.show && this.chartType !== "polarArea";
if (this.showExternalLabels && !w2.globals.noData) {
this.reserveExternalLabelSpace();
}
this.donutSize = w2.globals.radialSize * parseInt(w2.config.plotOptions.pie.donut.size, 10) / 100;
const scaleSize = w2.config.plotOptions.pie.customScale;
const halfW = w2.layout.gridWidth / 2;
const halfH = w2.layout.gridHeight / 2;
this.translateX = halfW - halfW * scaleSize;
this.translateY = halfH - halfH * scaleSize;
this.dataLabelsGroup = new Graphics(this.w).group({
class: "apexcharts-datalabels-group",
transform: `translate(${this.translateX}, ${this.translateY}) scale(${scaleSize})`
});
this.maxY = 0;
this.sliceLabels = [];
this.sliceSizes = [];
this.prevSliceSizes = [];
this.sliceLabelGroups = {};
this.externalLabelGroups = {};
this.elHoverOutline = null;
this.elHoverOutlinePath = null;
this.hoverOutlineIndex = -1;
this.prevSectorAngleArr = [];
}
/**
* The text shown in an outer (name) label for slice `i`. Applies the
* user `name.formatter` if provided, otherwise the raw series name. The
* formatter may return a string or an array of strings (one per line, e.g.
* `[name, percent]`); normalize via `getExternalLabelLines`.
* @param {number} i
* @returns {string | string[]}
*/
getExternalLabelText(i) {
var _a, _b, _c;
const w2 = this.w;
const name = w2.seriesData.seriesNames[i];
const fn = this.externalCfg.formatter;
if (typeof fn === "function") {
return fn(name, {
seriesIndex: i,
percent: (_b = (_a = w2.globals.seriesPercent) == null ? void 0 : _a[i]) == null ? void 0 : _b[0],
value: (_c = w2.globals.seriesTotals) == null ? void 0 : _c[i],
w: w2
});
}
return name == null ? "" : `${name}`;
}
/**
* Outer label content for slice `i` normalized to an array of line strings.
* Supports a formatter returning an array, or a string with `\n` separators.
* @param {number} i
* @returns {string[]}
*/
getExternalLabelLines(i) {
const raw = this.getExternalLabelText(i);
const arr = Array.isArray(raw) ? raw : `${raw == null ? "" : raw}`.split("\n");
return arr.map((l) => l == null ? "" : `${l}`);
}
/**
* Shrink the pie radius (and reposition its center) so outer name labels and
* their connector lines fit inside the chart area without clipping. Stores
* the reserved vertical band on `w.globals.pieExternalLabelMarginY` so
* Core.resizeNonAxisCharts can grow the SVG height to match.
*/
reserveExternalLabelSpace() {
const w2 = this.w;
const helpers = new CircularChartsHelpers(w2);
const lineSets = (w2.seriesData.seriesNames || []).map(
(_, i) => this.getExternalLabelLines(i)
);
const maxLabelWidth = helpers.getMaxLabelWidth(lineSets.flat(), {
fontSize: this.externalLabelStyle.fontSize,
fontFamily: this.externalLabelStyle.fontFamily
});
this.externalLabelMaxLines = lineSets.reduce((m2, s) => Math.max(m2, s.length), 1);
this.externalLabelLineH = Math.round(
(parseFloat(this.externalLabelStyle.fontSize) || 12) * 1.35
);
const cn = this.externalCfg.connector;
const blockHeight = this.externalLabelMaxLines * this.externalLabelLineH;
const mh = maxLabelWidth + (cn.length || 0) + (cn.gap || 0) + 12;
const mv = blockHeight / 2 + (cn.gap || 0) + 6;
const fitted = Math.min(
w2.globals.radialSize,
w2.layout.gridWidth / 2 - mh,
w2.layout.gridHeight / 2 - mv
);
w2.globals.radialSize = Math.max(fitted, this.defaultSize * 0.15);
w2.globals.pieExternalLabelMarginY = mv;
const heightStr = w2.config.chart.height ? String(w2.config.chart.height) : "";
const userSetFixedHeight = heightStr !== "" && heightStr !== "auto";
this.centerY = userSetFixedHeight ? w2.layout.gridHeight / 2 : w2.globals.radialSize + mv;
}
/**
* @param {any[]} series
*/
draw(series) {
var _a;
const self = this;
const w2 = this.w;
const graphics = new Graphics(this.w);
const elPie = graphics.group({
class: "apexcharts-pie"
});
if (w2.globals.noData) return elPie;
let total = 0;
for (let k = 0; k < series.length; k++) {
total += Utils.negToZero(series[k]);
}
const sectorAngleArr = [];
const elSeries = graphics.group();
if (total === 0) {
total = 1e-5;
}
series.forEach((m2) => {
this.maxY = Math.max(this.maxY, m2);
});
if (w2.config.yaxis[0].max) {
this.maxY = w2.config.yaxis[0].max;
}
if (w2.config.grid.position === "back" && this.chartType === "polarArea") {
this.drawPolarElements(elPie);
}
const collapsedIdx = w2.globals.collapsedSeriesIndices || [];
let polarVisible = 1;
if (this.chartType === "polarArea") {
let visible = 0;
for (let k = 0; k < series.length; k++) {
if (collapsedIdx.indexOf(k) === -1) visible++;
}
polarVisible = Math.max(1, visible);
}
for (let i = 0; i < series.length; i++) {
const angle = this.fullAngle * Utils.negToZero(series[i]) / total;
sectorAngleArr.push(angle);
if (this.chartType === "polarArea") {
sectorAngleArr[i] = collapsedIdx.indexOf(i) > -1 ? 0 : this.fullAngle / polarVisible;
this.sliceSizes.push(
w2.globals.radialSize * series[i] / (this.maxY || 1)
);
} else {
this.sliceSizes.push(w2.globals.radialSize);
}
}
const morphActive = ((_a = this.ctx.morphTypeChange) == null ? void 0 : _a.isActive()) === true;
if (w2.globals.dataChanged && !morphActive) {
if (this.chartType === "polarArea") {
const prevValues = w2.globals.previousPaths;
const stash = w2.globals.prevPolarAngles;
if (Array.isArray(stash) && stash.length === prevValues.length) {
this.prevSectorAngleArr = stash.slice();
} else {
for (let i = 0; i < prevValues.length; i++) {
this.prevSectorAngleArr.push(
this.fullAngle / Math.max(1, prevValues.length)
);
}
}
let prevMaxY = 0;
for (let k = 0; k < prevValues.length; k++) {
prevMaxY = Math.max(prevMaxY, Utils.negToZero(prevValues[k]));
}
if (w2.config.yaxis[0].max) {
prevMaxY = w2.config.yaxis[0].max;
}
this.prevSliceSizes = prevValues.map(
(v2) => w2.globals.radialSize * Utils.negToZero(v2) / (prevMaxY || 1)
);
} else {
let prevTotal = 0;
for (let k = 0; k < w2.globals.previousPaths.length; k++) {
prevTotal += Utils.negToZero(w2.globals.previousPaths[k]);
}
let previousAngle;
for (let i = 0; i < w2.globals.previousPaths.length; i++) {
previousAngle = this.fullAngle * Utils.negToZero(w2.globals.previousPaths[i]) / prevTotal;
this.prevSectorAngleArr.push(previousAngle);
}
}
}
if (this.chartType === "polarArea") {
w2.globals.prevPolarAngles = sectorAngleArr.slice();
}
if (this.donutSize < 0) {
this.donutSize = 0;
}
if (this.chartType === "donut") {
const circle = graphics.drawCircle(this.donutSize);
circle.attr({
cx: this.centerX,
cy: this.centerY,
fill: w2.config.plotOptions.pie.donut.background ? w2.config.plotOptions.pie.donut.background : "transparent"
});
elSeries.add(circle);
}
const elG = self.drawArcs(sectorAngleArr, series);
this.sliceLabels.forEach((s) => {
elG.add(s);
});
elSeries.attr({
transform: `translate(${this.translateX}, ${this.translateY}) scale(${w2.config.plotOptions.pie.customScale})`
});
elSeries.add(elG);
elPie.add(elSeries);
if (this.donutDataLabels.show) {
const shouldFadeInLabels = this.initialAnim && !w2.globals.resized && !w2.globals.dataChanged && this.animDur > 0;
const dataLabels = this.renderInnerDataLabels(
this.dataLabelsGroup,
this.donutDataLabels,
{
hollowSize: this.donutSize,
centerX: this.centerX,
centerY: this.centerY,
opacity: shouldFadeInLabels ? 0 : this.donutDataLabels.show
}
);
if (shouldFadeInLabels) {
const labelsNode = this.dataLabelsGroup.node;
labelsNode.style.transition = "opacity 280ms ease-out";
setTimeout(() => {
labelsNode.style.opacity = "1";
}, this.animDur);
}
elPie.add(dataLabels);
}
if (w2.config.grid.position === "front" && this.chartType === "polarArea") {
this.drawPolarElements(elPie);
}
return elPie;
}
// core function for drawing pie arcs
/**
* @param {any[]} sectorAngleArr
* @param {any[]} series
*/
drawArcs(sectorAngleArr, series) {
var _a, _b, _c, _d, _e;
const w2 = this.w;
const filters = new Filters(this.w);
const graphics = new Graphics(this.w);
const fill = new Fill(this.w);
const g2 = graphics.group({
class: "apexcharts-slices"
});
this.elHoverOutline = graphics.group({
class: "apexcharts-pie-hover-outline"
});
this.elHoverOutline.node.style.pointerEvents = "none";
this.elHoverOutline.node.style.opacity = "0";
if (w2.config.chart.animations.enabled) {
this.elHoverOutline.node.style.transition = HOVER_OUTLINE_TRANSITION;
}
g2.add(this.elHoverOutline);
let startAngle = this.initialAngle;
let prevStartAngle = this.initialAngle;
let endAngle = this.initialAngle;
let prevEndAngle = this.initialAngle;
this.strokeWidth = w2.config.stroke.show ? w2.config.stroke.width : 0;
const morphActive = ((_a = this.ctx.morphTypeChange) == null ? void 0 : _a.isActive()) === true;
for (let i = 0; i < sectorAngleArr.length; i++) {
const elPieArc = graphics.group({
class: `apexcharts-series apexcharts-pie-series`,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[i]),
rel: i + 1,
"data:realIndex": i
});
g2.add(elPieArc);
startAngle = endAngle;
prevStartAngle = prevEndAngle;
endAngle = startAngle + sectorAngleArr[i];
prevEndAngle = prevStartAngle + this.prevSectorAngleArr[i];
const angle = endAngle < startAngle ? this.fullAngle + endAngle - startAngle : endAngle - startAngle;
const pathFill = fill.fillPath({
seriesNumber: i,
size: this.sliceSizes[i],
value: series[i]
});
const morphFrom = morphActive ? this.ctx.morphTypeChange.getInitialPathFor(i, 0) : null;
const prevSize = this.chartType === "polarArea" && w2.globals.dataChanged && !morphActive ? this.prevSliceSizes[i] || 0 : void 0;
const path = morphFrom || this.getChangedPath(prevStartAngle, prevEndAngle, prevSize);
const elPath = graphics.drawPath({
d: path,
// Pie/donut/polarArea data is a single series, so a user-supplied
// `stroke.colors` shorter than the slice count is NOT padded by the
// theme engine (unlike fill colors, which cycle). Without this, only
// slice 0 gets the requested color and the rest fall back to a grey
// default. Cycle the array — matching fill-color behaviour — so a
// single `stroke.colors: ['#fff']` borders every slice as expected.
stroke: Array.isArray(this.lineColorArr) ? (_b = this.lineColorArr[i]) != null ? _b : this.lineColorArr[i % this.lineColorArr.length] : this.lineColorArr,
strokeWidth: 0,
fill: pathFill,
fillOpacity: w2.config.fill.opacity,
classes: `apexcharts-pie-area apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
});
elPath.attr({
index: 0,
j: i
});
filters.setSelectionFilter(elPath, 0, i);
if (w2.config.chart.dropShadow.enabled) {
const shadow = w2.config.chart.dropShadow;
filters.dropShadow(elPath, shadow, i);
}
this.addListeners(elPath, this.donutDataLabels, i);
let labelPosition = {
x: 0,
y: 0
};
const midAngle = (startAngle + angle / 2) % this.fullAngle;
let arcCenter = { x: this.centerX, y: this.centerY };
if (this.chartType === "pie" || this.chartType === "polarArea") {
labelPosition = Utils.polarToCartesian(
this.centerX,
this.centerY,
w2.globals.radialSize / 1.25 + w2.config.plotOptions.pie.dataLabels.offset,
midAngle
);
arcCenter = Utils.polarToCartesian(
this.centerX,
this.centerY,
w2.globals.radialSize / 2,
midAngle
);
} else if (this.chartType === "donut") {
labelPosition = Utils.polarToCartesian(
this.centerX,
this.centerY,
(w2.globals.radialSize + this.donutSize) / 2 + w2.config.plotOptions.pie.dataLabels.offset,
midAngle
);
arcCenter = Utils.polarToCartesian(
this.centerX,
this.centerY,
(w2.globals.radialSize + this.donutSize) / 2,
midAngle
);
}
Graphics.setAttrs(elPath.node, {
"data:angle": angle,
"data:startAngle": startAngle,
"data:strokeWidth": this.strokeWidth,
"data:value": series[i],
"data:cx": arcCenter.x,
"data:cy": arcCenter.y
});
elPieArc.add(elPath);
let pieceClaimed = false;
if (morphActive) {
const finalD = this.getPiePath({
me: this,
startAngle,
angle,
size: this.sliceSizes[i]
});
elPath.node.setAttribute("data:pathFinal", finalD);
pieceClaimed = ((_d = (_c = this.ctx.morphTypeChange).claimsTargetMark) == null ? void 0 : _d.call(_c, i, 0)) === true;
if (pieceClaimed) {
elPath.attr({ d: finalD });
elPath.node.setAttribute("data:pathOrig", finalD);
elPath.node.setAttribute("opacity", "0");
elPath.node.setAttribute("data-piece-hidden", "1");
}
}
let dur = 0;
if (this.initialAnim && !w2.globals.resized && !w2.globals.dataChanged) {
dur = angle / this.fullAngle * w2.config.chart.animations.speed;
if (dur === 0) dur = 1;
this.animDur = dur + this.animDur;
this.animBeginArr.push(this.animDur);
} else {
this.animBeginArr.push(0);
}
if (pieceClaimed) ;
else if (morphActive && morphFrom) {
const targetD = this.getPiePath({
me: this,
startAngle,
angle,
size: this.sliceSizes[i]
});
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
const animations = this.ctx.animations;
elPath.node.setAttribute("data:pathOrig", targetD);
const morphRunner = elPath.animate(morphSpeed).plot(targetD, "polygons").attr({ "stroke-width": this.strokeWidth });
if (morphRunner && typeof morphRunner.after === "function") {
morphRunner.after(() => animations.animationCompleted(elPath));
} else {
animations.animationCompleted(elPath);
}
} else if (this.dynamicAnim && w2.globals.dataChanged) {
this.animatePaths(elPath, {
size: this.sliceSizes[i],
prevSize,
endAngle,
startAngle,
prevStartAngle,
prevEndAngle,
animateStartingPos: true,
i,
animBeginArr: this.animBeginArr,
shouldSetPrevPaths: true,
dur: w2.config.chart.animations.dynamicAnimation.speed
});
} else {
this.animatePaths(elPath, {
size: this.sliceSizes[i],
endAngle,
startAngle,
i,
totalItems: sectorAngleArr.length - 1,
animBeginArr: this.animBeginArr,
dur
});
}
if (this.getExpandOffset() > 0) {
elPath.node.addEventListener("mouseup", this.pieClicked.bind(this, i));
} else if (i === 0 && Filters.drilldownBlocksSliceOffset(w2)) {
(_e = this.ctx.drilldown) == null ? void 0 : _e.warnSliceOffsetDisabled();
}
if (typeof w2.interact.selectedDataPoints[0] !== "undefined" && w2.interact.selectedDataPoints[0].indexOf(i) > -1) {
if (this.initialAnim && !w2.globals.resized && !w2.globals.dataChanged && this.animDur > 0) {
const _this = this;
const _i = i;
setTimeout(() => _this.pieClicked(_i, { animate: false }), this.animDur);
} else {
this.pieClicked(i, { animate: false });
}
}
if (w2.config.dataLabels.enabled) {
const xPos = labelPosition.x;
const yPos = labelPosition.y;
let text = 100 * angle / this.fullAngle + "%";
if (angle !== 0 && w2.config.plotOptions.pie.dataLabels.minAngleToShowLabel < sectorAngleArr[i]) {
const formatter = w2.config.dataLabels.formatter;
if (formatter !== void 0) {
text = formatter(w2.globals.seriesPercent[i][0], {
seriesIndex: i,
w: w2
});
}
const foreColor = w2.globals.dataLabels.style.colors[i];
const elPieLabelWrap = graphics.group({
class: `apexcharts-datalabels`
});
const elPieLabel = graphics.drawText({
x: xPos,
y: yPos,
text,
textAnchor: "middle",
fontSize: w2.config.dataLabels.style.fontSize,
fontFamily: w2.config.dataLabels.style.fontFamily,
fontWeight: w2.config.dataLabels.style.fontWeight,
foreColor
});
elPieLabelWrap.add(elPieLabel);
if (w2.config.dataLabels.dropShadow.enabled) {
const textShadow = w2.config.dataLabels.dropShadow;
filters.dropShadow(elPieLabel, textShadow);
}
elPieLabel.node.classList.add("apexcharts-pie-label");
if (w2.config.chart.animations.animate && w2.globals.resized === false) {
elPieLabel.node.classList.add("apexcharts-pie-label-delay");
elPieLabel.node.style.animationDelay = w2.config.chart.animations.speed / 940 + "s";
}
this.sliceLabels.push(elPieLabelWrap);
this.sliceLabelGroups[i] = elPieLabelWrap.node;
}
}
if (this.showExternalLabels && angle !== 0) {
const lines = this.getExternalLabelLines(i);
if (lines.some((l) => l !== "")) {
const anchor = Utils.polarToCartesian(
this.centerX,
this.centerY,
w2.globals.radialSize,
midAngle
);
const elbow = Utils.polarToCartesian(
this.centerX,
this.centerY,
w2.globals.radialSize + (this.externalCfg.connector.gap || 0),
midAngle
);
const isRight = elbow.x >= this.centerX;
const baseLabelX = isRight ? elbow.x + (this.externalCfg.connector.length || 0) : elbow.x - (this.externalCfg.connector.length || 0);
this.externalLabels.push({
i,
lines,
anchor,
elbow,
side: isRight ? "right" : "left",
labelX: baseLabelX + parseFloat(this.externalCfg.offsetX || 0),
idealY: elbow.y + parseFloat(this.externalCfg.offsetY || 0),
connectorColor: this.externalCfg.connector.color || w2.globals.colors[i],
foreColor: this.externalCfg.color || w2.config.chart.foreColor
});
}
}
}
if (this.showExternalLabels && this.externalLabels.length) {
this.placeExternalLabels();
const revealOnAnimEnd = Environment.isBrowser() && (morphActive || this.dynamicAnim && w2.globals.dataChanged || this.initialAnim && !w2.globals.resized && !w2.globals.dataChanged);
this.externalLabels.forEach((lbl) => {
const group = new CircularChartsHelpers(w2).drawExternalLabel({
lines: lbl.lines,
lineHeight: this.externalLabelLineH,
anchor: lbl.anchor,
elbow: lbl.elbow,
labelX: lbl.labelX,
labelY: lbl.labelY,
side: lbl.side,
connector: {
show: this.externalCfg.connector.show,
width: this.externalCfg.connector.width,
color: lbl.connectorColor
},
style: this.externalLabelStyle,
foreColor: lbl.foreColor
});
if (revealOnAnimEnd) {
group.node.classList.add("apexcharts-element-hidden");
w2.globals.delayedElements.push({ el: group.node });
}
this.externalLabelGroups[lbl.i] = group.node;
g2.add(group);
});
}
return g2;
}
/**
* Vertical de-overlap for outer (name) labels: per side, sort by ideal y and
* push neighbours apart so they keep at least one line-height of spacing.
* Mutates each entry's `labelY`. Connector lines re-route to the moved y.
*/
placeExternalLabels() {
const w2 = this.w;
const lineHeight = this.externalLabelMaxLines * this.externalLabelLineH + 2;
const maxY = this.centerY + w2.globals.radialSize + w2.globals.pieExternalLabelMarginY;
["left", "right"].forEach((side) => {
spaceOutLabels(
this.externalLabels.filter((l) => l.side === side),
lineHeight,
maxY
);
});
}
/**
* @param {any} elPath
* @param {Record<string, any>} dataLabels
* @param {number} [i] slice index, for the hover outline band
*/
addListeners(elPath, dataLabels, i) {
const graphics = new Graphics(this.w, this.ctx);
elPath.node.addEventListener(
"mouseenter",
graphics.pathMouseEnter.bind(graphics, elPath)
);
elPath.node.addEventListener(
"mouseleave",
graphics.pathMouseLeave.bind(graphics, elPath)
);
elPath.node.addEventListener(
"mouseleave",
this.revertDataLabelsInner.bind(this)
);
if (typeof i === "number") {
elPath.node.addEventListener(
"mouseenter",
this.showHoverOutline.bind(this, i)
);
elPath.node.addEventListener(
"mouseleave",
this.hideHoverOutline.bind(this)
);
}
elPath.node.addEventListener(
"mousedown",
graphics.pathMouseDown.bind(graphics, elPath)
);
if (!this.donutDataLabels.total.showAlways) {
elPath.node.addEventListener(
"mouseenter",
this.printDataLabelsInner.bind(this, elPath.node, dataLabels)
);
elPath.node.addEventListener(
"mousedown",
this.printDataLabelsInner.bind(this, elPath.node, dataLabels)
);
}
}
// This function can be used for other circle charts too
/**
* @param {any} el
* @param {Record<string, any>} opts
*/
animatePaths(el, opts) {
const w2 = this.w;
const me = this;
let angle = opts.endAngle < opts.startAngle ? this.fullAngle + opts.endAngle - opts.startAngle : opts.endAngle - opts.startAngle;
let prevAngle = angle;
let fromStartAngle = opts.startAngle;
const toStartAngle = opts.startAngle;
if (opts.prevStartAngle !== void 0 && opts.prevEndAngle !== void 0) {
fromStartAngle = opts.prevEndAngle;
prevAngle = opts.prevEndAngle < opts.prevStartAngle ? this.fullAngle + opts.prevEndAngle - opts.prevStartAngle : opts.prevEndAngle - opts.prevStartAngle;
}
if (opts.i === w2.config.series.length - 1) {
if (angle + toStartAngle > this.fullAngle) {
opts.endAngle = opts.endAngle - (angle + toStartAngle);
} else if (angle + toStartAngle < this.fullAngle) {
opts.endAngle = opts.endAngle + (this.fullAngle - (angle + toStartAngle));
}
}
if (angle === this.fullAngle) angle = this.fullAngle - 0.01;
me.animateArc(el, fromStartAngle, toStartAngle, angle, prevAngle, opts);
}
/**
* @param {any} el
* @param {number} fromStartAngle
* @param {number} toStartAngle
* @param {number} angle
* @param {number} prevAngle
* @param {Record<string, any>} opts
*/
animateArc(el, fromStartAngle, toStartAngle, angle, prevAngle, opts) {
const me = this;
const w2 = this.w;
const animations = new Animations(this.w);
const size = opts.size;
let path;
if (isNaN(fromStartAngle) || isNaN(prevAngle)) {
fromStartAngle = toStartAngle;
prevAngle = angle;
opts.dur = 0;
}
let currAngle = angle;
let startAngle = toStartAngle;
const fromAngle = fromStartAngle < toStartAngle ? this.fullAngle + fromStartAngle - toStartAngle : fromStartAngle - toStartAngle;
const hasPrevSize = typeof opts.prevSize === "number";
if (w2.globals.dataChanged && opts.shouldSetPrevPaths) {
if (opts.prevEndAngle) {
path = me.getPiePath({
me,
startAngle: opts.prevStartAngle,
angle: opts.prevEndAngle < opts.prevStartAngle ? this.fullAngle + opts.prevEndAngle - opts.prevStartAngle : opts.prevEndAngle - opts.prevStartAngle,
size: hasPrevSize ? opts.prevSize : size
});
el.attr({ d: path });
}
}
if (opts.dur !== 0) {
el.animate(opts.dur, opts.animBeginArr[opts.i]).after(
/** @this {any} */
function() {
if (me.chartType === "pie" || me.chartType === "donut" || me.chartType === "polarArea") {
this.animate(
w2.config.chart.animations.dynamicAnimation.speed
).attr({
"stroke-width": me.strokeWidth
});
}
if (opts.i === w2.config.series.length - 1) {
animations.animationCompleted(el);
}
}
).during((pos) => {
currAngle = fromAngle + (angle - fromAngle) * pos;
if (opts.animateStartingPos) {
currAngle = prevAngle + (angle - prevAngle) * pos;
startAngle = fromStartAngle - prevAngle + (toStartAngle - (fromStartAngle - prevAngle)) * pos;
}
path = me.getPiePath({
me,
startAngle,
angle: currAngle,
size: hasPrevSize ? opts.prevSize + (size - opts.prevSize) * pos : size
});
el.node.setAttribute("data:pathOrig", path);
el.attr({
d: path
});
});
} else {
path = me.getPiePath({
me,
startAngle,
angle,
size
});
if (!opts.isTrack) {
w2.globals.animationEnded = true;
}
el.node.setAttribute("data:pathOrig", path);
el.attr({
d: path,
"stroke-width": me.strokeWidth
});
}
}
/**
* Toggle slice `i` in or out of the pie. Only one slice sits outside at a
* time. Bound to `mouseup` on each slice, and also reached from the
* `toggleDataPointSelection` API and from the pre-selected-slice pass in
* drawArcs (which passes `animate: false` so the slice is already parked by
* the time the first frame is painted).
* @param {number} i
* @param {{animate?: boolean}} [opts] a MouseEvent when called as a listener,
* which carries no `animate`, so real clicks animate
*/
pieClicked(i, opts) {
const w2 = this.w;
const me = this;
const animate = !(opts && opts.animate === false);
const elPath = w2.dom.Paper.findOne(
`.apexcharts-${me.chartType.toLowerCase()}-slice-${i}`
);
if (!elPath) return;
if (elPath.attr("data:pieClicked") === "true") {
elPath.attr({
"data:pieClicked": "false"
});
this.revertDataLabelsInner();
this.offsetSlice(i, 0, animate);
return;
}
const allEls = w2.dom.baseEl.getElementsByClassName("apexcharts-pie-area");
Array.prototype.forEach.call(allEls, (pieSlice) => {
const wasOut = pieSlice.getAttribute("data:pieClicked") === "true";
pieSlice.setAttribute("data:pieClicked", "false");
if (wasOut) {
this.offsetSlice(parseInt(pieSlice.getAttribute("j"), 10), 0, animate);
}
});
w2.interact.capturedDataPointIndex = i;
elPath.attr("data:pieClicked", "true");
this.offsetSlice(i, this.getExpandOffset(), animate);
}
/**
* How far a clicked slice slides out, in px. 0 when the pull-out is off, when
* drilldown owns the click, and always for polarArea: there the radius
* encodes the value, so moving a slice outward would read as a bigger number.
* @returns {number}
*/
getExpandOffset() {
const pie = this.w.config.plotOptions.pie;
if (!pie.expandOnClick || this.chartType === "polarArea") return 0;
if (Filters.drilldownBlocksSliceOffset(this.w)) return 0;
const offset = Number(pie.expandOffset);
return Number.isFinite(offset) && offset > 0 ? offset : 0;
}
/**
* Every node that has to travel with slice `i`: the slice path itself plus
* its labels, which live in sibling groups so they paint above all slices.
* @param {number} i
* @returns {SVGElement[]}
*/
getSliceMovers(i) {
const elPath = this.w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
return [
elPath ? elPath.node : null,
this.sliceLabelGroups[i],
this.externalLabelGroups[i],
// The hover band, when it is this slice's: clicking a slice you are
// hovering has to take its outline along, or the band is left behind
// sitting in the gap the slice just opened.
this.hoverOutlineIndex === i && this.elHoverOutlinePath ? this.elHoverOutlinePath.node : null
].filter(Boolean);
}
/**
* Slide slice `i` `dist` px out of the pie along its own mid-angle, or back
* to its resting place when `dist` is 0.
*
* The slice is *translated*, never re-drawn: the arc keeps the exact radius
* and span it had, so the pulled-out slice still encodes the same quantity
* (growing the radius, as this used to, quietly inflates it) and a clean gap
* opens between it and the rest of the pie.
* @param {number} i
* @param {number} dist
* @param {boolean} [animate] false to park it instantly, with no transition
*/
offsetSlice(i, dist, animate = true) {
const w2 = this.w;
const elPath = w2.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
if (!elPath) return;
const { dx, dy } = this.getSliceOffsetVector(i, dist);
const transform = `translate(${dx} ${dy})`;
const transition = animate && w2.config.chart.animations.enabled ? SLICE_OFFSET_TRANSITION : "";
this.getSliceMovers(i).forEach((node) => {
node.style.transition = transition;
node.setAttribute("transform", transform);
});
}
/**
* The px vector `dist` along slice `i`'s mid-angle. Zero for a slice that
* fills the pie: there is no "outside" for it to move to, and sliding it
* would just shift the whole chart sideways.
* @param {number} i
* @param {number} dist
* @returns {{dx: number, dy: number}}
*/
getSliceOffsetVector(i, dist) {
const elPath = this.w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
const angle = elPath ? parseFloat(elPath.attr("data:angle")) : NaN;
if (!dist || !Number.isFinite(angle) || angle >= this.fullAngle) {
return { dx: 0, dy: 0 };
}
const startAngle = parseFloat(elPath.attr("data:startAngle"));
const midRad = Math.PI * (startAngle + angle / 2 - 90) / 180;
return { dx: dist * Math.cos(midRad), dy: dist * Math.sin(midRad) };
}
/**
* Fade in the hover outline: a translucent band traced just outside the rim
* of slice `i`, in the slice's own colour. It replaces lightening the slice
* (see Filters.hoverOutlineOwnsHoverState) so a hovered slice keeps the
* colour the legend and the data labels claim it has.
* @param {number} i
*/
showHoverOutline(i) {
const w2 = this.w;
if (!Filters.hoverOutlineOwnsHoverState(w2)) return;
if (!this.elHoverOutline) return;
const path = this.getHoverOutlinePath(i);
if (!path) return;
const cfg = w2.config.plotOptions.pie.hoverOutline;
const fill = cfg.color || w2.globals.colors[i];
if (!this.elHoverOutlinePath) {
const graphics = new Graphics(w2);
this.elHoverOutlinePath = graphics.drawPath({
d: path,
fill,
strokeWidth: 0,
classes: "apexcharts-pie-hover-outline-band"
});
this.elHoverOutline.add(this.elHoverOutlinePath);
}
this.elHoverOutlinePath.attr({
d: path,
fill,
"fill-opacity": cfg.opacity
});
const { dx, dy } = this.getSliceOffsetVector(
i,
this.isSliceOut(i) ? this.getExpandOffset() : 0
);
this.elHoverOutlinePath.node.style.transition = "";
this.elHoverOutlinePath.node.setAttribute("transform", `translate(${dx} ${dy})`);
this.hoverOutlineIndex = i;
this.elHoverOutline.node.style.opacity = "1";
}
/** Fade the hover outline back out, leaving the band node in place. */
hideHoverOutline() {
if (this.elHoverOutline) {
this.elHoverOutline.node.style.opacity = "0";
}
}
/** @param {number} i @returns {boolean} */
isSliceOut(i) {
const elPath = this.w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
return !!elPath && elPath.attr("data:pieClicked") === "true";
}
/**
* Band geometry for the hover outline of slice `i`: an annulus from the
* slice rim (plus the stroke and the configured clearance) outward, over the
* same angular extent the slice is actually drawn over, so it lines up with
* both slice edges even with `spacing` insetting them. Rounded into a pill
* when the band is thick enough for the fillets to fit.
* @param {number} i
* @returns {string | null}
*/
getHoverOutlinePath(i) {
const w2 = this.w;
const cfg = w2.config.plotOptions.pie.hoverOutline;
const elPath = w2.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
if (!elPath) return null;
const angle = parseFloat(elPath.attr("data:angle"));
if (!Number.isFinite(angle) || angle <= 0) return null;
const size = this.sliceSizes[i];
const thickness = Number(cfg.size);
if (!Number.isFinite(size) || !Number.isFinite(thickness) || thickness <= 0) {
return null;
}
const { startDeg, spanDeg } = this.getSliceExtent({
me: this,
startAngle: parseFloat(elPath.attr("data:startAngle")),
angle,
size
});
if (!(spanDeg > 0)) return null;
const rIn = size + (this.strokeWidth || 0) / 2 + (Number(cfg.gap) || 0);
const rOut = rIn + thickness;
const geo = {
cx: this.centerX,
cy: this.centerY,
rIn,
rOut,
a0: startDeg,
a1: startDeg + spanDeg,
spanDeg
};
const r = Math.min(thickness / 2, spanDeg * Math.PI / 180 / 2 * rIn);
return r > 0.5 ? roundedDonutSegmentPath(__spreadProps(__spreadValues({}, geo), { r })) : sharpDonutSegmentPath(geo);
}
/**
* @param {number} prevStartAngle
* @param {number} prevEndAngle
*/
/**
* @param {number} prevStartAngle
* @param {number} prevEndAngle
* @param {number} [prevSize] - polarArea passes its previous slice radius,
* so the pre-animation frame sits at the previous VALUE too, not just the
* previous angles.
*/
getChangedPath(prevStartAngle, prevEndAngle, prevSize) {
let path = "";
if (this.dynamicAnim && this.w.globals.dataChanged) {
path = this.getPiePath({
me: this,
startAngle: prevStartAngle,
angle: prevEndAngle - prevStartAngle,
// @ts-ignore — size is set dynamically during draw()
size: typeof prevSize === "number" ? prevSize : this.size
});
}
return path;
}
/**
* The angular extent a slice is actually drawn over: the raw start / span
* clamped so a full circle never overlaps itself, then inset by
* `plotOptions.pie.spacing`. Shared by getPiePath and the hover outline, so
* the band lines up with the slice edges instead of with the un-inset angles
* cached on the path node.
* @param {{me: any, startAngle: number, angle: number, size: number}} opts
* @returns {{startDeg: number, spanDeg: number, endDeg: number}}
*/
getSliceExtent({ me, startAngle, angle, size }) {
const w2 = this.w;
let startDeg = startAngle;
let endDeg = angle + startAngle;
if (Math.ceil(endDeg) >= this.fullAngle + this.w.config.plotOptions.pie.startAngle % this.fullAngle) {
endDeg = this.fullAngle + this.w.config.plotOptions.pie.startAngle % this.fullAngle - 0.01;
}
let spanDeg = endDeg - startDeg;
const isSliceType = me.chartType === "pie" || me.chartType === "donut" || me.chartType === "polarArea";
const spacing = w2.config.plotOptions.pie.spacing;
if (isSliceType && spacing > 0 && spanDeg > 0) {
const rRef = me.chartType === "donut" ? (size + me.donutSize) / 2 : size;
const gapDeg = rRef > 0 ? spacing / rRef * (180 / Math.PI) : 0;
const inset = Math.min(gapDeg / 2, Math.max(0, spanDeg / 2 - 0.5));
startDeg += inset;
spanDeg -= 2 * inset;
}
endDeg = startDeg + spanDeg;
if (Math.ceil(endDeg) > this.fullAngle) endDeg -= this.fullAngle;
return { startDeg, spanDeg, endDeg };
}
/** @param {{me: any, startAngle: any, angle: any, size: any}} opts */
getPiePath({ me, startAngle, angle, size }) {
let path;
const w2 = this.w;
const graphics = new Graphics(this.w);
const { startDeg, spanDeg, endDeg } = this.getSliceExtent({
me,
startAngle,
angle,
size
});
const isSliceType = me.chartType === "pie" || me.chartType === "donut" || me.chartType === "polarArea";
const startRadians = Math.PI * (startDeg - 90) / 180;
const borderRadius = w2.config.plotOptions.pie.borderRadius;
if (borderRadius > 0 && isSliceType) {
const roundedPath = this.getRoundedSlicePath({
me,
startDeg,
spanDeg,
size,
borderRadius
});
if (roundedPath) return roundedPath;
}
const endRadians = Math.PI * (endDeg - 90) / 180;
const x1 = me.centerX + size * Math.cos(startRadians);
const y1 = me.centerY + size * Math.sin(startRadians);
const x2 = me.centerX + size * Math.cos(endRadians);
const y2 = me.centerY + size * Math.sin(endRadians);
const startInner = Utils.polarToCartesian(
me.centerX,
me.centerY,
me.donutSize,
endDeg
);
const endInner = Utils.polarToCartesian(
me.centerX,
me.centerY,
me.donutSize,
startDeg
);
const largeArc = spanDeg > 180 ? 1 : 0;
const pathBeginning = ["M", x1, y1, "A", size, size, 0, largeArc, 1, x2, y2];
if (me.chartType === "donut") {
path = [
...pathBeginning,
"L",
startInner.x,
startInner.y,
"A",
me.donutSize,
me.donutSize,
0,
largeArc,
0,
endInner.x,
endInner.y,
"L",
x1,
y1,
"z"
].join(" ");
} else if (me.chartType === "pie" || me.chartType === "polarArea") {
path = [...pathBeginning, "L", me.centerX, me.centerY, "L", x1, y1].join(
" "
);
} else {
path = [...pathBeginning].join(" ");
}
return graphics.roundPathCorners(path, this.strokeWidth * 2);
}
/**
* Build a slice path with rounded corners (plotOptions.pie.borderRadius).
*
* The generic roundPathCorners() only rounds line->line joins, but a slice
* corner is an arc<->line join, so we construct the fillets explicitly here:
* every corner is inset by the (clamped) radius along both the arc and the
* radial edge, and a quadratic Bezier with its control point at the original
* sharp corner bridges the two inset points. Donut slices round all four
* corners; pie / polarArea slices round the two outer corners and keep the
* center apex sharp.
*
* Returns null when the slice is too small to round meaningfully, so the
* caller can fall back to a sharp-corner path.
*
* @param {{me: any, startDeg: number, spanDeg: number, size: number, borderRadius: number}} opts
* @returns {string | null}
*/
getRoundedSlicePath({ me, startDeg, spanDeg, size, borderRadius }) {
if (!(spanDeg > 0)) return null;
const D2R2 = Math.PI / 180;
const cx = me.centerX;
const cy = me.centerY;
const isDonut = me.chartType === "donut";
const rOut = size;
const rIn = isDonut ? me.donutSize : 0;
const spanRad = spanDeg * D2R2;
let r = borderRadius;
r = Math.min(r, spanRad * rOut / 2);
if (isDonut) {
r = Math.min(r, spanRad * rIn / 2);
r = Math.min(r, (rOut - rIn) / 2);
} else {
r = Math.min(r, rOut / 2);
}
if (!(r > 0.5)) return null;
const a0 = startDeg;
const a1 = startDeg + spanDeg;
if (isDonut) {
return roundedDonutSegmentPath({ cx, cy, rIn, rOut, a0, a1, r, spanDeg });
}
return roundedPieSegmentPath({ cx, cy, rOut, a0, a1, r, spanDeg });
}
/**
* @param {any} parent
*/
drawPolarElements(parent) {
const w2 = this.w;
const scale = new Scales(this.w);
const graphics = new Graphics(this.w);
const helpers = new CircularChartsHelpers(this.w);
const gCircles = graphics.group();
const gYAxis = graphics.group();
const yScale = scale.niceScale(0, Math.ceil(this.maxY), 0);
const yTexts = yScale.result.reverse();
const len = yScale.result.length;
this.maxY = yScale.niceMax;
let circleSize = w2.globals.radialSize;
const diff = circleSize / (len - 1);
for (let i = 0; i < len - 1; i++) {
const circle = graphics.drawCircle(circleSize);
circle.attr({
cx: this.centerX,
cy: this.centerY,
fill: "none",
"stroke-width": w2.config.plotOptions.polarArea.rings.strokeWidth,
stroke: w2.config.plotOptions.polarArea.rings.strokeColor
});
if (w2.config.yaxis[0].show) {
const yLabel = helpers.drawYAxisTexts(
this.centerX,
this.centerY - circleSize + parseInt(w2.config.yaxis[0].labels.style.fontSize, 10) / 2,
i,
yTexts[i]
);
gYAxis.add(yLabel);
}
gCircles.add(circle);
circleSize = circleSize - diff;
}
this.drawSpokes(parent);
parent.add(gCircles);
parent.add(gYAxis);
}
/**
* @param {any} dataLabelsGroup
* @param {Record<string, any>} dataLabelsConfig
* @param {Record<string, any>} opts
*/
renderInnerDataLabels(dataLabelsGroup, dataLabelsConfig, opts) {
const w2 = this.w;
const graphics = new Graphics(this.w);
const showTotal = dataLabelsConfig.total.show;
dataLabelsGroup.node.innerHTML = "";
dataLabelsGroup.node.style.opacity = opts.opacity;
const x = opts.centerX;
const y = !this.donutDataLabels.total.label ? opts.centerY - opts.centerY / 6 : opts.centerY;
let labelColor, valueColor;
if (dataLabelsConfig.name.color === void 0) {
labelColor = w2.globals.colors[0];
} else {
labelColor = dataLabelsConfig.name.color;
}
let labelFontSize = dataLabelsConfig.name.fontSize;
let labelFontFamily = dataLabelsConfig.name.fontFamily;
let labelFontWeight = dataLabelsConfig.name.fontWeight;
if (dataLabelsConfig.value.color === void 0) {
valueColor = w2.config.chart.foreColor;
} else {
valueColor = dataLabelsConfig.value.color;
}
const lbFormatter = dataLabelsConfig.value.formatter;
let val = "";
let name = "";
if (showTotal) {
labelColor = dataLabelsConfig.total.color;
labelFontSize = dataLabelsConfig.total.fontSize;
labelFontFamily = dataLabelsConfig.total.fontFamily;
labelFontWeight = dataLabelsConfig.total.fontWeight;
name = !this.donutDataLabels.total.label ? "" : dataLabelsConfig.total.label;
val = dataLabelsConfig.total.formatter(w2);
} else {
if (w2.seriesData.series.length === 1) {
val = lbFormatter(w2.seriesData.series[0], w2);
name = w2.seriesData.seriesNames[0];
}
}
if (name) {
name = dataLabelsConfig.name.formatter(
name,
dataLabelsConfig.total.show,
w2
);
}
if (dataLabelsConfig.name.show) {
const elLabel = graphics.drawText({
x,
y: y + parseFloat(dataLabelsConfig.name.offsetY),
text: name,
textAnchor: "middle",
foreColor: labelColor,
fontSize: labelFontSize,
fontWeight: labelFontWeight,
fontFamily: labelFontFamily
});
elLabel.node.classList.add("apexcharts-datalabel-label");
dataLabelsGroup.add(elLabel);
}
if (dataLabelsConfig.value.show) {
const valOffset = dataLabelsConfig.name.show ? parseFloat(dataLabelsConfig.value.offsetY) + 16 : dataLabelsConfig.value.offsetY;
const elValue = graphics.drawText({
x,
y: y + valOffset,
text: val,
textAnchor: "middle",
foreColor: valueColor,
fontWeight: dataLabelsConfig.value.fontWeight,
fontSize: dataLabelsConfig.value.fontSize,
fontFamily: dataLabelsConfig.value.fontFamily
});
elValue.node.classList.add("apexcharts-datalabel-value");
dataLabelsGroup.add(elValue);
}
return dataLabelsGroup;
}
/**
*
* @param {string} name - The name of the series
* @param {string} val - The value of that series
* @param {any} el - Optional el (indicates which series was hovered/clicked). If this param is not present, means we need to show total
* @param {Record<string, any>} labelsConfig
*/
printInnerLabels(labelsConfig, name, val, el) {
const w2 = this.w;
let labelColor;
if (el) {
if (labelsConfig.name.color === void 0) {
labelColor = w2.globals.colors[parseInt(el.parentNode.getAttribute("rel"), 10) - 1];
} else {
labelColor = labelsConfig.name.color;
}
} else {
if (w2.seriesData.series.length > 1 && labelsConfig.total.show) {
labelColor = labelsConfig.total.color;
}
}
const elLabel = w2.dom.baseEl.querySelector(".apexcharts-datalabel-label");
const elValue = w2.dom.baseEl.querySelector(".apexcharts-datalabel-value");
const lbFormatter = labelsConfig.value.formatter;
val = lbFormatter(val, w2);
if (!el && typeof labelsConfig.total.formatter === "function") {
val = labelsConfig.total.formatter(w2);
}
const isTotal = name === labelsConfig.total.label;
name = !this.donutDataLabels.total.label ? "" : labelsConfig.name.formatter(name, isTotal, w2);
if (elLabel !== null) {
elLabel.textContent = name;
}
if (elValue !== null) {
elValue.textContent = val;
}
if (elLabel !== null) {
const elLabelEl = (
/** @type {HTMLElement} */
elLabel
);
elLabelEl.style.fill = labelColor;
}
}
/**
* @param {any} el
* @param {Record<string, any>} dataLabelsConfig
*/
printDataLabelsInner(el, dataLabelsConfig) {
const w2 = this.w;
const val = el.getAttribute("data:value");
const name = w2.seriesData.seriesNames[parseInt(el.parentNode.getAttribute("rel"), 10) - 1];
if (w2.seriesData.series.length > 1) {
this.printInnerLabels(dataLabelsConfig, name, val, el);
}
const dataLabelsGroup = w2.dom.baseEl.querySelector(
".apexcharts-datalabels-group"
);
if (dataLabelsGroup !== null) {
const dataLabelsGroupEl = (
/** @type {HTMLElement} */
dataLabelsGroup
);
dataLabelsGroupEl.style.opacity = "1";
}
}
/**
* @param {any} parent
*/
drawSpokes(parent) {
const w2 = this.w;
const graphics = new Graphics(this.w);
const spokeConfig = w2.config.plotOptions.polarArea.spokes;
if (spokeConfig.strokeWidth === 0) return;
const spokes = [];
const angleDivision = 360 / w2.seriesData.series.length;
for (let i = 0; i < w2.seriesData.series.length; i++) {
spokes.push(
Utils.polarToCartesian(
this.centerX,
this.centerY,
w2.globals.radialSize,
w2.config.plotOptions.pie.startAngle + angleDivision * i
)
);
}
spokes.forEach((p2, i) => {
const line = graphics.drawLine(
p2.x,
p2.y,
this.centerX,
this.centerY,
Array.isArray(spokeConfig.connectorColors) ? spokeConfig.connectorColors[i] : spokeConfig.connectorColors
);
parent.add(line);
});
}
revertDataLabelsInner() {
const w2 = this.w;
if (this.donutDataLabels.show) {
const dataLabelsGroup = w2.dom.Paper.findOne(
`.apexcharts-datalabels-group`
);
const dataLabels = this.renderInnerDataLabels(
dataLabelsGroup,
this.donutDataLabels,
{
hollowSize: this.donutSize,
centerX: this.centerX,
centerY: this.centerY,
opacity: this.donutDataLabels.show
}
);
const elPie = w2.dom.Paper.findOne(
".apexcharts-radialbar, .apexcharts-pie"
);
elPie.add(dataLabels);
}
}
}
const RADAR_HIT_AREA_SIZE = 5;
class Radar {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.ctx = ctx;
this.w = w2;
this.chartType = this.w.config.chart.type;
this.initialAnim = this.w.config.chart.animations.enabled;
this.dynamicAnim = this.initialAnim && this.w.config.chart.animations.dynamicAnimation.enabled;
this.animDur = 0;
this.graphics = new Graphics(this.w);
this.lineColorArr = w2.globals.stroke.colors !== void 0 ? w2.globals.stroke.colors : w2.globals.colors;
this.defaultSize = w2.globals.svgHeight < w2.globals.svgWidth ? w2.layout.gridHeight : w2.layout.gridWidth;
this.isLog = w2.config.yaxis[0].logarithmic;
this.logBase = w2.config.yaxis[0].logBase;
this.coreUtils = new CoreUtils(this.w);
this.maxValue = this.isLog ? this.coreUtils.getLogVal(this.logBase, w2.globals.maxY, 0) : w2.globals.maxY;
this.minValue = this.isLog ? this.coreUtils.getLogVal(this.logBase, this.w.globals.minY, 0) : w2.globals.minY;
this.polygons = w2.config.plotOptions.radar.polygons;
this.strokeWidth = w2.config.stroke.show ? w2.config.stroke.width : 0;
this.size = this.defaultSize / 2.1 - this.strokeWidth - w2.config.chart.dropShadow.blur;
if (w2.config.xaxis.labels.show) {
this.size = this.size - w2.layout.xAxisLabelsWidth / 1.75;
}
if (w2.config.plotOptions.radar.size !== void 0) {
this.size = w2.config.plotOptions.radar.size;
}
this.dataRadiusOfPercent = /** @type {any} */
[];
this.dataRadius = /** @type {any} */
[];
this.angleArr = /** @type {any} */
[];
this.dataPointsLen = 0;
this.disAngle = 0;
this.yaxisLabelsTextsPos = [];
}
/**
* @param {any[]} series
*/
draw(series) {
const w2 = this.w;
const fill = new Fill(this.w);
const allSeries = [];
const dataLabels = new DataLabels(this.w, this.ctx);
if (series.length) {
this.dataPointsLen = series[w2.globals.maxValsInArrayIndex].length;
}
this.disAngle = Math.PI * 2 / this.dataPointsLen;
const halfW = w2.layout.gridWidth / 2;
const halfH = w2.layout.gridHeight / 2;
const translateX = halfW + w2.config.plotOptions.radar.offsetX;
const translateY = halfH + w2.config.plotOptions.radar.offsetY;
const ret = this.graphics.group({
class: "apexcharts-radar-series apexcharts-plot-series",
transform: `translate(${translateX || 0}, ${translateY || 0})`
});
let dataPointsPos = [];
let elPointsMain = null;
let elDataPointsMain = null;
this.yaxisLabels = this.graphics.group({
class: "apexcharts-yaxis"
});
series.forEach((s, i) => {
const longestSeries = s.length === w2.globals.dataPoints;
const elSeries = this.graphics.group().attr({
class: `apexcharts-series`,
"data:longestSeries": longestSeries,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[i]),
rel: i + 1,
"data:realIndex": i
});
this.dataRadiusOfPercent[i] = [];
this.dataRadius[i] = [];
this.angleArr[i] = [];
s.forEach((dv, j) => {
const range = Math.abs(this.maxValue - this.minValue);
dv = dv - this.minValue;
if (this.isLog) {
dv = this.coreUtils.getLogVal(this.logBase, dv, 0);
}
this.dataRadiusOfPercent[i][j] = dv / range;
this.dataRadius[i][j] = this.dataRadiusOfPercent[i][j] * this.size;
this.angleArr[i][j] = j * this.disAngle;
});
dataPointsPos = this.getDataPointsPos(
this.dataRadius[i],
this.angleArr[i]
);
const paths = this.createPaths(dataPointsPos, {
x: 0,
y: 0
});
elPointsMain = this.graphics.group({
class: "apexcharts-series-markers-wrap apexcharts-element-hidden"
});
elDataPointsMain = this.graphics.group({
class: `apexcharts-datalabels`,
"data:realIndex": i
});
w2.globals.delayedElements.push({
el: elPointsMain.node,
index: i
});
const defaultRenderedPathOptions = {
i,
realIndex: i,
animationDelay: i,
initialSpeed: w2.config.chart.animations.speed,
dataChangeSpeed: w2.config.chart.animations.dynamicAnimation.speed,
className: `apexcharts-radar`,
shouldClipToGrid: false,
bindEventsOnPaths: false,
stroke: w2.globals.stroke.colors[i],
strokeLineCap: w2.config.stroke.lineCap
};
let pathFrom = null;
if (w2.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(i);
}
for (let p2 = 0; p2 < paths.linePathsTo.length; p2++) {
const renderedLinePath = this.graphics.renderPaths(__spreadProps(__spreadValues({}, defaultRenderedPathOptions), {
pathFrom: pathFrom === null ? paths.linePathsFrom[p2] : pathFrom,
pathTo: paths.linePathsTo[p2],
strokeWidth: Array.isArray(this.strokeWidth) ? this.strokeWidth[i] : this.strokeWidth,
fill: "none",
drawShadow: false
}));
elSeries.add(renderedLinePath);
const pathFill = fill.fillPath({
seriesNumber: i
});
const renderedAreaPath = this.graphics.renderPaths(__spreadProps(__spreadValues({}, defaultRenderedPathOptions), {
pathFrom: pathFrom === null ? paths.areaPathsFrom[p2] : pathFrom,
pathTo: paths.areaPathsTo[p2],
strokeWidth: 0,
fill: pathFill,
drawShadow: false,
// Radial mask: the area fill blooms outward from the radar's center
// (in this group's local coords) instead of the default L→R rect wipe.
drawMask: { type: "radial", cx: 0, cy: 0, r: this.size }
}));
if (w2.config.chart.dropShadow.enabled) {
const filters = new Filters(this.w);
const shadow = w2.config.chart.dropShadow;
filters.dropShadow(
renderedAreaPath,
Object.assign({}, shadow, { noUserSpaceOnUse: true }),
i
);
}
elSeries.add(renderedAreaPath);
}
s.forEach((sj, j) => {
const markers = new Markers(this.w, this.ctx);
const opts = markers.getMarkerConfig({
cssClass: "apexcharts-marker",
seriesIndex: i,
dataPointIndex: j
});
if (!opts.pSize) {
opts.pSize = RADAR_HIT_AREA_SIZE;
opts.pointFillColor = "transparent";
opts.pointStrokeColor = "transparent";
opts.pointStrokeWidth = 0;
}
const point = this.graphics.drawMarker(
dataPointsPos[j].x,
dataPointsPos[j].y,
opts
);
point.attr("rel", j);
point.attr("j", j);
point.attr("index", i);
point.node.setAttribute("default-marker-size", opts.pSize);
const elPointsWrap = this.graphics.group({
class: "apexcharts-series-markers"
});
if (elPointsWrap) {
elPointsWrap.add(point);
}
elPointsMain.add(elPointsWrap);
elSeries.add(elPointsMain);
const dataLabelsConfig = w2.config.dataLabels;
if (dataLabelsConfig.enabled) {
const text = dataLabelsConfig.formatter(w2.seriesData.series[i][j], {
seriesIndex: i,
dataPointIndex: j,
w: w2
});
dataLabels.plotDataLabelsText({
x: dataPointsPos[j].x,
y: dataPointsPos[j].y,
text,
textAnchor: "middle",
i,
j: i,
// `j` above is the series index (kept for the color lookup), so
// pass the real data point index for per-point offsets
seriesIndex: i,
dataPointIndex: j,
parent: elDataPointsMain,
offsetCorrection: false,
dataLabelsConfig: __spreadValues({}, dataLabelsConfig)
});
}
elSeries.add(elDataPointsMain);
});
allSeries.push(elSeries);
});
this.drawPolygons({
parent: ret
});
if (w2.config.xaxis.labels.show) {
const xaxisTexts = this.drawXAxisTexts();
ret.add(xaxisTexts);
}
allSeries.forEach((elS) => {
ret.add(elS);
});
ret.add(this.yaxisLabels);
return ret;
}
/**
* @param {Record<string, any>} opts
*/
drawPolygons(opts) {
const w2 = this.w;
const { parent } = opts;
const helpers = new CircularChartsHelpers(this.w);
const yaxisTexts = w2.globals.yAxisScale[0].result.reverse();
const layers = yaxisTexts.length;
const radiusSizes = [];
const layerDis = this.size / (layers - 1);
for (let i = 0; i < layers; i++) {
radiusSizes[i] = layerDis * i;
}
radiusSizes.reverse();
const polygonStrings = [];
const lines = [];
radiusSizes.forEach((radiusSize, r) => {
const polygon = Utils.getPolygonPos(radiusSize, this.dataPointsLen);
let string = "";
polygon.forEach((p2, i) => {
if (r === 0) {
const line = this.graphics.drawLine(
p2.x,
p2.y,
0,
0,
Array.isArray(this.polygons.connectorColors) ? this.polygons.connectorColors[i] : this.polygons.connectorColors
);
lines.push(line);
}
if (i === 0) {
this.yaxisLabelsTextsPos.push({
x: p2.x,
y: p2.y
});
}
string += p2.x + "," + p2.y + " ";
});
polygonStrings.push(string);
});
polygonStrings.forEach((p2, i) => {
const strokeColors = this.polygons.strokeColors;
const strokeWidth = this.polygons.strokeWidth;
const polygon = this.graphics.drawPolygon(
p2,
Array.isArray(strokeColors) ? strokeColors[i] : strokeColors,
Array.isArray(strokeWidth) ? strokeWidth[i] : strokeWidth,
w2.globals.radarPolygons.fill.colors[i]
);
parent.add(polygon);
});
lines.forEach((l) => {
parent.add(l);
});
if (w2.config.yaxis[0].show) {
this.yaxisLabelsTextsPos.forEach(
(p2, i) => {
const yText = helpers.drawYAxisTexts(p2.x, p2.y, i, yaxisTexts[i]);
this.yaxisLabels.add(yText);
}
);
}
}
drawXAxisTexts() {
const w2 = this.w;
const xaxisLabelsConfig = w2.config.xaxis.labels;
const elXAxisWrap = this.graphics.group({
class: "apexcharts-xaxis"
});
const polygonPos = Utils.getPolygonPos(this.size, this.dataPointsLen);
w2.labelData.labels.forEach((label, i) => {
const formatter = w2.config.xaxis.labels.formatter;
const dataLabels = new DataLabels(this.w, this.ctx);
if (polygonPos[i]) {
const textPos = this.getTextPos(polygonPos[i], this.size);
const text = formatter(label, {
seriesIndex: -1,
dataPointIndex: i,
w: w2
});
const dataLabelText = dataLabels.plotDataLabelsText({
x: textPos.newX,
y: textPos.newY,
text,
textAnchor: textPos.textAnchor,
i,
j: i,
parent: elXAxisWrap,
className: "apexcharts-xaxis-label",
color: Array.isArray(xaxisLabelsConfig.style.colors) && xaxisLabelsConfig.style.colors[i] ? xaxisLabelsConfig.style.colors[i] : "#a8a8a8",
dataLabelsConfig: __spreadValues({
textAnchor: textPos.textAnchor,
dropShadow: { enabled: false }
}, xaxisLabelsConfig),
offsetCorrection: false
});
dataLabelText.on("click", (e) => {
if (typeof w2.config.chart.events.xAxisLabelClick === "function") {
const opts = Object.assign({}, w2, {
labelIndex: i
});
w2.config.chart.events.xAxisLabelClick(e, this.ctx, opts);
}
});
}
});
return elXAxisWrap;
}
/**
* @param {Array<Record<string, any>>} pos
* @param {Record<string, any>} origin
*/
createPaths(pos, origin) {
const linePathsTo = [];
let linePathsFrom = [];
const areaPathsTo = [];
let areaPathsFrom = [];
if (pos.length) {
linePathsFrom = [this.graphics.move(origin.x, origin.y)];
areaPathsFrom = [this.graphics.move(origin.x, origin.y)];
let linePathTo = this.graphics.move(pos[0].x, pos[0].y);
let areaPathTo = this.graphics.move(pos[0].x, pos[0].y);
pos.forEach((p2, i) => {
linePathTo += this.graphics.line(p2.x, p2.y);
areaPathTo += this.graphics.line(p2.x, p2.y);
if (i === pos.length - 1) {
linePathTo += "Z";
areaPathTo += "Z";
}
});
linePathsTo.push(linePathTo);
areaPathsTo.push(areaPathTo);
}
return {
linePathsFrom,
linePathsTo,
areaPathsFrom,
areaPathsTo
};
}
/**
* @param {Record<string, any>} pos
* @param {number} polygonSize
*/
getTextPos(pos, polygonSize) {
const limit = 10;
let textAnchor = "middle";
let newX = pos.x;
let newY = pos.y;
if (Math.abs(pos.x) >= limit) {
if (pos.x > 0) {
textAnchor = "start";
newX += 10;
} else if (pos.x < 0) {
textAnchor = "end";
newX -= 10;
}
} else {
textAnchor = "middle";
}
if (Math.abs(pos.y) >= polygonSize - limit) {
if (pos.y < 0) {
newY -= 10;
} else if (pos.y > 0) {
newY += 10;
}
}
return {
textAnchor,
newX,
newY
};
}
/**
* @param {number} realIndex
*/
getPreviousPath(realIndex) {
const w2 = this.w;
let pathFrom = null;
for (let pp = 0; pp < w2.globals.previousPaths.length; pp++) {
const gpp = w2.globals.previousPaths[pp];
if (gpp.paths.length > 0 && parseInt(gpp.realIndex, 10) === parseInt(String(realIndex), 10)) {
if (typeof w2.globals.previousPaths[pp].paths[0] !== "undefined") {
pathFrom = w2.globals.previousPaths[pp].paths[0].d;
}
}
}
return pathFrom;
}
/**
* @param {any[]} dataRadiusArr
* @param {any[]} angleArr
*/
getDataPointsPos(dataRadiusArr, angleArr, dataPointsLen = this.dataPointsLen) {
dataRadiusArr = dataRadiusArr || [];
angleArr = angleArr || [];
const dataPointsPosArray = [];
for (let j = 0; j < dataPointsLen; j++) {
const curPointPos = {};
curPointPos.x = dataRadiusArr[j] * Math.sin(angleArr[j]);
curPointPos.y = -dataRadiusArr[j] * Math.cos(angleArr[j]);
dataPointsPosArray.push(curPointPos);
}
return dataPointsPosArray;
}
}
class Radial extends Pie {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
super(w2, ctx);
this.ctx = ctx;
this.w = w2;
this.animBeginArr = [0];
this.animDur = 0;
this.startAngle = w2.config.plotOptions.radialBar.startAngle;
this.endAngle = w2.config.plotOptions.radialBar.endAngle;
this.totalAngle = Math.abs(
w2.config.plotOptions.radialBar.endAngle - w2.config.plotOptions.radialBar.startAngle
);
this.trackStartAngle = w2.config.plotOptions.radialBar.track.startAngle;
this.trackEndAngle = w2.config.plotOptions.radialBar.track.endAngle;
this.barLabels = this.w.config.plotOptions.radialBar.barLabels;
this.donutDataLabels = this.w.config.plotOptions.radialBar.dataLabels;
this.radialDataLabels = this.donutDataLabels;
if (!this.trackStartAngle) this.trackStartAngle = this.startAngle;
if (!this.trackEndAngle) this.trackEndAngle = this.endAngle;
if (this.endAngle === 360) this.endAngle = 359.99;
this.margin = parseInt(w2.config.plotOptions.radialBar.track.margin, 10);
this.onBarLabelClick = this.onBarLabelClick.bind(this);
}
/**
* @param {any[]} series
*/
draw(series) {
var _a;
const w2 = this.w;
const graphics = new Graphics(this.w);
const ret = graphics.group({
class: "apexcharts-radialbar"
});
if (w2.globals.noData) return ret;
const elSeries = graphics.group();
const centerY = this.defaultSize / 2;
const centerX = w2.layout.gridWidth / 2;
let size = this.defaultSize / 2.05;
if (!w2.config.chart.sparkline.enabled) {
size = size - w2.config.stroke.width - w2.config.chart.dropShadow.blur;
}
const colorArr = w2.globals.fill.colors;
const rb = w2.config.plotOptions.radialBar;
const hasBands = Array.isArray(rb.bands) && rb.bands.length > 0;
const hideTrack = hasBands && rb.bandsStyle && rb.bandsStyle.hideTrackWhenPresent;
const isNeedleShape = rb.shape === "needle";
if (rb.track.show && !hideTrack) {
const elTracks = this.drawTracks({
size,
centerX,
centerY,
colorArr,
series
});
elSeries.add(elTracks);
}
if (hasBands) {
const elBands = this.drawBands({
size,
centerX,
centerY,
series
});
elSeries.add(elBands);
}
const elG = this.drawArcs({
size,
centerX,
centerY,
colorArr,
series,
// When `needle.showValueArc` is true, render both the filled value-arc
// and the needle on top — required for gauges that want a progress
// ring with a pointer indicator (default still hides the arc when in
// needle shape, preserving prior behavior).
skipValueArc: isNeedleShape && !((_a = rb.needle) == null ? void 0 : _a.showValueArc)
});
if (rb.ticks && rb.ticks.show) {
const elTicks = this.drawTicks({
size,
centerX,
centerY,
series
});
const isInitialMount = this.initialAnim && !w2.globals.dataChanged && !w2.globals.resized;
if (isInitialMount && Environment.isBrowser() && w2.globals.shouldAnimate) {
const ticksNode = elTicks.node;
ticksNode.style.opacity = "0";
ticksNode.style.transition = "opacity 280ms ease-out";
const sweepDur = w2.config.chart.animations.speed || 800;
setTimeout(() => {
ticksNode.style.opacity = "1";
}, sweepDur);
}
elSeries.add(elTicks);
}
if (isNeedleShape) {
const elNeedle = this.drawNeedle({
size,
centerX,
centerY,
series
});
elSeries.add(elNeedle);
}
let totalAngle = 360;
if (w2.config.plotOptions.radialBar.startAngle < 0) {
totalAngle = this.totalAngle;
}
const angleRatio = (360 - totalAngle) / 360;
w2.globals.radialSize = size - size * angleRatio;
if (this.radialDataLabels.value.show) {
const offset = Math.max(
this.radialDataLabels.value.offsetY,
this.radialDataLabels.name.offsetY
);
w2.globals.radialSize += offset * angleRatio;
}
elSeries.add(elG.g);
if (w2.config.plotOptions.radialBar.hollow.position === "front") {
elG.g.add(elG.elHollow);
if (elG.dataLabels) {
elG.g.add(elG.dataLabels);
}
}
ret.add(elSeries);
return ret;
}
/**
* @param {Record<string, any>} opts
*/
drawTracks(opts) {
const w2 = this.w;
const graphics = new Graphics(this.w);
const g2 = graphics.group({
class: "apexcharts-tracks"
});
const filters = new Filters(this.w);
const fill = new Fill(this.w);
const strokeWidth = this.getStrokeWidth(opts);
opts.size = opts.size - strokeWidth / 2;
for (let i = 0; i < opts.series.length; i++) {
const elRadialBarTrack = graphics.group({
class: "apexcharts-radialbar-track apexcharts-track"
});
g2.add(elRadialBarTrack);
elRadialBarTrack.attr({
rel: i + 1
});
opts.size = opts.size - strokeWidth - this.margin;
const trackConfig = w2.config.plotOptions.radialBar.track;
const pathFill = fill.fillPath({
seriesNumber: 0,
size: opts.size,
fillColors: Array.isArray(trackConfig.background) ? trackConfig.background[i] : trackConfig.background,
solid: true
});
const startAngle = this.trackStartAngle;
let endAngle = this.trackEndAngle;
if (Math.abs(endAngle) + Math.abs(startAngle) >= 360)
endAngle = 360 - Math.abs(this.startAngle) - 0.1;
const elPath = graphics.drawPath({
d: "",
stroke: pathFill,
strokeWidth: strokeWidth * parseInt(trackConfig.strokeWidth, 10) / 100,
fill: "none",
strokeOpacity: trackConfig.opacity,
classes: "apexcharts-radialbar-area"
});
if (trackConfig.dropShadow.enabled) {
const shadow = trackConfig.dropShadow;
filters.dropShadow(elPath, shadow);
}
elRadialBarTrack.add(elPath);
elPath.attr("id", "apexcharts-radialbarTrack-" + i);
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: 0,
dur: 0,
isTrack: true
});
}
return g2;
}
/**
* @param {Record<string, any>} opts
*/
drawArcs(opts) {
var _a;
const w2 = this.w;
const graphics = new Graphics(this.w);
const fill = new Fill(this.w);
const filters = new Filters(this.w);
const g2 = graphics.group();
const strokeWidth = this.getStrokeWidth(opts);
opts.size = opts.size - strokeWidth / 2;
let hollowFillID = w2.config.plotOptions.radialBar.hollow.background;
const hollowSize = opts.size - strokeWidth * opts.series.length - this.margin * opts.series.length - strokeWidth * parseInt(w2.config.plotOptions.radialBar.track.strokeWidth, 10) / 100 / 2;
const hollowRadius = hollowSize - w2.config.plotOptions.radialBar.hollow.margin;
if (w2.config.plotOptions.radialBar.hollow.image !== void 0) {
hollowFillID = this.drawHollowImage(opts, g2, hollowSize, hollowFillID);
}
const elHollow = this.drawHollow({
size: hollowRadius,
centerX: opts.centerX,
centerY: opts.centerY,
fill: hollowFillID ? hollowFillID : "transparent"
});
if (w2.config.plotOptions.radialBar.hollow.dropShadow.enabled) {
const shadow = w2.config.plotOptions.radialBar.hollow.dropShadow;
filters.dropShadow(elHollow, shadow);
}
let shown = 1;
if (!this.radialDataLabels.total.show && w2.seriesData.series.length > 1) {
shown = 0;
}
let dataLabels = null;
if (this.radialDataLabels.show) {
const dataLabelsGroup = w2.dom.Paper.findOne(
`.apexcharts-datalabels-group`
);
dataLabels = this.renderInnerDataLabels(
dataLabelsGroup,
this.radialDataLabels,
{
hollowSize,
centerX: opts.centerX,
centerY: opts.centerY,
opacity: shown
}
);
}
if (w2.config.plotOptions.radialBar.hollow.position === "back") {
g2.add(elHollow);
if (dataLabels) {
g2.add(dataLabels);
}
}
let reverseLoop = false;
if (w2.config.plotOptions.radialBar.inverseOrder) {
reverseLoop = true;
}
const morphActive = ((_a = this.ctx.morphTypeChange) == null ? void 0 : _a.isActive()) === true;
for (let i = reverseLoop ? opts.series.length - 1 : 0; reverseLoop ? i >= 0 : i < opts.series.length; reverseLoop ? i-- : i++) {
const elRadialBarArc = graphics.group({
class: `apexcharts-series apexcharts-radial-series`,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[i])
});
g2.add(elRadialBarArc);
elRadialBarArc.attr({
rel: i + 1,
"data:realIndex": i
});
Series.addCollapsedClassToSeries(this.w, elRadialBarArc, i);
opts.size = opts.size - strokeWidth - this.margin;
const pathFill = fill.fillPath({
seriesNumber: i,
size: opts.size,
value: opts.series[i]
});
const startAngle = this.startAngle;
let prevStartAngle;
const rb = w2.config.plotOptions.radialBar;
const domainMin = typeof rb.min === "number" ? rb.min : 0;
const domainMax = typeof rb.max === "number" ? rb.max : 100;
const domainSpan = domainMax === domainMin ? 1 : domainMax - domainMin;
const valueToFraction = (v2) => {
const clamped = Math.min(Math.max(v2, domainMin), domainMax);
return Math.max(0, (clamped - domainMin) / domainSpan);
};
const dataValue = valueToFraction(Utils.negToZero(opts.series[i]));
let endAngle = Math.round(this.totalAngle * dataValue) + this.startAngle;
let prevEndAngle;
if (w2.globals.dataChanged) {
prevStartAngle = this.startAngle;
prevEndAngle = Math.round(
this.totalAngle * valueToFraction(Utils.negToZero(w2.globals.previousPaths[i]))
) + prevStartAngle;
}
const currFullAngle = Math.abs(endAngle) + Math.abs(startAngle);
if (currFullAngle > 360) {
endAngle = endAngle - 0.01;
}
const prevFullAngle = Math.abs(prevEndAngle) + Math.abs(prevStartAngle);
if (prevFullAngle > 360) {
prevEndAngle = prevEndAngle - 0.01;
}
const angle = endAngle - startAngle;
const dashArray = Array.isArray(w2.config.stroke.dashArray) ? w2.config.stroke.dashArray[i] : w2.config.stroke.dashArray;
const morphFrom = morphActive ? this.ctx.morphTypeChange.getInitialPathFor(i, 0) : null;
const morphFromType = morphActive ? this.ctx.morphTypeChange.getFromType() : null;
const morphFromFilled = !!morphFrom && (morphFromType === "bar" || morphFromType === "funnel" || morphFromType === "pyramid" || morphFromType === "pie" || morphFromType === "donut" || morphFromType === "polarArea");
const elPath = graphics.drawPath({
d: morphFrom || "",
stroke: morphFromFilled ? "transparent" : opts.skipValueArc ? "transparent" : pathFill,
strokeWidth: morphFromFilled ? 0 : opts.skipValueArc ? 0 : strokeWidth,
fill: morphFromFilled ? pathFill : "none",
fillOpacity: w2.config.fill.opacity,
classes: "apexcharts-radialbar-area apexcharts-radialbar-slice-" + i,
strokeDashArray: dashArray
});
const radialMidAngle = startAngle + angle / 2;
const radialArcCenter = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
opts.size,
radialMidAngle
);
Graphics.setAttrs(elPath.node, {
"data:angle": angle,
"data:value": opts.series[i],
"data:cx": radialArcCenter.x,
"data:cy": radialArcCenter.y
});
if (w2.config.chart.dropShadow.enabled) {
const shadow = w2.config.chart.dropShadow;
filters.dropShadow(elPath, shadow, i);
}
filters.setSelectionFilter(elPath, 0, i);
this.addListeners(elPath, this.radialDataLabels);
elRadialBarArc.add(elPath);
elPath.attr({
index: 0,
j: i
});
if (this.barLabels.enabled) {
const barStartCords = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
opts.size,
startAngle
);
const text = this.barLabels.formatter(w2.seriesData.seriesNames[i], {
seriesIndex: i,
w: w2
});
const classes = ["apexcharts-radialbar-label"];
if (!this.barLabels.onClick) {
classes.push("apexcharts-no-click");
}
let textColor = this.barLabels.useSeriesColors ? w2.globals.colors[i] : w2.config.chart.foreColor;
if (!textColor) {
textColor = w2.config.chart.foreColor;
}
const x = barStartCords.x + this.barLabels.offsetX;
const y = barStartCords.y + this.barLabels.offsetY;
const elText = graphics.drawText({
x,
y,
text,
textAnchor: "end",
dominantBaseline: "middle",
fontFamily: this.barLabels.fontFamily,
fontWeight: this.barLabels.fontWeight,
fontSize: this.barLabels.fontSize,
foreColor: textColor,
cssClass: classes.join(" ")
});
elText.on("click", this.onBarLabelClick);
elText.attr({
rel: i + 1
});
if (startAngle !== 0) {
elText.attr({
"transform-origin": `${x} ${y}`,
transform: `rotate(${startAngle} 0 0)`
});
}
elRadialBarArc.add(elText);
}
let dur = 0;
if (this.initialAnim && !w2.globals.resized && !w2.globals.dataChanged) {
dur = w2.config.chart.animations.speed;
}
if (w2.globals.dataChanged) {
dur = w2.config.chart.animations.dynamicAnimation.speed;
}
this.animDur = dur / (opts.series.length * 1.2) + this.animDur;
this.animBeginArr.push(this.animDur);
if (morphActive && morphFrom) {
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
const actualArcD = this.getPiePath({
me: this,
startAngle,
angle,
size: opts.size
});
if (morphFromFilled) {
const targetD = this.ctx.morphTypeChange.buildRingSegmentPath(
opts.centerX,
opts.centerY,
opts.size,
strokeWidth,
startAngle,
startAngle + angle
);
elPath.animate(morphSpeed).plot(targetD, "polygons").after(
/** @this {any} */
function() {
this.attr({
d: actualArcD,
fill: "none",
stroke: opts.skipValueArc ? "transparent" : pathFill,
"stroke-width": opts.skipValueArc ? 0 : strokeWidth
});
}
);
} else {
elPath.animate(morphSpeed).plot(actualArcD, "polygons").attr({ "stroke-width": strokeWidth });
}
} else {
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
prevEndAngle,
prevStartAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: this.animBeginArr,
dur,
shouldSetPrevPaths: true
});
}
}
return {
g: g2,
elHollow,
dataLabels
};
}
/**
* Map a domain value (between `min` and `max`) to the corresponding angle
* in the gauge's `startAngle`..`endAngle` range. Values outside the
* domain are clamped.
*
* @param {number} value
* @returns {number}
*/
_angleAtValue(value) {
const rb = this.w.config.plotOptions.radialBar;
const min = typeof rb.min === "number" ? rb.min : 0;
const max = typeof rb.max === "number" ? rb.max : 100;
const safeMax = max === min ? min + 1 : max;
const clamped = Math.max(min, Math.min(safeMax, Number(value)));
const t = (clamped - min) / (safeMax - min);
return this.startAngle + t * (this.endAngle - this.startAngle);
}
/**
* Build an SVG arc path from `startAngle` to `endAngle` at radius `r`
* around `(cx, cy)`. Angles are in degrees, with 0° at the top.
* Used by drawBands; mirrors the `M ... A ... ` form used elsewhere.
*
* @param {number} cx
* @param {number} cy
* @param {number} r
* @param {number} startAngle
* @param {number} endAngle
* @returns {string}
*/
_describeArc(cx, cy, r, startAngle, endAngle) {
const start = Utils.polarToCartesian(cx, cy, r, endAngle);
const end = Utils.polarToCartesian(cx, cy, r, startAngle);
const sweep = endAngle - startAngle;
const largeArc = Math.abs(sweep) > 180 ? 1 : 0;
return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 0 ${end.x} ${end.y}`;
}
/**
* Draw threshold bands as colored arc segments along the gauge arc.
* Bands sit behind the value-arc and tick marks. Used for gauges that
* indicate ranges like 0-30 critical / 30-70 warning / 70-100 healthy.
*
* @param {Record<string, any>} opts
*/
drawBands(opts) {
const w2 = this.w;
const graphics = new Graphics(this.w);
const rb = w2.config.plotOptions.radialBar;
const bands = rb.bands || [];
const g2 = graphics.group({
class: "apexcharts-gauge-bands"
});
const strokeWidth = this.getStrokeWidth(opts);
const radius = opts.size - strokeWidth / 2 - strokeWidth - this.margin;
const bandStroke = strokeWidth * parseInt(rb.bandsStyle.strokeWidth, 10) / 100;
const min = typeof rb.min === "number" ? rb.min : 0;
const max = typeof rb.max === "number" ? rb.max : 100;
const gapDeg = max === min ? 0 : (rb.bandsStyle.gap || 0) * ((this.endAngle - this.startAngle) / (max - min));
for (let b2 = 0; b2 < bands.length; b2++) {
const band = bands[b2];
if (band.from === void 0 || band.to === void 0) continue;
const a1 = this._angleAtValue(band.from);
const a2 = this._angleAtValue(band.to);
const startA = Math.min(a1, a2) + gapDeg / 2;
const endA = Math.max(a1, a2) - gapDeg / 2;
if (endA - startA <= 0) continue;
const elBand = graphics.drawPath({
d: this._describeArc(opts.centerX, opts.centerY, radius, startA, endA),
stroke: band.color || "#ccc",
strokeWidth: bandStroke,
fill: "none",
strokeLinecap: rb.bandsStyle.linecap || "butt",
classes: "apexcharts-gauge-band"
});
elBand.node.setAttribute("data-band-index", String(b2));
g2.add(elBand);
}
return g2;
}
/**
* Draw tick marks (major + minor) along the gauge arc, with optional
* value labels at each major tick.
*
* @param {Record<string, any>} opts
*/
drawTicks(opts) {
var _a, _b, _c, _d;
const w2 = this.w;
const graphics = new Graphics(this.w);
const rb = w2.config.plotOptions.radialBar;
const ticks = rb.ticks;
const g2 = graphics.group({ class: "apexcharts-gauge-ticks" });
const strokeWidth = this.getStrokeWidth(opts);
const radius = opts.size - strokeWidth / 2 - strokeWidth - this.margin;
const min = typeof rb.min === "number" ? rb.min : 0;
const max = typeof rb.max === "number" ? rb.max : 100;
const majorCount = Math.max(2, (_b = (_a = ticks.major) == null ? void 0 : _a.count) != null ? _b : 11);
const minorCount = Math.max(0, (_d = (_c = ticks.minor) == null ? void 0 : _c.count) != null ? _d : 0);
const drawTickAt = (value, cfg, isMajor) => {
var _a2, _b2, _c2;
const angle = this._angleAtValue(value);
const length = (_a2 = cfg.length) != null ? _a2 : 8;
const inner = cfg.placement === "inside" ? radius - length : radius;
const outer = cfg.placement === "inside" ? radius : radius + length;
const p1 = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
inner,
angle
);
const p2 = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
outer,
angle
);
const line = graphics.drawLine(
p1.x,
p1.y,
p2.x,
p2.y,
cfg.color || (isMajor ? "#666" : "#999"),
0,
cfg.width || (isMajor ? 2 : 1)
);
g2.add(line);
if (isMajor && ((_b2 = ticks.labels) == null ? void 0 : _b2.show)) {
const labelRadius = (cfg.placement === "inside" ? inner : outer) + (cfg.placement === "inside" ? -1 : 1) * ((_c2 = ticks.labels.offset) != null ? _c2 : 6);
const labelPos = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
labelRadius,
angle
);
const labelText = typeof ticks.labels.formatter === "function" ? ticks.labels.formatter(value) : String(value);
const elText = graphics.drawText({
x: labelPos.x,
y: labelPos.y,
text: labelText,
textAnchor: "middle",
dominantBaseline: "middle",
fontFamily: ticks.labels.fontFamily,
fontSize: ticks.labels.fontSize,
fontWeight: ticks.labels.fontWeight,
foreColor: ticks.labels.color,
cssClass: "apexcharts-gauge-tick-label"
});
g2.add(elText);
}
};
for (let m2 = 0; m2 < majorCount; m2++) {
const t = m2 / (majorCount - 1);
const value = min + t * (max - min);
drawTickAt(value, ticks.major || {}, true);
if (m2 < majorCount - 1 && minorCount > 0) {
for (let n = 1; n <= minorCount; n++) {
const tMinor = (m2 + n / (minorCount + 1)) / (majorCount - 1);
const minorValue = min + tMinor * (max - min);
drawTickAt(minorValue, ticks.minor || {}, false);
}
}
}
return g2;
}
/**
* Draw a rotating needle pointing at the current series value. Only
* called when `plotOptions.radialBar.shape === 'needle'`. The needle is
* a tapered polygon inside a `<g>` whose rotation transform is animated
* from `startAngle` to the value's angle.
*
* Renders a single needle for the first series value (gauge use case).
* Additional series are ignored — drilled-down multi-series gauges are
* out of scope for this iteration.
*
* @param {Record<string, any>} opts
*/
drawNeedle(opts) {
var _a, _b, _c, _d, _e;
const w2 = this.w;
const graphics = new Graphics(this.w);
const rb = w2.config.plotOptions.radialBar;
const cfg = rb.needle || {};
const g2 = graphics.group({ class: "apexcharts-gauge-needle" });
if (!opts.series || opts.series.length === 0) return g2;
const strokeWidth = this.getStrokeWidth(opts);
const arcRadius = opts.size - strokeWidth / 2 - strokeWidth - this.margin;
const length = typeof cfg.length === "string" && cfg.length.endsWith("%") ? arcRadius * parseInt(cfg.length, 10) / 100 : Number(cfg.length || arcRadius * 0.85);
const baseW = (_a = cfg.baseWidth) != null ? _a : 4;
const tipW = (_b = cfg.tipWidth) != null ? _b : 1;
const color = cfg.color || "#333";
const cx = opts.centerX;
const needleOffsetY = Number((_c = cfg.offsetY) != null ? _c : 0);
const cy = opts.centerY + needleOffsetY;
const path = `M ${cx + baseW / 2} ${cy} A ${baseW / 2} ${baseW / 2} 0 0 1 ${cx - baseW / 2} ${cy} L ${cx - tipW / 2} ${cy - length} L ${cx + tipW / 2} ${cy - length} Z`;
const elNeedle = graphics.drawPath({
d: path,
stroke: color,
strokeWidth: 0,
fill: color,
classes: "apexcharts-gauge-needle-shape"
});
g2.add(elNeedle);
const value = Number(opts.series[0]);
const targetAngle = this._angleAtValue(value);
const isInitialMount = this.initialAnim && !w2.globals.dataChanged && !w2.globals.resized;
const ctx = (
/** @type {any} */
this.ctx
);
const fromAngle = typeof ctx._lastNeedleAngle === "number" ? ctx._lastNeedleAngle : this.startAngle;
ctx._lastNeedleAngle = targetAngle;
const shouldAnimate = Environment.isBrowser() && w2.globals.shouldAnimate && (isInitialMount || w2.globals.dataChanged);
if (shouldAnimate && fromAngle !== targetAngle) {
const node = g2.node;
node.setAttribute("transform-origin", `${cx} ${cy}`);
node.setAttribute("transform", `rotate(${fromAngle})`);
const speed = ((_d = cfg.animation) == null ? void 0 : _d.duration) && Number(cfg.animation.duration) || cfg.animationSpeed && Number(cfg.animationSpeed) || ((_e = w2.config.chart.animations.dynamicAnimation) == null ? void 0 : _e.speed) || w2.config.chart.animations.speed || 800;
const c1 = 1.70158;
const c3 = c1 + 1;
const easeOutBack2 = (t) => 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
const easeOutCubic2 = (t) => 1 - Math.pow(1 - t, 3);
const ease = isInitialMount ? easeOutBack2 : easeOutCubic2;
if (w2.globals.radialNeedleRAF != null) {
BrowserAPIs.cancelAnimationFrame(w2.globals.radialNeedleRAF);
w2.globals.radialNeedleRAF = null;
}
const startAt = performance.now();
const step = (now) => {
if (this.w.globals.isDestroyed) {
w2.globals.radialNeedleRAF = null;
return;
}
const t = Math.max(0, Math.min(1, (now - startAt) / speed));
const angle = fromAngle + (targetAngle - fromAngle) * ease(t);
node.setAttribute("transform", `rotate(${angle})`);
if (t < 1) {
w2.globals.radialNeedleRAF = BrowserAPIs.requestAnimationFrame(step);
} else {
w2.globals.radialNeedleRAF = null;
}
};
w2.globals.radialNeedleRAF = BrowserAPIs.requestAnimationFrame(step);
} else {
g2.attr({
"transform-origin": `${cx} ${cy}`,
transform: `rotate(${targetAngle})`
});
}
return g2;
}
/**
* @param {Record<string, any>} opts
*/
drawHollow(opts) {
var _a;
const graphics = new Graphics(this.w);
const hollow = this.w.config.plotOptions.radialBar.hollow;
const circle = graphics.drawCircle(opts.size * 2);
const attrs = {
class: "apexcharts-radialbar-hollow",
cx: opts.centerX,
cy: opts.centerY,
r: opts.size,
fill: opts.fill
};
if (hollow.stroke || hollow.strokeDasharray) {
attrs.stroke = hollow.stroke || "transparent";
attrs["stroke-width"] = (_a = hollow.strokeWidth) != null ? _a : 1;
if (hollow.strokeDasharray) {
attrs["stroke-dasharray"] = hollow.strokeDasharray;
}
}
circle.attr(attrs);
return circle;
}
/**
* @param {Record<string, any>} opts
* @param {any} g
* @param {number} hollowSize
* @param {string} hollowFillID
*/
drawHollowImage(opts, g2, hollowSize, hollowFillID) {
const w2 = this.w;
const fill = new Fill(this.w);
const randID = Utils.randomId();
const hollowFillImg = w2.config.plotOptions.radialBar.hollow.image;
if (w2.config.plotOptions.radialBar.hollow.imageClipped) {
fill.clippedImgArea({
width: hollowSize,
height: hollowSize,
image: hollowFillImg,
patternID: `pattern${w2.globals.cuid}${randID}`
});
hollowFillID = `url(#pattern${w2.globals.cuid}${randID})`;
} else {
const imgWidth = w2.config.plotOptions.radialBar.hollow.imageWidth;
const imgHeight = w2.config.plotOptions.radialBar.hollow.imageHeight;
if (imgWidth === void 0 && imgHeight === void 0) {
const image = w2.dom.Paper.image(
hollowFillImg,
/** @this {any} */
function(loader) {
this.move(
opts.centerX - loader.width / 2 + w2.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY - loader.height / 2 + w2.config.plotOptions.radialBar.hollow.imageOffsetY
);
}
);
g2.add(image);
} else {
const image = w2.dom.Paper.image(
hollowFillImg,
/** @this {any} */
function() {
this.move(
opts.centerX - imgWidth / 2 + w2.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY - imgHeight / 2 + w2.config.plotOptions.radialBar.hollow.imageOffsetY
);
this.size(imgWidth, imgHeight);
}
);
g2.add(image);
}
}
return hollowFillID;
}
/**
* @param {Record<string, any>} opts
*/
getStrokeWidth(opts) {
const w2 = this.w;
return opts.size * (100 - parseInt(w2.config.plotOptions.radialBar.hollow.size, 10)) / 100 / (opts.series.length + 1) - this.margin;
}
/**
* @param {Event} e
*/
onBarLabelClick(e) {
var _a;
const target = (
/** @type {Element} */
e.target
);
const seriesIndex = parseInt((_a = target.getAttribute("rel")) != null ? _a : "", 10) - 1;
const legendClick = this.barLabels.onClick;
const w2 = this.w;
if (legendClick) {
legendClick(w2.seriesData.seriesNames[seriesIndex], { w: w2, seriesIndex });
}
}
}
class RangeBar extends Bar {
/**
* @param {any[]} series
* @param {number} seriesIndex
*/
draw(series, seriesIndex) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
const w2 = this.w;
const graphics = new Graphics(this.w);
this.rangeBarOptions = this.w.config.plotOptions.rangeBar;
this.series = series;
this.seriesRangeStart = w2.rangeData.seriesRangeStart;
this.seriesRangeEnd = w2.rangeData.seriesRangeEnd;
this.barHelpers.initVariables(series);
const ret = graphics.group({
class: "apexcharts-rangebar-series apexcharts-plot-series"
});
for (let i = 0; i < series.length; i++) {
let x, y;
const realIndex = w2.globals.comboCharts ? (
/** @type {any} */
seriesIndex[i]
) : i;
const { columnGroupIndex } = this.barHelpers.getGroupIndex(realIndex);
const elSeries = graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[realIndex]),
rel: i + 1,
"data:realIndex": realIndex
});
Series.addCollapsedClassToSeries(this.w, elSeries, realIndex);
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1;
}
let translationsIndex = 0;
if (this.yRatio.length > 1) {
this.yaxisIndex = /** @type {any} */
w2.globals.seriesYAxisReverseMap[realIndex][0];
translationsIndex = realIndex;
}
const initPositions = this.barHelpers.initialPositions(realIndex);
const {
y: initY,
zeroW,
// zeroW is the baseline where 0 meets x axis
x: initX,
zeroH
// zeroH is the baseline where 0 meets y axis
} = initPositions;
let barWidth = (_a = initPositions.barWidth) != null ? _a : 0;
let barHeight = (_b = initPositions.barHeight) != null ? _b : 0;
const yDivision = (_c = initPositions.yDivision) != null ? _c : 0;
const xDivision = (_d = initPositions.xDivision) != null ? _d : 0;
y = initY;
x = initX;
const elDataLabelsWrap = graphics.group({
class: "apexcharts-datalabels",
"data:realIndex": realIndex
});
const elGoalsMarkers = graphics.group({
class: "apexcharts-rangebar-goals-markers"
});
for (let j = 0; j < w2.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex);
const y1 = this.seriesRangeStart[i][j];
const y2 = this.seriesRangeEnd[i][j];
let paths = (
/** @type {any} */
null
);
let barXPosition = null;
let barYPosition = null;
const params = { x, y, strokeWidth, elSeries };
let seriesLen = this.seriesLen;
if (w2.config.plotOptions.bar.rangeBarGroupRows) {
seriesLen = 1;
}
if (typeof /** @type {Record<string,any>} */
((_e = w2.config.series[i].data) == null ? void 0 : _e[j]) === "undefined") {
break;
}
if (this.isHorizontal) {
barYPosition = y + barHeight * /** @type {any} */
this.visibleI;
const srty = (yDivision - barHeight * seriesLen) / 2;
if (
/** @type {Record<string,any>} */
(_g = (_f = w2.config.series[i].data) == null ? void 0 : _f[j]) == null ? void 0 : _g.x
) {
const positions = this.detectOverlappingBars({
i,
j,
barYPosition,
srty,
barHeight,
yDivision,
initPositions
});
barHeight = positions.barHeight;
barYPosition = positions.barYPosition;
}
paths = this.drawRangeBarPaths(__spreadValues({
indexes: { i, j, realIndex },
barHeight,
barYPosition,
zeroW,
yDivision,
y1,
y2
}, params));
barWidth = paths.barWidth;
} else {
if (w2.axisFlags.isXNumeric) {
x = (w2.seriesData.seriesX[i][j] - w2.globals.minX) / this.xRatio - barWidth / 2;
}
barXPosition = x + barWidth * /** @type {any} */
this.visibleI;
const srtx = (xDivision - barWidth * seriesLen) / 2;
if (
/** @type {Record<string,any>} */
(_i = (_h = w2.config.series[i].data) == null ? void 0 : _h[j]) == null ? void 0 : _i.x
) {
const positions = this.detectOverlappingBars({
i,
j,
barXPosition,
srtx,
barWidth,
xDivision,
initPositions
});
barWidth = positions.barWidth;
barXPosition = positions.barXPosition;
}
paths = this.drawRangeColumnPaths(__spreadValues({
indexes: { i, j, realIndex, translationsIndex },
barWidth,
barXPosition,
zeroH,
xDivision
}, params));
barHeight = paths.barHeight;
}
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth
});
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine);
}
y = paths.y;
x = paths.x;
const pathFill = this.barHelpers.getPathFillColor(
series,
i,
j,
realIndex
);
this.renderSeries({
realIndex,
pathFill: pathFill.color,
lineFill: pathFill.useRangeColor ? pathFill.color : w2.globals.stroke.colors[realIndex],
j,
i,
x,
y,
y1,
y2,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
series,
barHeight,
barWidth,
barXPosition,
barYPosition,
columnGroupIndex,
elDataLabelsWrap,
elGoalsMarkers,
visibleSeries: this.visibleI,
type: "rangebar"
});
}
ret.add(elSeries);
}
return ret;
}
/** @param {{ i?: any, j?: any, barYPosition?: any, barXPosition?: any, srty?: any, srtx?: any, barHeight?: any, barWidth?: any, yDivision?: any, xDivision?: any, initPositions?: any }} opts */
detectOverlappingBars({
i,
j,
barYPosition,
barXPosition,
srty,
srtx,
barHeight,
barWidth,
yDivision,
xDivision,
initPositions
}) {
var _a, _b, _c, _d;
const w2 = this.w;
let overlaps = [];
const rangeName = (_b = (_a = w2.globals.seriesRangeName) == null ? void 0 : _a[i]) == null ? void 0 : _b[j];
const x = (
/** @type {Record<string,any>} */
(_d = (_c = w2.config.series[i].data) == null ? void 0 : _c[j]) == null ? void 0 : _d.x
);
const labelX = Array.isArray(x) ? x.join(" ") : x;
const rowIndex = w2.labelData.labels.map((_) => Array.isArray(_) ? _.join(" ") : _).indexOf(labelX);
const overlappedIndex = w2.rangeData.seriesRange[i].findIndex(
(tx) => {
var _a2;
return tx.x === labelX && ((_a2 = tx.overlaps) == null ? void 0 : _a2.size) > 0;
}
);
if (this.isHorizontal) {
if (w2.config.plotOptions.bar.rangeBarGroupRows) {
barYPosition = srty + yDivision * rowIndex;
} else {
barYPosition = srty + barHeight * this.visibleI + yDivision * rowIndex;
}
if (overlappedIndex > -1 && !w2.config.plotOptions.bar.rangeBarOverlap) {
overlaps = Array.from(
/** @type {any} */
w2.rangeData.seriesRange[i][overlappedIndex].overlaps
);
if (overlaps.indexOf(rangeName) > -1) {
barHeight = initPositions.barHeight / overlaps.length;
barYPosition = barHeight * this.visibleI + yDivision * (100 - parseInt(this.barOptions.barHeight, 10)) / 100 / 2 + barHeight * (this.visibleI + overlaps.indexOf(rangeName)) + yDivision * rowIndex;
}
}
} else {
if (rowIndex > -1 && !w2.labelData.timescaleLabels.length) {
if (w2.config.plotOptions.bar.rangeBarGroupRows) {
barXPosition = srtx + xDivision * rowIndex;
} else {
barXPosition = srtx + barWidth * this.visibleI + xDivision * rowIndex;
}
}
if (overlappedIndex > -1 && !w2.config.plotOptions.bar.rangeBarOverlap) {
overlaps = Array.from(
/** @type {any} */
w2.rangeData.seriesRange[i][overlappedIndex].overlaps
);
if (overlaps.indexOf(rangeName) > -1) {
barWidth = initPositions.barWidth / overlaps.length;
barXPosition = barWidth * this.visibleI + xDivision * (100 - parseInt(this.barOptions.barWidth, 10)) / 100 / 2 + barWidth * (this.visibleI + overlaps.indexOf(rangeName)) + xDivision * rowIndex;
}
}
}
return {
barYPosition,
barXPosition,
barHeight,
barWidth
};
}
/** @param {{indexes: any, x: any, xDivision: any, barWidth: any, barXPosition: any, zeroH: any}} opts */
drawRangeColumnPaths({
indexes,
x,
xDivision,
barWidth,
barXPosition,
zeroH
}) {
var _a, _b;
const w2 = this.w;
const { i, j, realIndex, translationsIndex } = indexes;
const yRatio = this.yRatio[translationsIndex];
const range = this.getRangeValue(realIndex, j);
let y1 = Math.min(range.start, range.end);
let y2 = Math.max(range.start, range.end);
if (typeof /** @type {any} */
((_a = this.series[i]) == null ? void 0 : _a[j]) === "undefined" || /** @type {any} */
((_b = this.series[i]) == null ? void 0 : _b[j]) === null) {
y1 = zeroH;
} else {
y1 = zeroH - y1 / yRatio;
y2 = zeroH - y2 / yRatio;
}
const barHeight = Math.abs(y2 - y1);
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1,
y2,
strokeWidth: this.strokeWidth,
series: this.seriesRangeEnd,
realIndex,
i: realIndex,
j,
w: w2
});
if (!w2.axisFlags.isXNumeric) {
x = x + xDivision;
} else {
const xForNumericXAxis = this.getBarXForNumericXAxis({
x,
j,
realIndex,
barWidth
});
x = xForNumericXAxis.x;
barXPosition = xForNumericXAxis.barXPosition;
}
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
barHeight,
x,
y: range.start < 0 && range.end < 0 ? y1 : y2,
goalY: this.barHelpers.getGoalValues(
"y",
/** @type {any} */
null,
zeroH,
realIndex,
j,
translationsIndex
),
barXPosition
};
}
/**
* @param {number} val
*/
preventBarOverflow(val) {
const w2 = this.w;
if (val < 0) {
val = 0;
}
if (val > w2.layout.gridWidth) {
val = w2.layout.gridWidth;
}
return val;
}
/** @param {{indexes: any, y: any, y1: any, y2: any, yDivision: any, barHeight: any, barYPosition: any, zeroW: any}} opts */
drawRangeBarPaths({
indexes,
y,
y1,
y2,
yDivision,
barHeight,
barYPosition,
zeroW
}) {
const w2 = this.w;
const { realIndex, j } = indexes;
const x1 = this.preventBarOverflow(zeroW + y1 / this.invertedYRatio);
const x2 = this.preventBarOverflow(zeroW + y2 / this.invertedYRatio);
const range = this.getRangeValue(realIndex, j);
const barWidth = Math.abs(x2 - x1);
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1,
x2,
strokeWidth: this.strokeWidth,
series: this.seriesRangeEnd,
i: realIndex,
realIndex,
j,
w: w2
});
if (!w2.axisFlags.isXNumeric) {
y = y + yDivision;
}
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
barWidth,
x: range.start < 0 && range.end < 0 ? x1 : x2,
goalX: this.barHelpers.getGoalValues(
"x",
zeroW,
/** @type {any} */
null,
realIndex,
j,
0
),
y
};
}
/**
* @param {number} i
* @param {number} j
*/
getRangeValue(i, j) {
const w2 = this.w;
return {
start: w2.rangeData.seriesRangeStart[i][j],
end: w2.rangeData.seriesRangeEnd[i][j]
};
}
}
function normalize(data, area) {
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum += data[i];
}
const multiplier = area / sum;
const result = new Array(data.length);
for (let i = 0; i < data.length; i++) {
result[i] = data[i] * multiplier;
}
return result;
}
function calculateRatio(rowMin, rowMax, rowSum, length) {
const lengthSq = length * length;
const sumSq = rowSum * rowSum;
return Math.max(
lengthSq * rowMax / sumSq,
sumSq / (lengthSq * rowMin)
);
}
function improvesRatio(rowLen, rowMin, rowMax, rowSum, nextNode, length) {
if (rowLen === 0) return true;
const currentRatio = calculateRatio(rowMin, rowMax, rowSum, length);
const newRatio = calculateRatio(
Math.min(rowMin, nextNode),
Math.max(rowMax, nextNode),
rowSum + nextNode,
length
);
return currentRatio >= newRatio;
}
function emitCoordinates(coords, row, rowLen, rowSum, xoffset, yoffset, width, height) {
if (width >= height) {
const areaWidth = rowSum / height;
let subY = yoffset;
for (let i = 0; i < rowLen; i++) {
const h = row[i] / areaWidth;
coords.push([xoffset, subY, xoffset + areaWidth, subY + h]);
subY += h;
}
} else {
const areaHeight = rowSum / width;
let subX = xoffset;
for (let i = 0; i < rowLen; i++) {
const w2 = row[i] / areaHeight;
coords.push([subX, yoffset, subX + w2, yoffset + areaHeight]);
subX += w2;
}
}
}
function squarify(data, xoffset, yoffset, width, height) {
const coords = [];
const n = data.length;
if (n === 0) return coords;
const row = new Array(n);
let rowLen = 0;
let rowSum = 0;
let rowMin = Infinity;
let rowMax = -Infinity;
let i = 0;
while (i < n) {
const length = Math.min(width, height);
const val = data[i];
if (improvesRatio(rowLen, rowMin, rowMax, rowSum, val, length)) {
row[rowLen] = val;
rowLen++;
rowSum += val;
if (val < rowMin) rowMin = val;
if (val > rowMax) rowMax = val;
i++;
} else {
emitCoordinates(coords, row, rowLen, rowSum, xoffset, yoffset, width, height);
if (width >= height) {
const areaWidth = rowSum / height;
xoffset += areaWidth;
width -= areaWidth;
} else {
const areaHeight = rowSum / width;
yoffset += areaHeight;
height -= areaHeight;
}
rowLen = 0;
rowSum = 0;
rowMin = Infinity;
rowMax = -Infinity;
}
}
if (rowLen > 0) {
emitCoordinates(coords, row, rowLen, rowSum, xoffset, yoffset, width, height);
}
return coords;
}
function generate(data, width, height) {
const n = data.length;
const sums = new Array(n);
for (let i = 0; i < n; i++) {
let s = 0;
const series = data[i];
for (let j = 0; j < series.length; j++) {
s += series[j];
}
sums[i] = s;
}
const seriesRects = squarify(
normalize(sums, width * height),
0,
0,
width,
height
);
const results = new Array(n);
for (let i = 0; i < n; i++) {
const rect = seriesRects[i];
const rx = rect[0];
const ry = rect[1];
const rw = rect[2] - rx;
const rh = rect[3] - ry;
results[i] = squarify(
normalize(data[i], rw * rh),
rx,
ry,
rw,
rh
);
}
return results;
}
function computeArea(node) {
const kids = node.children;
if (kids && kids.length) {
let s = 0;
for (let i = 0; i < kids.length; i++) {
s += computeArea(kids[i]);
}
node._area = s;
return s;
}
const v2 = Number(node.value);
const a = isNaN(v2) ? 0 : Math.abs(v2);
node._area = a;
return a;
}
function layoutLevel(nodes, xoffset, yoffset, width, height, depth, padding, header) {
const n = nodes.length;
if (n === 0 || width <= 0 || height <= 0) return;
const areas = new Array(n);
let total = 0;
for (let i = 0; i < n; i++) {
areas[i] = nodes[i]._area;
total += areas[i];
}
if (total <= 0) return;
const rects = squarify(
normalize(areas, width * height),
xoffset,
yoffset,
width,
height
);
for (let i = 0; i < n; i++) {
const node = nodes[i];
const r = rects[i];
if (!r) continue;
node.rect = r;
node.depth = depth;
const kids = node.children;
if (!kids || !kids.length) continue;
const rw = r[2] - r[0];
const rh = r[3] - r[1];
let pad = Math.max(0, padding(node, depth, rw, rh) || 0);
if (pad * 2 >= rw || pad * 2 >= rh) pad = 0;
let head = Math.max(0, header(node, depth, rw, rh) || 0);
if (head > 0 && rh - pad * 2 - head < head) head = 0;
node.headerHeight = head;
layoutLevel(
kids,
r[0] + pad,
r[1] + head + pad,
rw - pad * 2,
rh - head - pad * 2,
depth + 1,
padding,
header
);
}
}
function generateNested(nodes, width, height, opts = {}) {
if (!Array.isArray(nodes) || nodes.length === 0) return nodes || [];
const padding = opts.padding || (() => 0);
const header = opts.header || (() => 0);
for (let i = 0; i < nodes.length; i++) computeArea(nodes[i]);
layoutLevel(nodes, 0, 0, width, height, 0, padding, header);
return nodes;
}
const TreemapSquared = { generate, generateNested };
function drilldownById(w2, id) {
const dd = w2.config.drilldown;
const list = dd && Array.isArray(dd.series) ? dd.series : [];
return list.find((s) => s && s.id === id);
}
function toNode(w2, d, i, paletteFromParent, parentKey, seenIds = null, opts = {}) {
var _a, _b, _c;
const isObj = d && typeof d === "object";
const name = isObj ? (_b = (_a = d.x) != null ? _a : d.name) != null ? _b : "" : "";
const value = isObj ? Number((_c = d.y) != null ? _c : d.value) : Number(d);
const node = {
name: String(name),
value: isNaN(value) ? null : value,
color: isObj && d.color ? d.color : void 0,
// Identity across data updates: the path of names (indexed so same-named
// siblings stay distinct). Update animations morph matched keys in place.
_key: `${parentKey}/${i}:${name}`
};
if (paletteFromParent && !node.color) {
node.color = paletteFromParent[i % paletteFromParent.length];
}
if (opts.keepDatum) node._datum = d;
if (isObj && Array.isArray(d.children) && d.children.length) {
node.children = d.children.map(
(c, j) => toNode(w2, c, j, null, node._key, seenIds, opts)
);
} else if (isObj && d.drilldown != null && opts.expandDrilldown !== false) {
const visited = seenIds || /* @__PURE__ */ new Set();
if (!visited.has(d.drilldown)) {
const dd = drilldownById(w2, d.drilldown);
if (dd && Array.isArray(dd.data) && dd.data.length) {
const nextSeen = new Set(visited);
nextSeen.add(d.drilldown);
const palette = Array.isArray(dd.colors) ? dd.colors : null;
node.children = dd.data.map(
(c, j) => toNode(w2, c, j, palette, node._key, nextSeen, opts)
);
}
}
}
return node;
}
function buildHierarchy(w2, opts = {}) {
const cfgSeries = (
/** @type {any} */
w2.config.series
);
const first = cfgSeries && cfgSeries[0];
const data = first && Array.isArray(first.data) ? first.data : cfgSeries;
if (!Array.isArray(data)) return [];
return data.map(
(d, i) => toNode(w2, d, i, null, "", null, opts)
);
}
function buildSeriesRoots(w2, series, opts = {}) {
const cfgSeries = (
/** @type {any} */
series || w2.config.series
);
if (!Array.isArray(cfgSeries)) return [];
return cfgSeries.map((s, i) => {
var _a, _b;
const data = s && Array.isArray(s.data) ? s.data : [];
const key = `${i}:${(_a = s == null ? void 0 : s.name) != null ? _a : ""}`;
const root = {
name: String((_b = s == null ? void 0 : s.name) != null ? _b : ""),
value: null,
color: (s == null ? void 0 : s.color) || void 0,
_key: key,
_seriesIndex: i,
children: data.map(
(d, j) => toNode(w2, d, j, null, key, null, opts)
)
};
return root;
});
}
function fillValues(node) {
if (node.children && node.children.length) {
node.children.forEach((c) => fillValues(c));
if (node.value == null || isNaN(node.value)) {
node.value = node.children.reduce(
(s, c) => s + Math.max(0, c.value || 0),
0
);
}
}
if (node.value == null || isNaN(node.value)) node.value = 0;
}
function morphKey(key) {
if (typeof key !== "string") return "";
const i = key.indexOf("/");
return i === -1 ? "" : key.slice(i);
}
function annotate(roots) {
const leaves = [];
let maxDepth = 0;
roots.forEach((root, si) => {
const seriesLeaves = [];
const walk = (node, depth, parent) => {
node._parent = parent;
node._depth = depth;
node._si = si;
node._leaf = !(node.children && node.children.length);
if (depth > maxDepth) maxDepth = depth;
if (node._leaf) {
node._di = seriesLeaves.length;
seriesLeaves.push(node);
} else {
node._di = -1;
node.children.forEach(
(c) => walk(c, depth + 1, node)
);
}
};
walk(root, 0, null);
leaves.push(seriesLeaves);
});
return { leaves, maxDepth };
}
function getTreemapRoots(w2) {
const stashed = w2.globals.treemapRoots;
if (stashed && stashed.length) {
return {
roots: stashed,
maxDepth: w2.globals.treemapMaxDepth || 1,
nested: true
};
}
const roots = buildSeriesRoots(w2, w2.config.series, {
keepDatum: true,
expandDrilldown: false
});
roots.forEach(fillValues);
const { maxDepth } = annotate(roots);
return { roots, maxDepth, nested: false };
}
const areaOf = (r) => (r[2] - r[0]) * (r[3] - r[1]);
class TreemapChart {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.ctx = ctx;
this.w = w2;
this.strokeWidth = this.w.config.stroke.width;
this.helpers = new TreemapHelpers(w2, ctx);
this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation;
this.labels = [];
this.roots = [];
this.drawn = [];
this.nested = false;
this.showParents = false;
this.scale = null;
this._levelCache = null;
this._total = null;
this._avgLabelSize = null;
this._tooltipEl = null;
this._tipOwned = false;
this._morphLeafIndex = 0;
}
/**
* @param {any[]} series
*/
draw(series) {
var _a;
const w2 = this.w;
const graphics = new Graphics(this.w, this.ctx);
const fill = new Fill(this.w);
const ret = graphics.group({
class: "apexcharts-treemap"
});
if (w2.globals.noData) return ret;
series.forEach((s) => {
s.map((v2) => {
return Math.abs(v2);
});
});
this.negRange = this.helpers.checkColorRange();
w2.config.series.forEach((s, i) => {
s.data.forEach((l) => {
if (!Array.isArray(this.labels[i])) this.labels[i] = [];
this.labels[i].push(l.x);
});
});
const tree = getTreemapRoots(w2);
this.nested = tree.nested;
this.roots = tree.roots;
this.scale = buildContinuousScale(w2);
const drawn = tree.roots.length === 1 ? tree.roots[0].children || [] : tree.roots;
this.drawn = drawn;
const parentsCfg = w2.config.plotOptions.treemap.parents;
this.showParents = parentsCfg.show === true || parentsCfg.show !== false && this.nested;
const focus = this._resolveFocus(drawn);
const layoutRoots = focus ? [focus] : drawn;
TreemapSquared.generateNested(
layoutRoots,
w2.layout.gridWidth,
w2.layout.gridHeight,
{
padding: (node, depth, rw, rh) => this.showParents ? this._levelPadding(depth, rw, rh) : 0,
header: (node, depth, rw, rh) => this.showParents ? this._levelHeader(node, depth, rw, rh) : 0
}
);
const morphSrc = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange;
const morphActive = !!morphSrc && typeof morphSrc.isActive === "function" && morphSrc.isActive() && typeof morphSrc.getInitialPathAt === "function";
this._morphLeafIndex = 0;
const leavesBySeries = this._leavesBySeries(layoutRoots, w2.config.series.length);
const parentsBySeries = this.showParents ? this._parentsBySeries(layoutRoots, w2.config.series.length) : [];
leavesBySeries.forEach((node, i) => {
var _a2;
const elSeries = graphics.group({
class: `apexcharts-series apexcharts-treemap-series`,
seriesName: Utils.escapeString(w2.seriesData.seriesNames[i]),
rel: i + 1,
"data:realIndex": i
});
graphics.setupEventDelegation(elSeries, ".apexcharts-treemap-rect");
if (w2.config.chart.dropShadow.enabled) {
const shadow = w2.config.chart.dropShadow;
const filters = new Filters(this.w);
filters.dropShadow(ret, shadow, i);
}
const elDataLabelWrap = graphics.group({
class: "apexcharts-data-labels"
});
const bounds = {
xMin: Infinity,
yMin: Infinity,
xMax: -Infinity,
yMax: -Infinity
};
if (this.showParents) {
(parentsBySeries[i] || []).forEach((p2) => {
this._drawParent(elSeries, p2, i);
});
}
const animCfg = w2.config.chart.animations;
const gradCfg = animCfg.animateGradually;
const cascadeEnabled = gradCfg && gradCfg.enabled !== false;
const cascadeDelays = new Array(node.length).fill(0);
if (cascadeEnabled) {
const tileCount = node.length || 1;
const baseDelay = Math.min(
gradCfg.delay || 0,
animCfg.speed * 0.5 / tileCount
);
const ranked = node.map(
/** @param {any} leaf @param {number} k */
(leaf, k) => ({ j: k, area: leaf.rect ? areaOf(leaf.rect) : 0 })
).sort(
/** @param {{j: number, area: number}} a @param {{j: number, area: number}} b */
(a, b2) => b2.area - a.area
);
ranked.forEach(
/** @param {{j: number, area: number}} item @param {number} rank */
(item, rank) => {
cascadeDelays[item.j] = rank * baseDelay;
}
);
}
node.forEach((leaf, k) => {
const r = leaf.rect;
if (!r) return;
const j = leaf._di;
const x1 = r[0];
const y1 = r[1];
const x2 = r[2];
const y2 = r[3];
bounds.xMin = Math.min(bounds.xMin, x1);
bounds.yMin = Math.min(bounds.yMin, y1);
bounds.xMax = Math.max(bounds.xMax, x2);
bounds.yMax = Math.max(bounds.yMax, y2);
const colorProps = this._leafColor(i, j);
const color = colorProps.color;
const pathFill = fill.fillPath({
color,
seriesNumber: i,
dataPointIndex: j
});
const morphFrom = morphActive ? this._morphSourceForLeaf(leaf) : null;
const elRect = morphFrom ? graphics.drawPath({
d: this._tilePath(x1, y1, x2, y2),
fill: "#fff",
stroke: w2.config.plotOptions.treemap.useFillColorAsStroke ? color : w2.globals.stroke.colors[i],
strokeWidth: this.strokeWidth,
fillOpacity: 1
}) : graphics.drawRect(
x1,
y1,
x2 - x1,
y2 - y1,
w2.config.plotOptions.treemap.borderRadius,
"#fff",
1,
this.strokeWidth,
w2.config.plotOptions.treemap.useFillColorAsStroke ? color : w2.globals.stroke.colors[i]
);
elRect.attr({
cx: x1,
cy: y1,
index: i,
i,
j,
width: x2 - x1,
height: y2 - y1,
fill: pathFill
});
elRect.node.classList.add("apexcharts-treemap-rect");
elRect.node.setAttribute("data:key", morphKey(leaf._key));
let fromRect = {
x: x1 + (x2 - x1) / 2,
y: y1 + (y2 - y1) / 2,
width: 0,
height: 0
};
const toRect = {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1
};
if (morphFrom) {
this._morphTile(
elRect,
morphFrom,
this._tilePath(x1, y1, x2, y2),
this.ctx.morphTypeChange.getSpeed(),
i,
j
);
} else if (w2.config.chart.animations.enabled && !w2.globals.dataChanged) {
let speed = 1;
if (!w2.globals.resized) {
speed = w2.config.chart.animations.speed;
}
this.animateTreemap(
elRect,
fromRect,
toRect,
speed,
// Ranked by draw order, not by data index — the cascade is about
// what is on screen.
cascadeDelays[k] || 0
);
}
if (w2.globals.dataChanged) {
let speed = 1;
if (this.dynamicAnim.enabled && w2.globals.shouldAnimate) {
speed = this.dynamicAnim.speed;
if (w2.globals.previousPaths[i] && /** @type {Record<string,any>} */
w2.globals.previousPaths[i][j] && /** @type {Record<string,any>} */
w2.globals.previousPaths[i][j].rect) {
fromRect = /** @type {Record<string,any>} */
w2.globals.previousPaths[i][j].rect;
}
this.animateTreemap(elRect, fromRect, toRect, speed);
}
}
let fontSize = this.getFontSize(r);
if (w2.config.plotOptions.treemap.dataLabels.format === "truncate") {
fontSize = parseInt(String(w2.config.dataLabels.style.fontSize), 10);
}
let dataLabels = null;
if (w2.config.dataLabels.enabled && this._labelCanShow(fontSize, x2 - x1, y2 - y1)) {
let formattedText = w2.config.dataLabels.formatter(this.labels[i][j], {
value: w2.seriesData.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w: w2
});
if (w2.config.plotOptions.treemap.dataLabels.format === "truncate") {
formattedText = this.truncateLabels(
String(formattedText),
fontSize,
x1,
y1,
x2,
y2
);
}
if (w2.seriesData.series[i][j]) {
dataLabels = this.helpers.calculateDataLabels({
text: formattedText,
x: (x1 + x2) / 2,
y: (y1 + y2) / 2 + this.strokeWidth / 2 + fontSize / 3,
i,
j,
colorProps,
fontSize,
series
});
}
if (w2.config.dataLabels.enabled && dataLabels) {
this.rotateToFitLabel(
dataLabels,
fontSize,
formattedText,
x1,
y1,
x2,
y2
);
}
}
elSeries.add(elRect);
if (dataLabels !== null) {
elSeries.add(dataLabels);
}
});
const seriesTitle = w2.config.plotOptions.treemap.seriesTitle;
if (!this.showParents && w2.config.series.length > 1 && seriesTitle && seriesTitle.show) {
const sName = (
/** @type {Record<string,any>} */
w2.config.series[i].name || ""
);
if (sName && bounds.xMin < Infinity && bounds.yMin < Infinity) {
const {
offsetX,
offsetY,
borderColor,
borderWidth,
borderRadius,
style
} = seriesTitle;
const textColor = style.color || w2.config.chart.foreColor;
const padding = {
left: style.padding.left,
right: style.padding.right,
top: style.padding.top,
bottom: style.padding.bottom
};
const textSize = graphics.getTextRects(
sName,
style.fontSize,
style.fontFamily
);
const labelRectWidth = textSize.width + padding.left + padding.right;
const labelRectHeight = textSize.height + padding.top + padding.bottom;
const labelX = bounds.xMin + (offsetX || 0);
const labelY = bounds.yMin + (offsetY || 0);
const elLabelRect = graphics.drawRect(
labelX,
labelY,
labelRectWidth,
labelRectHeight,
borderRadius,
style.background,
1,
borderWidth,
borderColor
);
const elLabelText = graphics.drawText({
x: labelX + padding.left,
y: labelY + padding.top + ((_a2 = textSize == null ? void 0 : textSize.height) != null ? _a2 : 0) * 0.75,
text: sName,
fontSize: style.fontSize,
fontFamily: style.fontFamily,
fontWeight: style.fontWeight,
foreColor: textColor,
cssClass: style.cssClass || ""
});
elSeries.add(elLabelRect);
elSeries.add(elLabelText);
}
}
elSeries.add(elDataLabelWrap);
ret.add(elSeries);
});
this._renderBreadcrumb();
return ret;
}
// ----------------------------------------------------------------- levels
/**
* Per-level config: `plotOptions.treemap.parents` is the base, and
* `plotOptions.treemap.levels[depth]` overrides it. Depth 0 is the outermost
* group actually drawn (the series, unless a single series was unwrapped).
* @param {number} depth
* @returns {any}
*/
_levelCfg(depth) {
if (!this._levelCache) this._levelCache = [];
if (this._levelCache[depth]) return this._levelCache[depth];
const tm = this.w.config.plotOptions.treemap;
const base = tm.parents || {};
const lvl = (tm.levels || [])[depth] || {};
const merged = __spreadProps(__spreadValues(__spreadValues({}, base), lvl), {
header: __spreadProps(__spreadValues(__spreadValues({}, base.header || {}), lvl.header || {}), {
style: __spreadValues(__spreadValues({}, (base.header || {}).style || {}), (lvl.header || {}).style || {})
}),
hover: __spreadValues(__spreadValues({}, base.hover || {}), lvl.hover || {})
});
this._levelCache[depth] = merged;
return merged;
}
/**
* The inset between a parent's edge and its children, at this depth.
* @param {number} depth
* @param {number} _rw
* @param {number} _rh
* @returns {number}
*/
_levelPadding(depth, _rw, _rh) {
const cfg = this._levelCfg(depth);
return Number(cfg.padding) || 0;
}
/**
* The header strip height at this depth, or 0 when the tile is too small to
* carry one. The geometry library clamps against its own box; this is the
* legibility rule, which needs the font size and so lives here.
* @param {any} node
* @param {number} depth
* @param {number} rw
* @param {number} rh
* @returns {number}
*/
_levelHeader(node, depth, rw, rh) {
const cfg = this._levelCfg(depth);
const header = cfg.header || {};
if (header.show === false) return 0;
const h = Number(header.height);
if (!Number.isFinite(h) || h <= 0) return 0;
const minWidth = Number(header.minWidth) || 40;
if (rw < minWidth) return 0;
if (rh < h * 2) return 0;
return h;
}
// ------------------------------------------------------------------ nodes
/**
* Leaves of the drawn tree, bucketed by the series they came from and kept in
* depth-first order.
* @param {any[]} roots
* @param {number} seriesCount
* @returns {any[][]}
*/
_leavesBySeries(roots, seriesCount) {
const out = new Array(Math.max(1, seriesCount));
for (let i = 0; i < out.length; i++) out[i] = [];
const walk = (node) => {
if (node.children && node.children.length) {
node.children.forEach(walk);
} else {
const si = node._si || 0;
if (out[si]) out[si].push(node);
}
};
roots.forEach(walk);
return out;
}
/**
* Non-leaf nodes of the drawn tree, bucketed by series, shallowest first so a
* container is painted before anything nested inside it.
* @param {any[]} roots
* @param {number} seriesCount
* @returns {any[][]}
*/
_parentsBySeries(roots, seriesCount) {
const out = new Array(Math.max(1, seriesCount));
for (let i = 0; i < out.length; i++) out[i] = [];
const walk = (node) => {
if (!node.children || !node.children.length) return;
const si = node._si || 0;
if (out[si]) out[si].push(node);
node.children.forEach(walk);
};
roots.forEach(walk);
out.forEach(
(list) => list.sort(
(a, b2) => (a.depth || 0) - (b2.depth || 0)
)
);
return out;
}
// ---------------------------------------------------------------- colours
/**
* A leaf's fill.
*
* Continuous colour (a datum carrying a second metric) takes over when it is
* configured, because shading a tile by its own area value is precisely what
* that mode replaces. Everything else - `colorScale.ranges`, `enableShades`,
* `distributed`, negative handling - is the path it always was.
*
* The shape of the return value matches `Helpers.getShadeColor` so the
* caller cannot tell the two apart.
*
* @param {number} i
* @param {number} j
* @returns {any}
*/
_leafColor(i, j) {
const w2 = this.w;
if (this.scale) {
const cv = colorValueOf(w2, i, j);
if (cv != null) {
const color = this.scale.at(cv);
return {
color,
// The discrete path has never supplied a label colour, so only the
// continuous path sets one: a diverging ramp runs right through the
// middle of the luminance range and a fixed light-or-dark label
// would be unreadable at one end or the other.
foreColor: readableOn(color),
colorProps: { color, foreColor: readableOn(color), percent: 0 }
};
}
}
return this.helpers.getShadeColor(
w2.config.chart.type,
i,
j,
this.negRange
);
}
// ---------------------------------------------------------------- parents
/**
* Resolved container/header colours for a level. Parent marks are chrome, not
* data, so they default to a neutral tint of the background rather than to a
* series colour: with continuous colour on the leaves, a coloured container
* would read as another data value.
* @param {number} depth
* @returns {any}
*/
_parentChrome(depth) {
const w2 = this.w;
const cfg = this._levelCfg(depth);
const dark = w2.config.theme.mode === "dark";
const header = cfg.header || {};
const hstyle = header.style || {};
const step = Math.min(depth, 3);
const base = dark ? 255 : 0;
const rgb = `${base},${base},${base}`;
return {
fill: cfg.fill || `rgba(${rgb},${(dark ? 0.04 : 0.03) + step * 0.02})`,
fillOpacity: cfg.fillOpacity == null ? 1 : cfg.fillOpacity,
borderColor: cfg.borderColor || `rgba(${rgb},${dark ? 0.18 : 0.14})`,
borderWidth: cfg.borderWidth == null ? 1 : cfg.borderWidth,
borderRadius: cfg.borderRadius == null ? w2.config.plotOptions.treemap.borderRadius : cfg.borderRadius,
headerBg: hstyle.background || `rgba(${rgb},${dark ? 0.1 : 0.07})`,
headerColor: hstyle.color || (dark ? "#e8e8e8" : w2.config.chart.foreColor),
headerFontSize: hstyle.fontSize || "12px",
headerFontFamily: hstyle.fontFamily || w2.config.chart.fontFamily,
headerFontWeight: hstyle.fontWeight == null ? 600 : hstyle.fontWeight,
hoverColor: cfg.hover && cfg.hover.color || (dark ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.7)"),
hoverWidth: cfg.hover && cfg.hover.width || 2,
hoverShow: !(cfg.hover && cfg.hover.show === false)
};
}
/**
* Draw one parent as a real mark: a container rect, and the header strip the
* layout already reserved room for.
*
* The container is `pointer-events: none` over its interior so the tiles
* inside it keep receiving hover; only the padding gutter and the header
* strip belong to the parent.
*
* @param {any} elSeries
* @param {any} node
* @param {number} i seriesIndex
*/
_drawParent(elSeries, node, i) {
var _a;
const r = node.rect;
if (!r) return;
const w2 = this.w;
const graphics = new Graphics(this.w, this.ctx);
const depth = node.depth || 0;
const cfg = this._levelCfg(depth);
const chrome = this._parentChrome(depth);
const x1 = r[0];
const y1 = r[1];
const width = r[2] - r[0];
const height = r[3] - r[1];
if (width <= 0 || height <= 0) return;
const elGroup = graphics.group({
class: "apexcharts-treemap-parent",
"data:depth": depth,
"data:name": Utils.escapeString(node.name)
});
const key = morphKey(node._key);
const morphFrom = this._morphKeyed() ? this.ctx.morphTypeChange.getInitialPathForKey(key) : null;
const elRect = morphFrom ? graphics.drawPath({
d: this._tilePath(x1, y1, x1 + width, y1 + height),
fill: chrome.fill,
stroke: chrome.borderColor,
strokeWidth: chrome.borderWidth,
fillOpacity: chrome.fillOpacity
}) : graphics.drawRect(
x1,
y1,
width,
height,
chrome.borderRadius,
chrome.fill,
chrome.fillOpacity,
chrome.borderWidth,
chrome.borderColor
);
elRect.node.classList.add("apexcharts-treemap-parent-rect");
elRect.node.setAttribute("data:key", key);
elRect.node.setAttribute("data:depth", String(depth));
if (morphFrom) {
this._morphTile(
elRect,
morphFrom,
this._tilePath(x1, y1, x1 + width, y1 + height),
this.ctx.morphTypeChange.getSpeed(),
i,
depth
);
}
elGroup.add(elRect);
const headerHeight = node.headerHeight || 0;
let headerText = "";
if (headerHeight > 0) {
const header = cfg.header || {};
const elHeaderRect = graphics.drawRect(
x1,
y1,
width,
headerHeight,
0,
chrome.headerBg,
1,
0,
"transparent"
);
elHeaderRect.node.classList.add("apexcharts-treemap-parent-header");
elGroup.add(elHeaderRect);
let text = String((_a = node.name) != null ? _a : "");
if (typeof header.formatter === "function") {
text = String(
header.formatter(node.name, {
value: node.value,
depth,
seriesIndex: i,
node,
w: w2
})
);
} else if (header.showValue) {
text = `${text} ${this._formatValue(node.value)}`;
}
headerText = text;
const offsetX = Number(header.offsetX) || 0;
const align = header.align || "left";
const pad = 6;
const maxWidth = Math.max(0, width - pad * 2 - Math.abs(offsetX));
const fontSize = parseFloat(String(chrome.headerFontSize)) || 12;
const clipped = graphics.getTextBasedOnMaxWidth({
text,
maxWidth,
fontSize
});
if (clipped) {
let tx = x1 + pad + offsetX;
let anchor = "start";
if (align === "center") {
tx = x1 + width / 2 + offsetX;
anchor = "middle";
} else if (align === "right") {
tx = x1 + width - pad + offsetX;
anchor = "end";
}
const elText = graphics.drawText({
x: tx,
y: y1 + headerHeight / 2 + fontSize / 3 + (Number(header.offsetY) || 0),
text: clipped,
textAnchor: anchor,
fontSize: chrome.headerFontSize,
fontFamily: chrome.headerFontFamily,
fontWeight: chrome.headerFontWeight,
foreColor: chrome.headerColor,
cssClass: `apexcharts-treemap-parent-label ${header.style && header.style.cssClass || ""}`
});
elText.node.setAttribute("pointer-events", "none");
elGroup.add(elText);
}
this._attachParentEvents(elHeaderRect.node, node, chrome, elRect);
this._makeParentAccessible(
elHeaderRect.node,
node,
chrome,
elRect,
headerText
);
}
this._attachParentEvents(elRect.node, node, chrome, elRect);
elGroup.node.setAttribute("role", "group");
elGroup.node.setAttribute("aria-label", this._parentLabel(node, headerText));
elSeries.add(elGroup);
}
/**
* The spoken description of a branch: what it is, how big, and how much of
* the chart it accounts for.
*
* Starts from the header's own rendered text when there is one (before
* clipping), so the accessible name contains the visible label rather than a
* differently-formatted number - a formatter that renders "$3.25T" must not
* be announced as "3246".
*
* @param {any} node
* @param {string} [visibleText] the header text, formatter applied, unclipped
* @returns {string}
*/
_parentLabel(node, visibleText) {
const total = this._drawnTotal();
const pct = total > 0 ? (node._area / total * 100).toFixed(1) : "0";
const n = this._countLeaves(node);
const lead = visibleText && String(visibleText).trim() ? String(visibleText).replace(/\s+/g, " ").trim() : `${node.name}, ${this._formatValue(node._area)}`;
return `${lead}, ${n} ${n === 1 ? "item" : "items"}, ${pct}% of total`;
}
/**
* Make a branch reachable and operable from the keyboard.
*
* Only the header strip takes focus, and only while click-to-zoom is live: a
* treemap can hold hundreds of tiles, and making every mark a tab stop would
* bury the chart's own controls behind a few hundred presses. Tabbing the
* groups and pressing Enter is the same path the mouse takes, and the
* breadcrumb is already real buttons, so the way back is reachable too.
*
* @param {any} el
* @param {any} node
* @param {any} chrome
* @param {any} elRect
* @param {string} [visibleText] the header's rendered text, unclipped
*/
_makeParentAccessible(el, node, chrome, elRect, visibleText) {
if (!el || !el.setAttribute) return;
if (!this._zoomEnabled()) return;
el.setAttribute("role", "button");
el.setAttribute("tabindex", "0");
el.setAttribute(
"aria-label",
`${this._parentLabel(node, visibleText)}. Zoom in`
);
el.setAttribute("aria-expanded", "false");
if (!Environment.isBrowser() || !el.addEventListener) return;
el.addEventListener("focus", () => {
elRect.node.setAttribute("stroke", chrome.hoverColor);
elRect.node.setAttribute("stroke-width", String(chrome.hoverWidth + 1));
});
el.addEventListener("blur", () => {
elRect.node.setAttribute("stroke", chrome.borderColor);
elRect.node.setAttribute("stroke-width", String(chrome.borderWidth));
});
el.addEventListener("keydown", (e) => {
if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return;
e.preventDefault();
this._zoomTo(node, true);
});
}
/**
* Hover outline, aggregate tooltip and click-to-zoom for a parent mark.
* @param {any} el the element receiving the pointer
* @param {any} node
* @param {any} chrome
* @param {any} elRect the container rect to outline
*/
_attachParentEvents(el, node, chrome, elRect) {
if (!Environment.isBrowser() || !el || !el.addEventListener) return;
const w2 = this.w;
if (chrome.hoverShow) {
el.addEventListener("mouseenter", () => {
elRect.node.setAttribute("stroke", chrome.hoverColor);
elRect.node.setAttribute("stroke-width", String(chrome.hoverWidth));
});
el.addEventListener("mouseleave", () => {
elRect.node.setAttribute("stroke", chrome.borderColor);
elRect.node.setAttribute("stroke-width", String(chrome.borderWidth));
});
}
if (w2.config.tooltip.enabled) {
el.addEventListener(
"mouseenter",
(e) => this._showParentTooltip(e, node)
);
el.addEventListener(
"mousemove",
(e) => this._positionTooltip(e)
);
el.addEventListener("mouseleave", () => this._hideParentTooltip());
}
if (this._zoomEnabled()) {
el.style.cursor = "pointer";
el.addEventListener("click", () => this._zoomTo(node));
}
}
// ------------------------------------------------------------------- zoom
/**
* Click-to-zoom is live only when nothing else already owns the click.
*
* The drilldown feature is the other way into a hierarchy on a treemap, and
* the two are different models of the same gesture: drilldown replaces the
* view one level at a time, zoom reframes a tree that stays whole. Running
* both would fight over the same click and over the one breadcrumb slot in
* the wrap, so drilldown wins where it is active and this stands down.
* @returns {boolean}
*/
_zoomEnabled() {
const w2 = this.w;
const z = w2.config.plotOptions.treemap.zoom;
if (!z || !z.enabled || !this.showParents) return false;
const dd = w2.config.drilldown;
if (dd && dd.enabled && Array.isArray(dd.series) && dd.series.length) {
if (!this._warnedZoomConflict) {
this._warnedZoomConflict = true;
console.warn(
"ApexCharts treemap: `plotOptions.treemap.zoom` and the drilldown feature both navigate the hierarchy, so zoom is ignored here. Drop `drilldown.series` to zoom a nested treemap instead."
);
}
return false;
}
return true;
}
/**
* The focused branch for this render, looked up by the key stashed on the
* last click. Keys are rebuilt identically from the same data, so the focus
* survives a re-render; a key that no longer resolves (the data changed under
* it) simply falls back to the whole tree.
* @param {any[]} drawn
* @returns {any}
*/
_resolveFocus(drawn) {
const key = this.w.globals.treemapFocusKey;
if (!key || !this._zoomEnabled()) return null;
let found = null;
const walk = (node) => {
if (found) return;
if (node._key === key) {
found = node;
return;
}
if (node.children) node.children.forEach(walk);
};
drawn.forEach(walk);
return found && found.children && found.children.length ? found : null;
}
/**
* @param {any} node
* @param {boolean} [restoreFocus] move focus into the new view once it is
* drawn. A zoom re-renders the chart, which destroys the element the
* keyboard user was standing on; without this they would be returned to the
* top of the document.
*/
_zoomTo(node, restoreFocus = false) {
const w2 = this.w;
if (!node || !node.children || !node.children.length) return;
const next = w2.globals.treemapFocusKey === node._key ? null : node._key;
w2.globals.treemapFocusKey = next;
this._hideParentTooltip();
const done = this.ctx.update();
if (!restoreFocus || !done || typeof done.then !== "function") return;
done.then(() => {
if (!Environment.isBrowser()) return;
const crumb = (
/** @type {any} */
w2.dom.baseEl.querySelector(
".apexcharts-breadcrumb .apexcharts-breadcrumb-item"
)
);
if (crumb && crumb.focus) {
crumb.focus();
return;
}
const header = (
/** @type {any} */
w2.dom.baseEl.querySelector(
".apexcharts-treemap-parent-header[tabindex]"
)
);
if (header && header.focus) header.focus();
});
}
/**
* Outermost drawn group -> focus chain, for the breadcrumb.
*
* Stops at a drawn root rather than walking all the way to `_parent === null`:
* when a single series was unwrapped, the series node is still every level-0
* node's parent, and it is not a level the reader ever sees.
*/
_focusChain() {
const chain = [];
const drawnRoots = new Set(this.drawn || []);
let n = this._resolveFocus(this.drawn || []);
while (n) {
chain.unshift(n);
if (drawnRoots.has(n)) break;
n = n._parent;
}
return chain;
}
/** The breadcrumb config: a treemap-local override on the shared block. */
_breadcrumbCfg() {
const z = this.w.config.plotOptions.treemap.zoom;
return breadcrumbConfig(this.w, z && z.breadcrumb);
}
/**
* Breadcrumb back out of a zoom. Markup, config and accessible semantics are
* the shared ones, so a zoomed treemap and a drilled-in chart present the
* same affordance.
*/
_renderBreadcrumb() {
if (!Environment.isBrowser()) return;
const w2 = this.w;
if (!w2.dom.elWrap) return;
clearBreadcrumb(w2);
if (!this._zoomEnabled()) return;
const chain = this._focusChain();
if (!chain.length) return;
const nav = renderBreadcrumb(w2, {
ariaLabel: "Treemap breadcrumb",
config: this._breadcrumbCfg(),
compact: true,
crumbs: [{ label: "All", data: null }].concat(
chain.map((n) => ({ label: n.name, data: n }))
),
onNavigate: (_i, crumb) => {
w2.globals.treemapFocusKey = crumb.data ? crumb.data._key : null;
this._hideParentTooltip();
this.ctx.update();
}
});
if (!nav) return;
this._placeBreadcrumb(nav);
avoidChromeOverlap(w2, nav);
}
/**
* Sit the breadcrumb in the band the layout reserved for it
* (Dimensions.gridPadForBreadcrumb), just above the grid.
*
* The fallback below should never fire: the reserve is unconditional once
* zoom is enabled. It stays for the cases the reserve cannot cover - a
* responsive override that turns zoom on after layout, or a host stylesheet
* that grows the font - where a readable chip over the tiles beats a
* breadcrumb clipped by the plot.
*
* @param {any} nav
*/
_placeBreadcrumb(nav) {
var _a, _b;
const w2 = this.w;
const gridTop = w2.layout.translateY || 0;
const dimHelpers = (_b = (_a = this.ctx) == null ? void 0 : _a.dimensions) == null ? void 0 : _b.dimHelpers;
const titleArea = dimHelpers ? dimHelpers.getTitleSubtitleCoords("title").height + dimHelpers.getTitleSubtitleCoords("subtitle").height : 0;
const navH = nav.getBoundingClientRect().height || BREADCRUMB_HEIGHT;
if (gridTop - titleArea >= navH + 1) {
nav.style.top = `${gridTop - navH - 1}px`;
return;
}
nav.style.top = `${titleArea}px`;
const dark = w2.config.theme.mode === "dark";
nav.style.background = dark ? "rgba(20,24,30,0.82)" : "rgba(255,255,255,0.86)";
nav.style.borderRadius = "4px";
}
// ---------------------------------------------------------------- tooltip
/**
* A parent is not a row in the series matrix, so the shared tooltip - which
* addresses everything by (seriesIndex, dataPointIndex) - has nothing to look
* up. It writes into the same tooltip element instead, so the aggregate looks
* like every other tooltip in the chart.
* @param {MouseEvent} e
* @param {any} node
*/
_showParentTooltip(e, node) {
const w2 = this.w;
const t = this._tip();
if (!t) return;
const total = this._drawnTotal();
const parentVal = node._parent ? node._parent._area : total;
const pctTotal = total > 0 ? (node._area / total * 100).toFixed(1) : "0.0";
const pctParent = parentVal > 0 ? (node._area / parentVal * 100).toFixed(1) : pctTotal;
const cfg = this.w.config.plotOptions.treemap.parents;
const custom = cfg && cfg.tooltip && cfg.tooltip.formatter;
const leafCount = this._countLeaves(node);
let html;
if (typeof custom === "function") {
html = custom({
name: node.name,
value: node._area,
depth: node.depth || 0,
leafCount,
percentOfParent: Number(pctParent),
percentOfTotal: Number(pctTotal),
node,
w: w2
});
} else {
const marker = this._parentChrome(node.depth || 0).headerBg;
const groupBg = w2.config.tooltip.fillSeriesColor ? `background-color:${marker};` : "";
html = `<div class="apexcharts-tooltip-series-group apexcharts-active" style="display:flex;${groupBg}"><div class="apexcharts-tooltip-text"><div class="apexcharts-tooltip-y-group"><span class="apexcharts-tooltip-text-y-label">${Utils.escapeString(
node.name
)}: </span><span class="apexcharts-tooltip-text-y-value">${this._formatValue(
node._area
)}</span></div><div class="apexcharts-tooltip-y-group"><span class="apexcharts-tooltip-text-y-label">${leafCount} items, </span><span class="apexcharts-tooltip-text-y-value">${pctParent}% of parent, ${pctTotal}% of total</span></div></div></div>`;
}
t.innerHTML = html;
t.classList.add("apexcharts-active");
t.style.opacity = "1";
this._tipOwned = true;
this._positionTooltip(e);
}
/** @returns {any} */
_tip() {
if (!this._tooltipEl) {
this._tooltipEl = this.w.dom.baseEl.querySelector(".apexcharts-tooltip");
}
return this._tooltipEl;
}
/**
* Position beside the cursor, flipping to the opposite side when the box
* would overflow the chart wrap, and clamping inside it either way.
* @param {MouseEvent} e
*/
_positionTooltip(e) {
const t = this._tip();
if (!t || !this._tipOwned) return;
const rect = this.w.dom.elWrap.getBoundingClientRect();
const tw = t.offsetWidth;
const th = t.offsetHeight;
const pad = 12;
let x = e.clientX - rect.left + pad;
if (x + tw > rect.width) x = e.clientX - rect.left - tw - pad;
x = Math.max(0, Math.min(x, rect.width - tw));
let y = e.clientY - rect.top + pad;
if (y + th > rect.height) y = e.clientY - rect.top - th - pad;
y = Math.max(0, Math.min(y, rect.height - th));
t.style.left = x + "px";
t.style.top = y + "px";
}
_hideParentTooltip() {
const t = this._tip();
if (!t || !this._tipOwned) return;
this._tipOwned = false;
t.classList.remove("apexcharts-active");
t.style.opacity = "0";
}
/**
* The whole tree's area, zoomed in or not: "% of total" has to mean the same
* thing at every zoom level, otherwise the branch you just zoomed into
* reports 100%.
* @returns {number}
*/
_drawnTotal() {
if (this._total == null) {
this._total = (this.drawn || []).reduce(
(s, r) => s + this._subtreeArea(r),
0
);
}
return this._total || 0;
}
/**
* A node's area, computed the same way the layout does it. Zooming lays out
* only the focused branch, so every other branch reaches here without the
* `_area` the layout would otherwise have left on it.
* @param {any} node
* @returns {number}
*/
_subtreeArea(node) {
if (node._area != null) return node._area;
const kids = node.children;
if (kids && kids.length) {
let s = 0;
for (let i = 0; i < kids.length; i++) s += this._subtreeArea(kids[i]);
return s;
}
const v2 = Number(node.value);
return isNaN(v2) ? 0 : Math.abs(v2);
}
/**
* @param {any} node
* @returns {number}
*/
_countLeaves(node) {
if (!node.children || !node.children.length) return 1;
return node.children.reduce(
(s, c) => s + this._countLeaves(c),
0
);
}
/**
* Format an aggregate the way the chart formats its own y values, so a
* parent's total reads like the tiles it contains.
* @param {number} v
* @returns {string}
*/
_formatValue(v2) {
var _a, _b, _c, _d, _e;
const w2 = this.w;
const fmt = ((_b = (_a = w2.config.tooltip) == null ? void 0 : _a.y) == null ? void 0 : _b.formatter) || ((_e = (_d = (_c = w2.config.yaxis) == null ? void 0 : _c[0]) == null ? void 0 : _d.labels) == null ? void 0 : _e.formatter);
if (typeof fmt === "function") {
try {
return String(fmt(v2, { seriesIndex: 0, dataPointIndex: 0, w: w2 }));
} catch (_) {
}
}
return String(v2);
}
/**
* Whether a tile could show a label at all, decided from geometry alone so
* nothing has to be measured to find out.
*
* Two rules, both about legibility rather than about saving work:
* - a tile that cannot hold one character in EITHER orientation has no
* label to draw, rotated or not
* - below `dataLabels.minFontSize` the text is decoration, not information
*
* Together these keep a dense treemap from spending its whole render
* measuring text nobody can read.
*
* @param {number} fontSize
* @param {number} tileWidth
* @param {number} tileHeight
* @returns {boolean}
*/
_labelCanShow(fontSize, tileWidth, tileHeight) {
if (!Number.isFinite(fontSize) || fontSize <= 0) return false;
const cfg = this.w.config.plotOptions.treemap.dataLabels;
const min = cfg && cfg.minFontSize != null ? Number(cfg.minFontSize) : 0;
if (fontSize < min) return false;
return Math.max(tileWidth, tileHeight) >= fontSize;
}
/**
* Mean label length across the whole chart.
*
* It scales every tile's font size, and it is the same number for all of
* them, so it is computed once per draw. It used to be recomputed inside
* `getFontSize`, which runs per tile: two full walks of every label, per
* label, which is quadratic and was the entire cost of a large treemap (a
* 10k-tile chart spent ~9s here).
*
* @returns {number}
*/
_averageLabelSize() {
if (this._avgLabelSize != null) return this._avgLabelSize;
function totalLabelLength(arr) {
let i, total = 0;
if (Array.isArray(arr[0])) {
for (i = 0; i < arr.length; i++) {
total += totalLabelLength(arr[i]);
}
} else {
for (i = 0; i < arr.length; i++) {
total += arr[i].length;
}
}
return total;
}
function countLabels(arr) {
let i, total = 0;
if (Array.isArray(arr[0])) {
for (i = 0; i < arr.length; i++) {
total += countLabels(arr[i]);
}
} else {
for (i = 0; i < arr.length; i++) {
total += 1;
}
}
return total;
}
this._avgLabelSize = totalLabelLength(this.labels) / countLabels(this.labels);
return this._avgLabelSize;
}
// This calculates a font-size based upon
// average label length and the size of the box
/**
* @param {number[]} coordinates
*/
getFontSize(coordinates) {
const w2 = this.w;
const averagelabelsize = this._averageLabelSize();
function fontSize(width, height) {
const area = width * height;
const arearoot = Math.pow(area, 0.5);
return Math.min(
arearoot / averagelabelsize,
parseInt(w2.config.dataLabels.style.fontSize, 10)
);
}
return fontSize(
coordinates[2] - coordinates[0],
coordinates[3] - coordinates[1]
);
}
/**
* @param {any} elText
* @param {string | number} fontSize
* @param {string} text
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
*/
rotateToFitLabel(elText, fontSize, text, x1, y1, x2, y2) {
const graphics = new Graphics(this.w);
const textRect = graphics.getTextRects(text, String(fontSize));
if (textRect.width + this.w.config.stroke.width + 5 > x2 - x1 && textRect.width <= y2 - y1) {
const labelRotatingCenter = graphics.rotateAroundCenter(elText.node);
elText.node.setAttribute(
"transform",
`rotate(-90 ${labelRotatingCenter.x} ${labelRotatingCenter.y}) translate(${textRect.height / 3})`
);
}
}
// This is an alternative label formatting method that uses a
// consistent font size, and trims the edge of long labels
/**
* @param {string} text
* @param {number} fontSize
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
*/
truncateLabels(text, fontSize, x1, y1, x2, y2) {
const graphics = new Graphics(this.w);
const textRect = graphics.getTextRects(text, String(fontSize));
const labelMaxWidth = textRect.width + this.w.config.stroke.width + 5 > x2 - x1 && y2 - y1 > x2 - x1 ? y2 - y1 : x2 - x1;
const truncatedText = graphics.getTextBasedOnMaxWidth({
text,
maxWidth: labelMaxWidth,
fontSize
});
if (text.length !== truncatedText.length && labelMaxWidth / fontSize < 5) {
return "";
} else {
return truncatedText;
}
}
/**
* True when the active cross-type morph can pair marks by branch identity
* rather than by draw order.
* @returns {boolean}
*/
_morphKeyed() {
var _a;
const m2 = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange;
return !!(m2 && typeof m2.hasKeyedMarks === "function" && m2.hasKeyedMarks() && typeof m2.getInitialPathForKey === "function");
}
/**
* The captured shape a LEAF tile unrolls from, or null when the outgoing
* chart had nothing to give it.
*
* Branch identity first, so a tile unrolls from the arc that stood for the
* same row rather than the k-th one. That key can find nothing even when
* both sides carry keys, and a FLAT treemap taking from a nested sunburst is
* exactly that case: its tiles are keyed at depth one ('/0:Tops') while the
* arcs are keyed at the depth they actually sit ('/0:Apparel/0:Tops'), so no
* key ever matches. Draw order is the fallback, the same one the mirror
* direction has always had (Sunburst._morphSourceFor), and without it
* treemap -> sunburst read as a morph while the return trip grew from
* nothing.
*
* Leaves only. A container taking a leaf's path by position would unroll
* from a different branch entirely, so `_morphParent` stays key-or-nothing.
*
* @param {any} leaf
* @returns {string | null}
*/
_morphSourceForLeaf(leaf) {
var _a;
const morph = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange;
if (!morph || typeof morph.getInitialPathAt !== "function") return null;
if (this._morphKeyed()) {
const keyed = morph.getInitialPathForKey(morphKey(leaf._key));
if (keyed) return keyed;
}
return morph.getInitialPathAt(this._morphLeafIndex++);
}
/**
* A tile as closed path data, for the cross-type morph (a <rect> cannot hold
* an arc, so a morphing tile is drawn as a <path>).
* @param {number} x1 @param {number} y1 @param {number} x2 @param {number} y2
* @returns {string}
*/
_tilePath(x1, y1, x2, y2) {
return `M ${x1} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x1} ${y2} Z`;
}
/**
* Tween a tile's path data from a captured shape (a sunburst arc) into its
* rectangle.
*
* Routed through Animations.morphSVG, the same call every other morphing
* renderer makes: it already selects the polygon-resample algorithm while a
* cross-type morph is active, which is what tweens between shapes as
* different as an arc and a rectangle. Reaching for the interpolator directly
* would also add a name to a module the split bundles share, which breaks
* them.
*
* @param {any} el
* @param {string} fromD
* @param {string} toD
* @param {number} speed
* @param {number} i
* @param {number} j
*/
_morphTile(el, fromD, toD, speed, i, j) {
const animations = new Animations(this.w, this.ctx);
animations.morphSVG(el, i, j, "none", fromD, toD, speed, 0);
}
/**
* @param {any} el
* @param {Record<string, any>} fromRect
* @param {Record<string, any>} toRect
* @param {number} speed
* @param {number} [delay] - per-tile cascade delay in ms
*/
animateTreemap(el, fromRect, toRect, speed, delay = 0) {
const animations = new Animations(this.w);
animations.animateRect(
el,
fromRect,
toRect,
speed,
() => {
animations.animationCompleted(el);
},
delay
);
}
}
const p = 0.05, m = 0.05, g = 1 / 45;
function v(e, t, s, i) {
const n = { value: e, velocity: 0, target: e, stiffness: t, damping: s };
return n;
}
function b(e, t) {
if (t <= 0) return M(e);
let s = t;
for (; s > 0; ) {
const t2 = Math.min(s, g), i = -e.stiffness * (e.value - e.target) - e.damping * e.velocity;
e.velocity += i * t2, e.value += e.velocity * t2, s -= t2;
}
return !!M(e) && (e.value = e.target, e.velocity = 0, true);
}
function w(e, t) {
e.target = t;
}
function M(e) {
var _a, _b;
const t = (_a = e.restVelocity) != null ? _a : p, s = (_b = e.restDisplacement) != null ? _b : m;
return Math.abs(e.velocity) < t && Math.abs(e.value - e.target) < s;
}
const S = { crisp: [210, 26], gentle: [120, 20], snappy: [320, 30] };
function L(e) {
var _a;
return (_a = S[e != null ? e : "crisp"]) != null ? _a : S.crisp;
}
const LAYOUT_KEY = "__apexcharts_unit_layouts__";
if (!/** @type {any} */
globalThis[LAYOUT_KEY]) {
globalThis[LAYOUT_KEY] = {};
}
function getLayouts() {
return (
/** @type {any} */
globalThis[LAYOUT_KEY]
);
}
function getUnitLayout(name) {
if (!name) return null;
return getLayouts()[name] || null;
}
const MARK_KEY = "__apexcharts_unit_marks__";
if (!/** @type {any} */
globalThis[MARK_KEY]) {
globalThis[MARK_KEY] = {};
}
function getMarks() {
return (
/** @type {any} */
globalThis[MARK_KEY]
);
}
function normalizeUnitMark(def, name) {
if (typeof def === "string") {
const d = def.trim();
if (!d) return null;
return Object.freeze({
name: "anonymous",
path: d,
viewBox: (
/** @type {[number,number,number,number]} */
[0, 0, 100, 100]
)
});
}
if (!def || typeof def !== "object") return null;
if (typeof def.path !== "string" || !def.path.trim()) return null;
const vb = Array.isArray(def.viewBox) && def.viewBox.length === 4 ? def.viewBox.map(Number) : [0, 0, 100, 100];
if (!vb.every((n) => isFinite(n)) || vb[2] <= 0 || vb[3] <= 0) {
return null;
}
return Object.freeze(__spreadProps(__spreadValues({}, def), {
name: def.name || "anonymous",
path: def.path.trim(),
viewBox: (
/** @type {[number,number,number,number]} */
/** @type {any} */
vb
),
fillRule: def.fillRule === "evenodd" ? "evenodd" : void 0
}));
}
function getUnitMark(name) {
if (!name) return null;
return getMarks()[name] || null;
}
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function easeOutBack(s) {
return (t) => 1 + (s + 1) * Math.pow(t - 1, 3) + s * Math.pow(t - 1, 2);
}
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
const SPRING_REFERENCE_SPEED = 800;
const MAX_FRAME_STEP = 0.25;
const PK_CIRCLE = 0;
const PK_CORNER = 1;
const PK_GLYPH = 2;
function springParams(preset, speed) {
const [stiffness, damping] = L(
/** @type {import('apex-commons').SpringPreset|undefined} */
preset
);
const scale = SPRING_REFERENCE_SPEED / Math.max(1, speed);
return [stiffness * scale * scale, damping * scale];
}
class Unit {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.ctx = ctx;
this.w = w2;
this._lastDotR = 1;
this._gridTrack = null;
this._gridDenom = 1;
this._scatterAxis = null;
this._specCache = /* @__PURE__ */ new Map();
this._markWarned = null;
}
/**
* @param {any[]} series - flat count array (non-axis / pie-shaped data)
* @returns {any} the chart's root group element
*/
draw(series) {
const w2 = this.w;
const graphics = new Graphics(w2, this.ctx);
const ret = graphics.group({ class: "apexcharts-unit" });
if (w2.globals.noData || !Array.isArray(series) || series.length === 0) {
return ret;
}
const opts = w2.config.plotOptions.unit;
const layout = opts.layout === "packed" ? "packed" : opts.layout === "columns" ? "columns" : opts.layout === "grid" ? "grid" : opts.layout === "scatter" ? "scatter" : opts.layout === "arc" ? "arc" : opts.layout === "custom" ? "custom" : "grouped";
const transition = opts.transition;
const flow = transition === "flow";
const identity = transition === "identity";
const unitValue = opts.unitValue > 0 ? opts.unitValue : 1;
let counts = series.map((v2) => {
const n = Math.abs(Utils.parseNumber(v2)) / unitValue;
return n > 0 ? Math.max(1, Math.round(n)) : 0;
});
counts = this._applyMaxUnits(counts, opts.maxUnits);
const total = counts.reduce((a, b2) => a + b2, 0);
const clusters = layout === "packed" ? this._layoutPacked(counts, opts) : layout === "columns" ? this._layoutColumns(counts, opts) : layout === "grid" ? this._layoutGrid(counts, opts) : layout === "scatter" ? this._layoutScatter(opts) : layout === "arc" ? this._layoutArc(counts, opts) : layout === "custom" ? this._layoutCustom(counts, opts) : this._layoutGrouped(counts, opts);
const gridSplit = layout === "grid" && !!(opts.grid && opts.grid.split);
if (gridSplit) this._drawGridTrack(ret, graphics, opts);
if (layout === "scatter") this._drawScatterAxes(ret, graphics);
const dotR = this._lastDotR;
const animate = this._shouldAnimate();
const morph = this.ctx && this.ctx.morphTypeChange;
const morphActive = animate && !!morph && typeof morph.isActive === "function" && morph.isActive() && typeof morph.getInitialCenterFor === "function";
const perRowBurst = morphActive && typeof morph.getInitialSlotFor === "function";
const pieceTakeover = morphActive && typeof morph.usesPieceTakeover === "function" && morph.usesPieceTakeover();
const prev = animate && !morphActive && this.ctx ? this.ctx._unitPrevDots : null;
const nextPrev = /* @__PURE__ */ new Map();
const animDots = [];
const unitData = w2.seriesData.unitData || [];
const sizeStats = this._bubbleStats(unitData, opts, dotR);
let gIndex = 0;
clusters.forEach((cluster) => {
const color = w2.globals.colors[cluster.i] || w2.globals.colors[0] || "#008FFB";
const elSeries = graphics.group({
class: "apexcharts-series",
seriesName: Utils.escapeString(
w2.seriesData.seriesNames[cluster.i] || `series-${cluster.i + 1}`
),
rel: cluster.i + 1,
"data:realIndex": cluster.i
});
const burst = morphActive && !perRowBurst ? morph.getInitialCenterFor(cluster.i) : null;
const burstCount = cluster.dots.length;
const catData = unitData[cluster.i];
cluster.dots.forEach((d, jj) => {
const j = d.j != null ? d.j : jj;
const datum = catData ? catData[j] : void 0;
const dotFill = datum && typeof datum === "object" && datum.fillColor ? datum.fillColor : color;
const rj = d.r != null ? d.r : sizeStats ? this._radiusForValue(this._unitValueOf(datum), sizeStats) : dotR;
const spec = this._markSpecFor(opts, datum, cluster.i, rj);
const el = this._drawDot(graphics, opts, rj, dotFill, cluster.i, j, spec);
elSeries.add(el);
let key;
if (identity) {
const id = datum && typeof datum === "object" ? datum.id != null ? datum.id : datum.name : void 0;
key = id != null ? `id:${id}` : `g:${gIndex}`;
} else if (flow) {
key = String(gIndex);
} else if (d.slot != null) {
key = `slot:${d.slot}`;
} else {
key = `${cluster.i}:${j}`;
}
gIndex++;
nextPrev.set(key, { x: d.x, y: d.y, fill: dotFill, r: rj, spec });
if (pieceTakeover) {
this._place(el.node, spec, d.x, d.y);
el.node.setAttribute("opacity", "0");
el.node.setAttribute("data-piece-hidden", "1");
} else if (animate) {
const from = prev && prev.get(key);
const anchor = from || (perRowBurst ? morph.getInitialSlotFor(cluster.i, j, burstCount) : burst);
const enter = opts.gather && opts.gather.enter || "burst";
const inPlace = gridSplit || enter === "fade" || enter === "rise";
const cx0 = anchor ? anchor.x : inPlace ? d.x : cluster.cx;
const cy0 = anchor ? anchor.y : enter === "rise" && !gridSplit ? d.y + 14 : inPlace ? d.y : cluster.cy;
el.node.style.opacity = anchor ? "1" : "0";
this._place(el.node, spec, cx0, cy0);
animDots.push({
node: el.node,
spec,
x: d.x,
y: d.y,
cx0,
cy0,
// Carries live spring state (position AND velocity) forward when
// this render interrupted one still in flight.
key,
// Radius tween: an identity-kept dot grows/shrinks from its previous
// size to its new one (e.g. bubble sizing turning on) instead of
// snapping. Enters/uniform updates keep r0 === r1 (no-op).
r0: from && from.r != null ? from.r : rj,
r1: rj,
// Colour tween: a dot that flows into a differently coloured group
// recolours as it travels rather than snapping at the first frame.
fill0: from ? from.fill : dotFill,
fill1: dotFill,
delay: 0,
// assigned below (staggered by global order)
isEnter: !anchor
});
} else {
this._place(el.node, spec, d.x, d.y);
}
});
if ((layout === "grouped" || layout === "columns" || gridSplit) && opts.clusterLabels && opts.clusterLabels.show && counts[cluster.i] > 0) {
const labelTotal = gridSplit ? this._gridDenom : total;
this._drawClusterLabel(elSeries, cluster, counts[cluster.i], labelTotal, opts, color);
}
ret.add(elSeries);
});
if (this._outerLabelsOn(opts)) {
this._drawOuterLabels(ret, clusters, counts, total, opts, animate && !prev);
}
if (prev) {
const exits = this._collectExits(prev, nextPrev, opts);
if (exits.length) {
const exitGroup = graphics.group({ class: "apexcharts-unit-exits" });
ret.add(exitGroup);
this._runExits(exitGroup, exits, opts);
}
}
if (this.ctx) this.ctx._unitPrevDots = nextPrev;
if (this.ctx && (!animate || morphActive)) this.ctx._unitSprings = null;
if (animate && animDots.length) {
this._runGather(animDots);
} else {
w2.globals.animationEnded = true;
}
return ret;
}
/**
* Cap total dots to `maxUnits`, scaling every category down proportionally
* (a non-zero category keeps at least one dot). Warns once when it clips.
* @param {number[]} counts
* @param {number} maxUnits
* @returns {number[]}
*/
_applyMaxUnits(counts, maxUnits) {
const total = counts.reduce((a, b2) => a + b2, 0);
if (!maxUnits || maxUnits <= 0 || total <= maxUnits) return counts;
const scale = maxUnits / total;
console.warn(
`[ApexCharts] unit chart: ${total} dots exceeds maxUnits (${maxUnits}); counts were scaled down proportionally. Raise plotOptions.unit.maxUnits or use plotOptions.unit.unitValue to represent more units per dot.`
);
return counts.map((c) => c > 0 ? Math.max(1, Math.round(c * scale)) : 0);
}
/**
* `layout: 'custom'`. Positions come from a caller-supplied provider rather
* than from a generator in this file.
*
* The provider is the whole extension point: `(objects, rect) => [{id, x, y,
* r?}]`. Everything downstream is unchanged, which is the point - the engine
* already tweens position, radius and colour, and already keeps a mark's
* identity across a relayout, so an arbitrary new arrangement needs no new
* transition code. A silhouette, a hex grid, a timeline, or a projection
* handed over by ApexMaps are all just this function.
*
* Marks the provider omits are dropped, so they animate out through the
* existing exit path. Ids matching no mark are ignored.
*
* @param {number[]} counts
* @param {any} opts
*/
_layoutCustom(counts, opts) {
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const total = counts.reduce((a, b2) => a + b2, 0);
const provider = this._resolveLayoutProvider(opts);
if (!provider) {
console.warn(
`[ApexCharts] unit chart: layout 'custom' needs plotOptions.unit.positions (a function, or the name of a layout registered with ApexCharts.registerUnitLayout). Falling back to 'grouped'.`
);
return this._layoutGrouped(counts, opts);
}
const gutter = this._outerLabelsOn(opts) ? this._outerLabelGutter(counts, opts) : 0;
this._lastOuterGutter = gutter;
const rect = {
x: gutter,
y: 0,
width: Math.max(1, gw - gutter * 2),
height: gh
};
const availR = Math.sqrt(rect.width * rect.height / Math.PI);
const step = this._resolveStep(opts, availR, total);
this._lastDotR = this._dotRadiusFromStep(step, opts);
const dotR = this._lastDotR;
const objects = this._layoutObjects(counts, dotR);
let placed;
try {
placed = provider(objects, rect);
} catch (e) {
console.warn(
"[ApexCharts] unit chart: the layout provider threw; falling back to 'grouped'.",
e
);
return this._layoutGrouped(counts, opts);
}
if (!Array.isArray(placed)) {
console.warn(
"[ApexCharts] unit chart: the layout provider must return an array of {id, x, y}; falling back to 'grouped'."
);
return this._layoutGrouped(counts, opts);
}
const byId = /* @__PURE__ */ new Map();
placed.forEach((p2) => {
if (!p2 || !isFinite(p2.x) || !isFinite(p2.y)) return;
byId.set(String(p2.id), {
x: p2.x,
y: p2.y,
r: typeof p2.r === "number" && p2.r > 0 ? p2.r : void 0
});
});
const clusters = counts.map((_, i) => ({
i,
cx: gw / 2,
cy: gh / 2,
outerR: dotR,
dots: []
}));
objects.forEach((o) => {
const hit = byId.get(o.id);
if (!hit) return;
clusters[o.seriesIndex].dots.push({
x: hit.x,
y: hit.y,
r: hit.r,
j: o.dataPointIndex
});
});
clusters.forEach((c) => {
if (!c.dots.length) return;
let sx = 0;
let sy = 0;
c.dots.forEach((d) => {
sx += d.x;
sy += d.y;
});
c.cx = sx / c.dots.length;
c.cy = sy / c.dots.length;
let far = 0;
c.dots.forEach((d) => {
far = Math.max(far, Math.hypot(d.x - c.cx, d.y - c.cy));
});
c.outerR = far + dotR;
});
return clusters;
}
/**
* One entry per mark, in global draw order, for a layout provider.
*
* `id` is the datum's own id/name where the per-unit object form supplies
* one, so a provider can address a specific unit ("Texas", "employee 41")
* rather than a positional slot. It falls back to `"<category>:<index>"`.
*
* @param {number[]} counts
* @param {number} dotR the radius the engine would use, so a provider that
* packs by size does not have to rediscover it
* @returns {{id:string,index:number,seriesIndex:number,dataPointIndex:number,label:string,value:number|undefined,datum:any,r:number}[]}
*/
_layoutObjects(counts, dotR) {
const w2 = this.w;
const unitData = w2.seriesData.unitData || [];
const names = w2.seriesData.seriesNames || [];
const objects = [];
let index = 0;
counts.forEach((n, i) => {
var _a;
const catData = unitData[i];
for (let j = 0; j < n; j++) {
const datum = catData ? catData[j] : void 0;
const id = datum && typeof datum === "object" && (datum.id != null || datum.name != null) ? String(datum.id != null ? datum.id : datum.name) : `${i}:${j}`;
objects.push({
id,
index,
seriesIndex: i,
dataPointIndex: j,
label: names[i],
// Normalised at the boundary: internally "no value" is null, but the
// public object shape uses an absent property.
value: (_a = this._unitValueOf(datum)) != null ? _a : void 0,
datum,
r: dotR
});
index++;
}
});
return objects;
}
/**
* Resolve `plotOptions.unit.positions` to a provider function: either the
* function itself, or the name of one registered through
* `ApexCharts.registerUnitLayout`.
* @param {any} opts
* @returns {Function|null}
*/
_resolveLayoutProvider(opts) {
const positions = opts.positions;
if (typeof positions === "function") return positions;
if (typeof positions === "string" && positions) {
const found = getUnitLayout(positions);
if (found) return found;
console.warn(
`[ApexCharts] unit chart: no layout named "${positions}" is registered. Register one with ApexCharts.registerUnitLayout("${positions}", fn).`
);
}
return null;
}
/**
* Lay out each category as its own cluster in a horizontal row. All clusters
* share one dot radius (so dot size is comparable across clusters); the blob
* radius encodes the count.
* @param {number[]} counts
* @param {any} opts
*/
_layoutGrouped(counts, opts) {
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const labelSpace = opts.clusterLabels && opts.clusterLabels.show ? 30 : 6;
const visible = counts.map((_, i) => i).filter((i) => counts[i] > 0);
const Kv = Math.max(1, visible.length);
const slotOf = new Array(counts.length).fill(-1);
visible.forEach((i, s) => slotOf[i] = s);
const cellW = gw / Kv;
const availH = gh - labelSpace;
const maxCount = Math.max(1, ...counts);
const pad = Math.min(cellW, availH) * 0.08;
const availR = Math.max(4, Math.min(cellW, availH) / 2 - pad);
const step = this._resolveStep(opts, availR, maxCount);
this._lastDotR = this._dotRadiusFromStep(step, opts);
const dotR = this._lastDotR;
const cy = labelSpace + availH / 2;
const outerRs = counts.map((n) => step * Math.sqrt(Math.max(1, n)) + dotR);
const cellCentre = (i) => slotOf[i] >= 0 ? cellW * (slotOf[i] + 0.5) : gw / 2;
let centers = counts.map((_, i) => cellCentre(i));
const visOuter = visible.map((i) => outerRs[i]);
let overlap = false;
for (let s = 1; s < Kv; s++) {
if (centers[visible[s]] - centers[visible[s - 1]] < visOuter[s] + visOuter[s - 1]) {
overlap = true;
break;
}
}
if (overlap) {
const gap = Math.max(2 * dotR, 8);
const totalW = visOuter.reduce((a, r) => a + 2 * r, 0) + gap * (Kv - 1);
let visCenters;
if (totalW <= gw) {
let x = (gw - totalW) / 2;
visCenters = visOuter.map((r) => {
const c = x + r;
x += 2 * r + gap;
return c;
});
} else if (Kv === 1) {
visCenters = [gw / 2];
} else {
const lo = visOuter[0];
const hi = gw - visOuter[Kv - 1];
visCenters = visOuter.map((_, s) => lo + (hi - lo) * s / (Kv - 1));
}
centers = counts.map(
(_, i) => slotOf[i] >= 0 ? visCenters[slotOf[i]] : gw / 2
);
}
return counts.map((n, i) => ({
i,
cx: centers[i],
cy,
outerR: outerRs[i],
dots: this._spiral(centers[i], cy, n, step, 0)
}));
}
/**
* Lay out all categories into ONE packed blob. Dots are assigned spiral
* indices in category order (smallest-first when sortByGroup), so the
* minority group nests in the centre.
* @param {number[]} counts
* @param {any} opts
*/
_layoutPacked(counts, opts) {
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const labelSpace = 6;
const total = Math.max(1, counts.reduce((a, b2) => a + b2, 0));
const availR = Math.max(
4,
Math.min(gw, gh - labelSpace) / 2 - Math.min(gw, gh) * 0.06
);
const step = this._resolveStep(opts, availR, total);
this._lastDotR = this._dotRadiusFromStep(step, opts);
const cx = gw / 2;
const cy = labelSpace + (gh - labelSpace) / 2;
const order = counts.map((_, i) => i);
if (opts.sortByGroup !== false) {
order.sort((a, b2) => counts[a] - counts[b2]);
}
const clusters = counts.map((_, i) => ({
i,
cx,
cy,
outerR: step * Math.sqrt(total) + this._lastDotR,
/** @type {{x:number,y:number,slot?:number}[]} */
dots: []
}));
let gi = 0;
order.forEach((catI) => {
for (let j = 0; j < counts[catI]; j++) {
const r = step * Math.sqrt(gi + 0.5);
const theta = gi * GOLDEN_ANGLE;
clusters[catI].dots.push({
x: cx + r * Math.cos(theta),
y: cy + r * Math.sin(theta),
slot: gi
});
gi++;
}
});
return clusters;
}
/**
* Lay out all marks as a PARLIAMENT / hemicycle: seats in concentric arced
* rows across an annulus, filled in category (party) order so each category
* forms a contiguous angular wedge (the classic seating chart). `arc` controls
* the sweep (`startAngle`/`endAngle`, radialBar convention: 0 = top, clockwise;
* default a top semicircle), the donut hole (`innerRadiusRatio`) and the row
* count (`rows`, or 'auto'). Like `packed` this is ONE shared shape coloured by
* category, so seats key by physical slot: a seat-count change recolours the
* party boundary in place and only the rim adds / removes seats.
* @param {number[]} counts
* @param {any} opts
*/
_layoutArc(counts, opts) {
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const total = Math.max(1, counts.reduce((a, b3) => a + b3, 0));
const acfg = opts.arc || {};
const startDeg = typeof acfg.startAngle === "number" ? acfg.startAngle : -90;
const endDeg = typeof acfg.endAngle === "number" ? acfg.endAngle : 90;
const a0 = startDeg * Math.PI / 180;
const a1 = endDeg * Math.PI / 180;
const span = a1 - a0 || Math.PI;
const innerRatio = Math.max(
0,
Math.min(0.95, typeof acfg.innerRadiusRatio === "number" ? acfg.innerRadiusRatio : 0.4)
);
const ux = (a) => Math.sin(a);
const uy = (a) => -Math.cos(a);
const b2 = this._arcBounds(a0, a1);
const pad = Math.min(gw, gh) * 0.04;
const boxW = Math.max(1e-6, b2.maxX - b2.minX);
const boxH = Math.max(1e-6, b2.maxY - b2.minY);
const r1 = Math.max(4, Math.min((gw - 2 * pad) / boxW, (gh - 2 * pad) / boxH));
const r0 = r1 * innerRatio;
const cx = gw / 2 - (b2.minX + b2.maxX) / 2 * r1;
const cy = gh / 2 - (b2.minY + b2.maxY) / 2 * r1;
const alloc = this._arcAllocate(total, r0, r1, span, opts);
this._lastDotR = alloc.dotR;
const seats = [];
for (let r = 0; r < alloc.R; r++) {
const rho = alloc.radii[r];
const n = alloc.seatsPerRow[r];
for (let k = 0; k < n; k++) {
const a = n === 1 ? (a0 + a1) / 2 : a0 + span * (k + 0.5) / n;
seats.push({ a, x: cx + rho * ux(a), y: cy + rho * uy(a) });
}
}
seats.sort((s1, s2) => s1.a - s2.a);
const clusters = counts.map((_, i) => ({
i,
cx,
cy,
outerR: r1,
/** @type {{x:number,y:number,slot?:number}[]} */
dots: []
}));
let ci = 0;
let used = 0;
seats.forEach((s, slot) => {
while (ci < counts.length && used >= counts[ci]) {
ci++;
used = 0;
}
if (ci >= counts.length) return;
clusters[ci].dots.push({ x: s.x, y: s.y, slot });
used++;
});
return clusters;
}
/**
* Bounding box of the outer arc (radius 1) over [a0, a1], including the centre
* and every cardinal angle (multiple of 90deg) inside the range, so a
* semicircle / full circle / arbitrary sweep is all bounded correctly.
* @param {number} a0 @param {number} a1
* @returns {{minX:number,maxX:number,minY:number,maxY:number}}
*/
_arcBounds(a0, a1) {
const ux = (a) => Math.sin(a);
const uy = (a) => -Math.cos(a);
const lo = Math.min(a0, a1);
const hi = Math.max(a0, a1);
const xs = [0, ux(a0), ux(a1)];
const ys = [0, uy(a0), uy(a1)];
const q = Math.PI / 2;
for (let k = Math.ceil(lo / q); k * q <= hi; k++) {
xs.push(ux(k * q));
ys.push(uy(k * q));
}
return {
minX: Math.min(...xs),
maxX: Math.max(...xs),
minY: Math.min(...ys),
maxY: Math.max(...ys)
};
}
/**
* Allocate `total` seats across concentric rows of the annulus [r0, r1] sweeping
* `span` radians: seats per row are proportional to the row radius (a longer arc
* holds more), summed EXACTLY to total by largest remainder. Row count is
* `arc.rows` if given, else derived from a fixed dot size, else auto-searched to
* maximise the dot radius (the largest dots that still pack without overlap,
* mirroring `size:'auto'` elsewhere).
* @param {number} total @param {number} r0 @param {number} r1 @param {number} span @param {any} opts
* @returns {{R:number, radii:number[], seatsPerRow:number[], dotR:number}}
*/
_arcAllocate(total, r0, r1, span, opts) {
const spacing = opts.spacing > 0 ? opts.spacing : 1.05;
const absSpan = Math.abs(span) || Math.PI;
const fixed = this._fixedRadius(opts);
const evalR = (R) => {
R = Math.max(1, Math.round(R));
const radii = [];
for (let r = 0; r < R; r++) {
radii.push(R === 1 ? (r0 + r1) / 2 : r0 + (r1 - r0) * (r / (R - 1)));
}
const weightSum = radii.reduce((a, x) => a + x, 0) || 1;
const raw = radii.map((rho) => total * rho / weightSum);
const seatsPerRow = raw.map((x) => Math.floor(x));
let left = total - seatsPerRow.reduce((a, x) => a + x, 0);
raw.map((x, idx) => ({ idx, frac: x - Math.floor(x) })).sort((p2, qq) => qq.frac - p2.frac).forEach((o) => {
if (left > 0) {
seatsPerRow[o.idx]++;
left--;
}
});
while (left > 0) {
seatsPerRow[R - 1]++;
left--;
}
const radialPitch = R === 1 ? r1 - r0 || r1 : (r1 - r0) / (R - 1);
let minArcPitch = Infinity;
for (let r = 0; r < R; r++) {
const n = seatsPerRow[r];
if (n <= 0) continue;
const arcPitch = radii[r] * absSpan / n;
if (arcPitch < minArcPitch) minArcPitch = arcPitch;
}
const pitch = Math.min(radialPitch, minArcPitch);
return { R, radii, seatsPerRow, dotR: Math.max(1, pitch / (2 * spacing)) };
};
const arcRows = opts.arc && opts.arc.rows;
let res;
if (typeof arcRows === "number" && arcRows >= 1) {
res = evalR(arcRows);
} else if (fixed) {
const pitch = 2 * fixed * spacing;
res = evalR((r1 - r0) / pitch + 1);
} else {
const maxR = Math.max(1, Math.min(40, Math.ceil(Math.sqrt(total)) + 6));
res = evalR(1);
for (let R = 2; R <= maxR; R++) {
const cand = evalR(R);
if (cand.dotR > res.dotR) res = cand;
}
}
if (fixed) res.dotR = fixed;
return res;
}
/**
* Lay out each category as a vertical BAR built from stacked dots (a unit /
* waffle column). Every bar shares one dot size and one width (the same
* number of dot columns); the bar's HEIGHT encodes its count. Dots fill each
* bar bottom-up, row by row. This is the "dot bar" state the circle layouts
* morph into: with `transition:'flow'` the dots glide straight from their
* circle slots into these bar slots (see the storyboard sample).
* @param {number[]} counts
* @param {any} opts
*/
_layoutColumns(counts, opts) {
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const labelsOn = !!(opts.clusterLabels && opts.clusterLabels.show);
const labelsBelow = labelsOn && opts.clusterLabels.position === "bottom";
const topPad = labelsOn && !labelsBelow ? 30 : 6;
const visible = counts.map((_, i) => i).filter((i) => counts[i] > 0);
const Kv = Math.max(1, visible.length);
const slotOf = new Array(counts.length).fill(-1);
visible.forEach((i, s) => slotOf[i] = s);
const cellW = gw / Kv;
const barW = cellW * 0.62;
const bottomPad = Math.max(8, gh * 0.04) + (labelsBelow ? 30 : 0);
const availH = Math.max(4, gh - topPad - bottomPad);
const maxCount = Math.max(1, ...counts);
const spacing = opts.spacing > 0 ? opts.spacing : 1;
const colSize = opts.columns ? opts.columns.size : void 0;
let fixed;
if (opts.shape !== "image" && colSize === "auto") {
fixed = null;
} else if (opts.shape !== "image" && typeof colSize === "number" && colSize > 0) {
fixed = colSize;
} else {
fixed = this._fixedRadius(opts);
}
let cols = 1;
let pitch = 0;
if (fixed) {
pitch = 2 * fixed * spacing;
this._lastDotR = fixed;
const rowsCap = Math.max(1, Math.floor(availH / pitch));
const maxColsByWidth = Math.max(1, Math.floor(barW / pitch));
cols = Math.max(1, Math.min(maxColsByWidth, Math.ceil(maxCount / rowsCap)));
} else {
let best = 0;
const maxCols = Math.max(1, Math.min(40, Math.round(barW / 4)));
for (let c = 1; c <= maxCols; c++) {
const rows = Math.ceil(maxCount / c);
const d = Math.min(barW / c, availH / rows);
if (d > best) {
best = d;
cols = c;
}
}
pitch = best;
this._lastDotR = Math.max(1, pitch / (2 * spacing));
}
const r = this._lastDotR;
const maxRows = Math.ceil(maxCount / cols);
const tallestBarH = Math.min(availH, maxRows * pitch);
const bottom = topPad + (availH + tallestBarH) / 2;
return counts.map((n, i) => {
const cx = slotOf[i] >= 0 ? cellW * (slotOf[i] + 0.5) : gw / 2;
const rows = Math.ceil(Math.max(1, n) / cols);
const barH = rows * pitch;
const left = cx - cols * pitch / 2 + pitch / 2;
const dots = [];
for (let j = 0; j < n; j++) {
const rowIdx = Math.floor(j / cols);
const colIdx = j % cols;
dots.push({
x: left + colIdx * pitch,
y: bottom - r - rowIdx * pitch
});
}
return {
i,
cx,
cy: bottom - barH / 2,
outerR: barH / 2,
// Flag read by _drawClusterLabel: a bar takes a straight label (above
// or below per clusterLabels.position), never a curved arc.
flat: true,
dots
};
});
}
/**
* Lay out ALL categories into ONE regular lattice - a waffle / grid. Dots take
* sequential slots in DECLARED category order and fill row-major, `columns`
* wide, so each category owns a contiguous band of cells: a part-to-whole
* square "pie". `grid.total` (optional) re-allocates the cells to a fixed
* budget (e.g. 100) by largest remainder, so the grid reads as exact
* percentages regardless of the raw totals; without it there is one cell per
* unit (respecting unitValue / maxUnits). `grid.fillFrom` picks the first row.
* The category bands follow the legend order (no smallest-first sort), and
* each physical slot is keyed so a proportion change recolours boundary cells
* in place rather than reshuffling the whole grid.
* @param {number[]} counts
* @param {any} opts
*/
_layoutGrid(counts, opts) {
if (opts.grid && opts.grid.split) return this._layoutGridSplit(counts, opts);
this._gridTrack = null;
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const gcfg = opts.grid || {};
const cols = Math.max(1, Math.round(gcfg.columns > 0 ? gcfg.columns : 10));
const fillFrom = gcfg.fillFrom === "top" ? "top" : "bottom";
const cells = gcfg.total > 0 ? this._largestRemainder(counts, Math.round(gcfg.total)) : counts.slice();
const totalCells = cells.reduce((a, b2) => a + b2, 0);
const rows = Math.max(1, Math.ceil(Math.max(1, totalCells) / cols));
const labelSpace = 6;
const spacing = opts.spacing > 0 ? opts.spacing : 1;
const availW = Math.max(4, gw);
const availH = Math.max(4, gh - labelSpace);
const fixed = this._fixedRadius(opts);
let pitch = 0;
if (fixed) {
pitch = 2 * fixed * spacing;
this._lastDotR = fixed;
} else {
pitch = Math.min(availW / cols, availH / rows);
this._lastDotR = Math.max(1, pitch / (2 * spacing));
}
const blockW = cols * pitch;
const blockH = rows * pitch;
const originX = (gw - blockW) / 2 + pitch / 2;
const topY = labelSpace + (availH - blockH) / 2;
const rowY = (rowIdx) => fillFrom === "bottom" ? topY + blockH - pitch / 2 - rowIdx * pitch : topY + pitch / 2 + rowIdx * pitch;
const clusters = counts.map((_, i) => ({
i,
cx: gw / 2,
cy: labelSpace + availH / 2,
outerR: Math.max(blockW, blockH) / 2,
/** @type {{x:number,y:number,slot?:number}[]} */
dots: []
}));
let k = 0;
for (let ci = 0; ci < cells.length; ci++) {
for (let j = 0; j < cells[ci]; j++) {
const col = k % cols;
const rowIdx = Math.floor(k / cols);
clusters[ci].dots.push({
x: originX + col * pitch,
y: rowY(rowIdx),
slot: k
});
k++;
}
}
return clusters;
}
/**
* Small-multiple ("trellis") waffles: ONE mini-waffle per category, laid out
* in a near-square grid of tiles. Each tile has `grid.total` cells (default
* 100 -> a 10x10 tile) and fills a fraction of them equal to the category's
* value over a denominator (`grid.max`, else the largest count so the leader
* fills its tile and every other tile stays proportionally full - no empty
* tiles for arbitrary data). The unfilled cells are drawn as a faint TRACK
* backdrop (see _drawGridTrack) so each tile reads as a part-to-whole "of N".
* Only VISIBLE (non-zero) categories claim a tile, so a legend hide drops the
* tile and the rest re-flow. Each filled cell is keyed by a physical
* `tile*cells + localCell` slot, so a value change grows/shrinks a tile's fill
* in place instead of reshuffling.
* @param {number[]} counts @param {any} opts
*/
_layoutGridSplit(counts, opts) {
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const gcfg = opts.grid || {};
const cols = Math.max(1, Math.round(gcfg.columns > 0 ? gcfg.columns : 10));
const fillFrom = gcfg.fillFrom === "top" ? "top" : "bottom";
const cellsPerTile = Math.max(1, Math.round(gcfg.total > 0 ? gcfg.total : 100));
const rowsPerTile = Math.max(1, Math.ceil(cellsPerTile / cols));
const visible = counts.map((_, i) => i).filter((i) => counts[i] > 0);
const K = Math.max(1, visible.length);
const denom = gcfg.max > 0 ? gcfg.max : Math.max(1, ...counts);
const tileCols = Math.max(
1,
Math.round(gcfg.tileColumns > 0 ? gcfg.tileColumns : Math.ceil(Math.sqrt(K)))
);
const tileRows = Math.max(1, Math.ceil(K / tileCols));
const labelsOn = !(opts.clusterLabels && opts.clusterLabels.show === false);
const labelsBelow = labelsOn && opts.clusterLabels && opts.clusterLabels.position === "bottom";
const topBand = labelsOn && !labelsBelow ? 22 : 4;
const botBand = labelsOn && labelsBelow ? 22 : 4;
const tileW = gw / tileCols;
const tileH = gh / tileRows;
const availTileW = Math.max(4, tileW * 0.86);
const availTileH = Math.max(4, tileH - topBand - botBand);
const spacing = opts.spacing > 0 ? opts.spacing : 1;
const fixed = this._fixedRadius(opts);
let pitch = 0;
if (fixed) {
pitch = 2 * fixed * spacing;
this._lastDotR = fixed;
} else {
pitch = Math.min(availTileW / cols, availTileH / rowsPerTile);
this._lastDotR = Math.max(1, pitch / (2 * spacing));
}
const blockW = cols * pitch;
const blockH = rowsPerTile * pitch;
const rowY = (topY, rowIdx) => fillFrom === "bottom" ? topY + blockH - pitch / 2 - rowIdx * pitch : topY + pitch / 2 + rowIdx * pitch;
const track = [];
const clusters = [];
visible.forEach((ci, t) => {
const tc = t % tileCols;
const tr = Math.floor(t / tileCols);
const tileX = tc * tileW;
const tileYtop = tr * tileH;
const originX = tileX + (tileW - blockW) / 2 + pitch / 2;
const topY = tileYtop + topBand + (availTileH - blockH) / 2;
const cellXY = (k) => ({
x: originX + k % cols * pitch,
y: rowY(topY, Math.floor(k / cols))
});
for (let k = 0; k < cellsPerTile; k++) track.push(cellXY(k));
const filled = Math.max(
0,
Math.min(cellsPerTile, Math.round(counts[ci] / denom * cellsPerTile))
);
const dots = [];
for (let k = 0; k < filled; k++) {
const p2 = cellXY(k);
dots.push({ x: p2.x, y: p2.y, slot: t * cellsPerTile + k });
}
clusters.push({
i: ci,
cx: tileX + tileW / 2,
cy: topY + blockH / 2,
outerR: blockH / 2,
// Straight per-tile label (never a curved arc), placed by position.
flat: true,
split: true,
dots
});
});
this._gridDenom = denom;
this._gridTrack = { cells: track };
return clusters;
}
/**
* Draw the faint "track" backdrop for the small-multiple grid: every cell of
* every tile's full lattice, so the filled (coloured) cells drawn on top read
* as a fraction of the whole. Static (redrawn each render, never animated);
* painted BEHIND the series groups. `grid.trackColor` overrides the default
* theme-neutral grey.
* @param {any} ret @param {Graphics} graphics @param {any} opts
*/
_drawGridTrack(ret, graphics, opts) {
const track = this._gridTrack;
if (!track || !track.cells || !track.cells.length) return;
const r = this._lastDotR;
const gcfg = opts.grid || {};
const trackColor = gcfg.trackColor || "rgba(128,128,128,0.14)";
const g2 = graphics.group({ class: "apexcharts-unit-track" });
track.cells.forEach((c) => {
let el;
if (opts.shape === "square") {
const side = r * 2;
el = graphics.drawRect(0, 0, side, side, opts.borderRadius || 0, trackColor, 1, 0, "none");
el.node.setAttribute("fill", trackColor);
el.node.setAttribute("x", String(c.x - r));
el.node.setAttribute("y", String(c.y - r));
} else {
el = graphics.drawCircle(r, { fill: trackColor, "stroke-width": 0, stroke: "none" });
el.node.setAttribute("fill", trackColor);
el.node.setAttribute("cx", String(c.x));
el.node.setAttribute("cy", String(c.y));
}
el.node.classList.add("apexcharts-unit-track-cell");
g2.add(el);
});
ret.add(g2);
}
/**
* Scatter / beeswarm layout: position every unit on a real numeric X value
* axis by its own value (`_unitValueOf`), laned by category on Y. Within a
* lane an anti-overlap "swarm" pack (or a random jitter) spreads the dots off
* the centre line so equal / close values do not stack on top of each other.
* This is the unit chart's answer to "put these on axes": one dot per datum,
* placed by data, with a drawn value axis + category lanes (see
* _drawScatterAxes). Needs the per-unit object form (each datum a numeric
* `value`/`y`); flat counts have no per-unit value, so their lanes stay empty.
* @param {any} opts
*/
_layoutScatter(opts) {
const w2 = this.w;
const scfg = opts.scatter || {};
if (scfg.y === "value") return this._layoutScatter2D(opts);
if (scfg.orientation === "vertical") return this._layoutScatterVertical(opts);
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const unitData = w2.seriesData.unitData || [];
const names = w2.seriesData.seriesNames || [];
const valueOf = (d) => this._unitValueOf(d);
const sizeStats = this._scatterSizeStats(scfg, unitData);
const catVals = unitData.map(
(cat) => Array.isArray(cat) ? cat.map(valueOf) : []
);
const isNum = (v2) => v2 != null && isFinite(v2);
const visible = catVals.map((_, i) => i).filter((i) => catVals[i].some(isNum));
const Kv = Math.max(1, visible.length);
let vmin = Infinity;
let vmax = -Infinity;
catVals.forEach(
(vs) => vs.forEach((v2) => {
if (v2 != null && isFinite(v2)) {
if (v2 < vmin) vmin = v2;
if (v2 > vmax) vmax = v2;
}
})
);
if (vmin === Infinity) {
vmin = 0;
vmax = 1;
}
const tickAmount = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5));
const domain = this._scatterValueDomain(scfg, vmin, vmax, tickAmount);
const xMin = domain.min;
const xMax = domain.max;
const xSpan = xMax - xMin || 1;
const laneW = scfg.laneLabelWidth != null ? Math.max(0, scfg.laneLabelWidth) : Kv > 1 ? 92 : 8;
const bottomGutter = 30 + (scfg.xTitle ? 20 : 0);
const plotL = laneW;
const plotR = gw - 8;
const plotT = 10;
const plotB = gh - bottomGutter;
const plotW = Math.max(4, plotR - plotL);
const plotH = Math.max(4, plotB - plotT);
const plotX = (v2) => plotL + (v2 - xMin) / xSpan * plotW;
const laneH = plotH / Kv;
const laneCy = (slot) => plotT + laneH * (slot + 0.5);
let r = 0;
const fixed = this._fixedRadius(opts);
if (fixed) {
r = fixed;
} else {
const maxLane = Math.max(
1,
...visible.map((i) => catVals[i].filter(isNum).length)
);
r = Math.max(
2,
Math.min(6, laneH * 0.12, plotW / (2.5 * Math.sqrt(maxLane)))
);
}
this._lastDotR = r;
const spacing = opts.spacing > 0 ? opts.spacing : 1;
const step = Math.max(0.5, r * spacing);
const jitter = scfg.spread === "jitter";
const clusters = [];
const lanes = [];
const maxR = sizeStats ? sizeStats.rMax : r;
visible.forEach((ci, slot) => {
const cy = laneCy(slot);
lanes.push({ i: ci, cy, name: names[ci] || `series-${ci + 1}` });
const cat = unitData[ci] || [];
const pts = cat.map((d, j) => {
const v2 = valueOf(d);
const p2 = { j, px: plotX(isNum(v2) ? v2 : xMin), y: cy };
if (sizeStats) p2.r = this._scatterRadius(d, sizeStats, r);
return p2;
});
if (jitter) {
const halfLane = Math.max(maxR, laneH / 2 - maxR);
pts.forEach((p2, k) => {
const t = (k * 9301 + 49297) % 233280 / 233280;
p2.y = cy + (t * 2 - 1) * halfLane;
});
} else {
this._beeswarm(pts, cy, r, step, maxR);
}
clusters.push({
i: ci,
cx: (plotL + plotR) / 2,
cy,
outerR: laneH / 2,
dots: pts.map((p2) => ({ x: p2.px, y: p2.y, r: p2.r }))
});
});
const ticks = domain.ticks;
this._scatterAxis = {
mode: "1d",
plotL,
plotR,
plotT,
plotB,
xMin,
xMax,
plotX,
ticks,
lanes,
xTitle: scfg.xTitle,
formatter: typeof scfg.xFormatter === "function" ? scfg.xFormatter : null,
gridlines: scfg.gridlines !== false
};
return clusters;
}
/**
* Vertical beeswarm: the transpose of _layoutScatter. The value runs UP the Y
* axis and each category is a column (lane) across X; the swarm pack spreads
* dots horizontally off each column's centre line. The value-axis config keys
* (`xMin`/`xMax`/`xTitle`/`xFormatter`/`tickAmount`) still describe the value
* axis (now Y), so flipping `orientation` keeps the same value settings.
* @param {any} opts
*/
_layoutScatterVertical(opts) {
const w2 = this.w;
const scfg = opts.scatter || {};
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const unitData = w2.seriesData.unitData || [];
const names = w2.seriesData.seriesNames || [];
const valueOf = (d) => this._unitValueOf(d);
const sizeStats = this._scatterSizeStats(scfg, unitData);
const catVals = unitData.map(
(cat) => Array.isArray(cat) ? cat.map(valueOf) : []
);
const isNum = (v2) => v2 != null && isFinite(v2);
const visible = catVals.map((_, i) => i).filter((i) => catVals[i].some(isNum));
const Kv = Math.max(1, visible.length);
let vmin = Infinity;
let vmax = -Infinity;
catVals.forEach(
(vs) => vs.forEach((v2) => {
if (v2 != null && isFinite(v2)) {
if (v2 < vmin) vmin = v2;
if (v2 > vmax) vmax = v2;
}
})
);
if (vmin === Infinity) {
vmin = 0;
vmax = 1;
}
const tickAmount = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5));
const domain = this._scatterValueDomain(scfg, vmin, vmax, tickAmount);
const vMin = domain.min;
const vMax = domain.max;
const vSpan = vMax - vMin || 1;
const leftGutter = 46 + (scfg.xTitle ? 18 : 0);
const bottomGutter = Kv > 1 ? 26 : 10;
const plotL = leftGutter;
const plotR = gw - 10;
const plotT = 10;
const plotB = gh - bottomGutter;
const plotW = Math.max(4, plotR - plotL);
const plotH = Math.max(4, plotB - plotT);
const plotY = (v2) => plotB - (v2 - vMin) / vSpan * plotH;
const laneW = plotW / Kv;
const laneCx = (slot) => plotL + laneW * (slot + 0.5);
let r = 0;
const fixed = this._fixedRadius(opts);
if (fixed) {
r = fixed;
} else {
const maxLane = Math.max(
1,
...visible.map((i) => catVals[i].filter(isNum).length)
);
r = Math.max(
2,
Math.min(6, laneW * 0.12, plotH / (2.5 * Math.sqrt(maxLane)))
);
}
this._lastDotR = r;
const spacing = opts.spacing > 0 ? opts.spacing : 1;
const step = Math.max(0.5, r * spacing);
const jitter = scfg.spread === "jitter";
const clusters = [];
const lanes = [];
const maxR = sizeStats ? sizeStats.rMax : r;
visible.forEach((ci, slot) => {
const cx = laneCx(slot);
lanes.push({ i: ci, cx, name: names[ci] || `series-${ci + 1}` });
const cat = unitData[ci] || [];
const pts = cat.map((d, j) => {
const v2 = valueOf(d);
const p2 = { j, py: plotY(isNum(v2) ? v2 : vMin), x: cx };
if (sizeStats) p2.r = this._scatterRadius(d, sizeStats, r);
return p2;
});
if (jitter) {
const halfLane = Math.max(maxR, laneW / 2 - maxR);
pts.forEach((p2, k) => {
const t = (k * 9301 + 49297) % 233280 / 233280;
p2.x = cx + (t * 2 - 1) * halfLane;
});
} else {
this._beeswarm(pts, cx, r, step, maxR, true);
}
clusters.push({
i: ci,
cx,
cy: (plotT + plotB) / 2,
outerR: laneW / 2,
dots: pts.map((p2) => ({ x: p2.x, y: p2.py, r: p2.r }))
});
});
const ticks = domain.ticks;
this._scatterAxis = {
mode: "1d",
orientation: "vertical",
plotL,
plotR,
plotT,
plotB,
vMin,
vMax,
plotY,
ticks,
lanes,
valueTitle: scfg.xTitle,
formatter: typeof scfg.xFormatter === "function" ? scfg.xFormatter : null,
gridlines: scfg.gridlines !== false
};
return clusters;
}
/**
* 2D value-value scatter: each datum is a point at (`x`, `y`) on two numeric
* axes (a scatter / bubble plot in the unit family - premium, keyed
* transitions, per-unit colour/tooltip). Category = colour (one series group
* per category). With `scatter.sizeRange` set, each dot is a BUBBLE scaled (by
* area) from its `sizeField` (default 'z'). Needs the object form with numeric
* `x` + `y`.
* @param {any} opts
*/
_layoutScatter2D(opts) {
const w2 = this.w;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const scfg = opts.scatter || {};
const unitData = w2.seriesData.unitData || [];
const isNum = (v2) => typeof v2 === "number" && isFinite(v2);
const xOf = (d) => d && typeof d === "object" ? d.x : null;
const yOf = (d) => d && typeof d === "object" ? d.y != null ? d.y : d.value : null;
const visible = unitData.map((_, i) => i).filter(
(i) => (unitData[i] || []).some((d) => isNum(xOf(d)) && isNum(yOf(d)))
);
let xmn = Infinity;
let xmx = -Infinity;
let ymn = Infinity;
let ymx = -Infinity;
unitData.forEach(
(cat) => (cat || []).forEach((d) => {
const x = xOf(d);
const y = yOf(d);
if (isNum(x) && isNum(y)) {
if (x < xmn) xmn = x;
if (x > xmx) xmx = x;
if (y < ymn) ymn = y;
if (y > ymx) ymx = y;
}
})
);
if (xmn === Infinity) {
xmn = 0;
xmx = 1;
ymn = 0;
ymx = 1;
}
const xTicksN = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5));
const yTicksN = Math.max(2, Math.round(scfg.yTickAmount > 0 ? scfg.yTickAmount : 5));
const nx = this._niceScale(
scfg.xMin != null ? scfg.xMin : xmn,
scfg.xMax != null ? scfg.xMax : xmx,
xTicksN
);
const ny = this._niceScale(
scfg.yMin != null ? scfg.yMin : ymn,
scfg.yMax != null ? scfg.yMax : ymx,
yTicksN
);
const xMin = scfg.xMin != null ? scfg.xMin : nx.min;
const xMax = scfg.xMax != null ? scfg.xMax : nx.max;
const yMin = scfg.yMin != null ? scfg.yMin : ny.min;
const yMax = scfg.yMax != null ? scfg.yMax : ny.max;
const xSpan = xMax - xMin || 1;
const ySpan = yMax - yMin || 1;
const leftGutter = 46 + (scfg.yTitle ? 18 : 0);
const bottomGutter = 30 + (scfg.xTitle ? 20 : 0);
const plotL = leftGutter;
const plotR = gw - 12;
const plotT = 10;
const plotB = gh - bottomGutter;
const plotW = Math.max(4, plotR - plotL);
const plotH = Math.max(4, plotB - plotT);
const plotX = (v2) => plotL + (v2 - xMin) / xSpan * plotW;
const plotY = (v2) => plotB - (v2 - yMin) / ySpan * plotH;
const sizeStats = this._scatterSizeStats(scfg, unitData);
const baseR = this._fixedRadius(opts) || 5;
this._lastDotR = baseR;
const clusters = [];
visible.forEach((ci) => {
const cat = unitData[ci] || [];
const dots = cat.map((d) => {
const x = xOf(d);
const y = yOf(d);
return {
x: plotX(isNum(x) ? x : xMin),
y: plotY(isNum(y) ? y : yMin),
r: sizeStats ? this._scatterRadius(d, sizeStats, baseR) : void 0
};
});
clusters.push({
i: ci,
cx: (plotL + plotR) / 2,
cy: (plotT + plotB) / 2,
outerR: plotH / 2,
dots
});
});
const mkTicks = (lo, hi, span, spacing, pinned, n) => {
const out = [];
if (pinned) {
for (let k = 0; k < n; k++) out.push(lo + span * k / (n - 1));
} else {
const sp = spacing || span / Math.max(1, n - 1);
for (let v2 = lo; v2 <= hi + sp * 0.5; v2 += sp) {
out.push(Math.abs(v2) < sp * 1e-9 ? 0 : v2);
}
}
return out;
};
this._scatterAxis = {
mode: "2d",
plotL,
plotR,
plotT,
plotB,
plotX,
plotY,
xTicks: mkTicks(
xMin,
xMax,
xSpan,
nx.spacing,
scfg.xMin != null || scfg.xMax != null,
xTicksN
),
yTicks: mkTicks(
yMin,
yMax,
ySpan,
ny.spacing,
scfg.yMin != null || scfg.yMax != null,
yTicksN
),
xTitle: scfg.xTitle,
yTitle: scfg.yTitle,
xFormatter: typeof scfg.xFormatter === "function" ? scfg.xFormatter : null,
yFormatter: typeof scfg.yFormatter === "function" ? scfg.yFormatter : null,
gridlines: scfg.gridlines !== false
};
return clusters;
}
/**
* Bubble size stats for the scatter layout, or null when `scatter.sizeRange`
* is not a `[minR, maxR]` pair. Reads the global range of each datum's
* `sizeField` (default 'z') so a value maps to a radius (area scale) in
* _scatterRadius.
* @param {any} scfg @param {any[][]} unitData
* @returns {{zmin:number,zmax:number,rMin:number,rMax:number,field:string}|null}
*/
_scatterSizeStats(scfg, unitData) {
const range = scfg && scfg.sizeRange;
if (!Array.isArray(range) || range.length < 2) return null;
const rMin = Math.max(0.5, +range[0]);
const rMax = Math.max(rMin, +range[1]);
const field = scfg.sizeField || "z";
let zmin = Infinity;
let zmax = -Infinity;
unitData.forEach(
(cat) => (cat || []).forEach((d) => {
const z = d && typeof d === "object" ? d[field] : null;
if (typeof z === "number" && isFinite(z)) {
if (z < zmin) zmin = z;
if (z > zmax) zmax = z;
}
})
);
if (zmin === Infinity) return null;
return { zmin, zmax, rMin, rMax, field };
}
/**
* Radius for one datum under the bubble size stats: area proportional to the
* `sizeField` value (so radius grows with sqrt), between rMin and rMax. A
* missing value collapses to rMin.
* @param {any} d
* @param {{zmin:number,zmax:number,rMin:number,rMax:number,field:string}} st
* @param {number} fallback @returns {number}
*/
_scatterRadius(d, st, fallback) {
if (!st) return fallback;
const z = d && typeof d === "object" ? d[st.field] : null;
if (typeof z !== "number" || !isFinite(z)) return st.rMin;
const t = st.zmax > st.zmin ? (z - st.zmin) / (st.zmax - st.zmin) : 1;
const tc = Math.max(0, Math.min(1, t));
const aMin = st.rMin * st.rMin;
const aMax = st.rMax * st.rMax;
return Math.sqrt(aMin + tc * (aMax - aMin));
}
/**
* One-dimensional anti-overlap "beeswarm" pack: given points with a fixed x
* (`px`) and a lane centre `cy`, assign each a y so no two dots overlap (centre
* distance >= r_i + r_j). Greedy in ascending-x order, trying offsets 0, +step,
* -step, +2step ... and taking the SMALLEST that clears every already-placed
* neighbour still within reach in x. No-overlap always wins: a very dense lane
* grows a taller swarm rather than stacking dots (offsets are not hard-clamped
* to the lane). Each point may carry its own radius `r` (bubble beeswarm),
* else `rFallback` applies; `maxR` bounds the value-window break. Deterministic
* (no physics, no randomness).
*
* Orientation-agnostic: the "fixed" axis is the value axis and the "spread"
* axis is the lane thickness. Horizontal (default): fixed = `px`, spread = `y`
* (mutates `.y`). Vertical: fixed = `py`, spread = `x` (mutates `.x`).
* @param {any[]} pts @param {number} center lane centre on the spread axis
* @param {number} rFallback @param {number} step @param {number} [maxR]
* @param {boolean} [vertical]
*/
_beeswarm(pts, center, rFallback, step, maxR, vertical = false) {
const fk = vertical ? "py" : "px";
const sk = vertical ? "x" : "y";
const order = pts.slice().sort((a, b2) => a[fk] - b2[fk]);
const placed = [];
const rCap = maxR != null ? maxR : rFallback;
order.forEach((p2) => {
const pr = p2.r != null ? p2.r : rFallback;
let chosen = 0;
for (let k = 0; k < 2e3; k++) {
const off = k === 0 ? 0 : Math.ceil(k / 2) * step * (k % 2 ? 1 : -1);
const s = center + off;
let ok = true;
for (let m2 = placed.length - 1; m2 >= 0; m2--) {
const q = placed[m2];
const df = p2[fk] - q.f;
if (df > pr + rCap) break;
const need = pr + q.r;
const ds = s - q.s;
if (df * df + ds * ds < need * need) {
ok = false;
break;
}
}
if (ok) {
chosen = off;
break;
}
}
p2[sk] = center + chosen;
placed.push({ f: p2[fk], s: p2[sk], r: pr });
});
}
/**
* Value-axis domain + ticks for a 1D beeswarm. The domain ALWAYS contains
* every datum: a swarm that clips a dot outside the plot box is a bug, so an
* explicit `xMin`/`xMax` only FRAMES the axis and is extended by whole
* tick-steps whenever the data would otherwise overflow. Orientation-agnostic:
* the same value axis is X for a horizontal swarm and Y for a vertical one.
* @param {any} scfg scatter config
* @param {number} vmin data minimum @param {number} vmax data maximum
* @param {number} tickAmount desired tick count
* @returns {{ min:number, max:number, ticks:number[] }}
*/
_scatterValueDomain(scfg, vmin, vmax, tickAmount) {
const buildTicks = (min, max, spacing2) => {
const ticks = [];
for (let v2 = min; v2 <= max + spacing2 * 0.5; v2 += spacing2) {
ticks.push(Math.abs(v2) < spacing2 * 1e-9 ? 0 : v2);
}
return ticks;
};
if (scfg.xMin != null || scfg.xMax != null) {
let min = scfg.xMin != null ? scfg.xMin : vmin;
let max = scfg.xMax != null ? scfg.xMax : vmax;
if (!(max > min)) max = min + 1;
const spacing2 = (max - min) / Math.max(1, tickAmount - 1);
if (vmin < min) min -= Math.ceil((min - vmin) / spacing2) * spacing2;
if (vmax > max) max += Math.ceil((vmax - max) / spacing2) * spacing2;
return { min, max, ticks: buildTicks(min, max, spacing2) };
}
const nice = this._niceScale(vmin, vmax, tickAmount);
const spacing = nice.spacing || (nice.max - nice.min) / Math.max(1, tickAmount - 1);
return { min: nice.min, max: nice.max, ticks: buildTicks(nice.min, nice.max, spacing) };
}
/**
* A "nice" numeric scale [min, max] + tick spacing covering [dataMin, dataMax]
* with about `ticks` ticks, using rounded 1/2/5 x 10^n steps. Homegrown (no
* dependency) - lean-core.
* @param {number} dataMin @param {number} dataMax @param {number} ticks
* @returns {{min:number,max:number,spacing:number}}
*/
_niceScale(dataMin, dataMax, ticks) {
const lo = dataMin;
let hi = dataMax;
if (!(hi > lo)) hi = lo + 1;
const range = this._niceNum(hi - lo, false);
const spacing = this._niceNum(range / Math.max(1, ticks - 1), true);
return {
min: Math.floor(lo / spacing) * spacing,
max: Math.ceil(hi / spacing) * spacing,
spacing
};
}
/**
* Round a range to a "nice" 1/2/5 x 10^n number (Heckbert's loose/round label
* algorithm).
* @param {number} range @param {boolean} round @returns {number}
*/
_niceNum(range, round) {
const rng = range > 0 ? range : 1;
const exp = Math.floor(Math.log(rng) / Math.LN10);
const frac = rng / Math.pow(10, exp);
let nf;
if (round) {
nf = frac < 1.5 ? 1 : frac < 3 ? 2 : frac < 7 ? 5 : 10;
} else {
nf = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 5 ? 5 : 10;
}
return nf * Math.pow(10, exp);
}
/**
* Draw the scatter chrome behind the dots, from the geometry the layout
* stashed on `this._scatterAxis`. 1D (beeswarm): vertical X gridlines +
* baseline + tick labels (+ x title) + a per-lane category label in the
* category colour. 2D: both X + Y gridlines, both axes' tick labels, and
* rotated/placed axis titles (no lane labels - category is colour). Browser-
* only (SSR renders the dots without the chrome, as with cluster labels).
* @param {any} ret @param {Graphics} graphics
*/
_drawScatterAxes(ret, graphics) {
const w2 = this.w;
if (!Environment.isBrowser()) return;
const ax = this._scatterAxis;
if (!ax) return;
const NS = "http://www.w3.org/2000/svg";
const g2 = graphics.group({ class: "apexcharts-unit-axis" });
const gridColor = w2.config.grid && w2.config.grid.borderColor || "rgba(128,128,128,0.18)";
const axisColor = "rgba(128,128,128,0.5)";
const cfgColors = w2.config.xaxis && w2.config.xaxis.labels && w2.config.xaxis.labels.style && w2.config.xaxis.labels.style.colors;
const configuredLabelColor = Array.isArray(cfgColors) ? cfgColors[0] : cfgColors;
const labelColor = configuredLabelColor || "rgba(120,130,140,0.9)";
const line = (x1, y1, x2, y2, stroke) => {
const l = BrowserAPIs.createElementNS(NS, "line");
l.setAttribute("x1", String(x1));
l.setAttribute("y1", String(y1));
l.setAttribute("x2", String(x2));
l.setAttribute("y2", String(y2));
l.setAttribute("stroke", stroke);
l.setAttribute("shape-rendering", "crispEdges");
g2.node.appendChild(l);
};
const text = (str, x, y, anchor, fill, size, weight, cls) => {
const t = BrowserAPIs.createElementNS(NS, "text");
t.setAttribute("class", cls);
t.setAttribute("x", String(x));
t.setAttribute("y", String(y));
t.setAttribute("text-anchor", anchor);
t.setAttribute("dominant-baseline", "middle");
t.setAttribute("font-size", `${size}px`);
t.setAttribute("font-family", w2.config.chart.fontFamily || "inherit");
t.setAttribute("font-weight", String(weight));
t.setAttribute("fill", fill);
t.textContent = str;
g2.node.appendChild(t);
};
if (ax.mode === "2d") {
ax.yTicks.forEach((v2) => {
const y = ax.plotY(v2);
if (ax.gridlines) line(ax.plotL, y, ax.plotR, y, gridColor);
const label = ax.yFormatter ? String(ax.yFormatter(v2)) : this._formatTick(v2);
text(label, ax.plotL - 8, y, "end", labelColor, 11, 400, "apexcharts-unit-tick");
});
ax.xTicks.forEach((v2) => {
const x = ax.plotX(v2);
if (ax.gridlines) line(x, ax.plotT, x, ax.plotB, gridColor);
const label = ax.xFormatter ? String(ax.xFormatter(v2)) : this._formatTick(v2);
text(label, x, ax.plotB + 14, "middle", labelColor, 11, 400, "apexcharts-unit-tick");
});
line(ax.plotL, ax.plotB, ax.plotR, ax.plotB, axisColor);
line(ax.plotL, ax.plotT, ax.plotL, ax.plotB, axisColor);
if (ax.xTitle) {
text(
String(ax.xTitle),
(ax.plotL + ax.plotR) / 2,
ax.plotB + 32,
"middle",
labelColor,
12,
600,
"apexcharts-unit-axis-title"
);
}
if (ax.yTitle) {
const yt = BrowserAPIs.createElementNS(NS, "text");
yt.setAttribute("class", "apexcharts-unit-axis-title");
const tx = 14;
const ty = (ax.plotT + ax.plotB) / 2;
yt.setAttribute("x", String(tx));
yt.setAttribute("y", String(ty));
yt.setAttribute("text-anchor", "middle");
yt.setAttribute("font-size", "12px");
yt.setAttribute("font-family", w2.config.chart.fontFamily || "inherit");
yt.setAttribute("font-weight", "600");
yt.setAttribute("fill", labelColor);
yt.setAttribute("transform", `rotate(-90 ${tx} ${ty})`);
yt.textContent = String(ax.yTitle);
g2.node.appendChild(yt);
}
ret.add(g2);
return;
}
if (ax.orientation === "vertical") {
ax.ticks.forEach((v2) => {
const y = ax.plotY(v2);
if (ax.gridlines) line(ax.plotL, y, ax.plotR, y, gridColor);
const label = ax.formatter ? String(ax.formatter(v2)) : this._formatTick(v2);
text(label, ax.plotL - 8, y, "end", labelColor, 11, 400, "apexcharts-unit-tick");
});
line(ax.plotL, ax.plotT, ax.plotL, ax.plotB, axisColor);
if (ax.valueTitle) {
const yt = BrowserAPIs.createElementNS(NS, "text");
yt.setAttribute("class", "apexcharts-unit-axis-title");
const tx = 14;
const ty = (ax.plotT + ax.plotB) / 2;
yt.setAttribute("x", String(tx));
yt.setAttribute("y", String(ty));
yt.setAttribute("text-anchor", "middle");
yt.setAttribute("font-size", "12px");
yt.setAttribute("font-family", w2.config.chart.fontFamily || "inherit");
yt.setAttribute("font-weight", "600");
yt.setAttribute("fill", labelColor);
yt.setAttribute("transform", `rotate(-90 ${tx} ${ty})`);
yt.textContent = String(ax.valueTitle);
g2.node.appendChild(yt);
}
ax.lanes.forEach((lane) => {
const color = configuredLabelColor || w2.globals.colors[lane.i] || w2.globals.colors[0] || "#008FFB";
text(lane.name, lane.cx, ax.plotB + 16, "middle", color, 12, 600, "apexcharts-unit-lane-label");
});
ret.add(g2);
return;
}
ax.ticks.forEach((v2, idx) => {
const x = ax.plotX(v2);
if (ax.gridlines) line(x, ax.plotT, x, ax.plotB, gridColor);
const label = ax.formatter ? String(ax.formatter(v2)) : this._formatTick(v2);
const anchor = idx === 0 ? "start" : idx === ax.ticks.length - 1 ? "end" : "middle";
text(label, x, ax.plotB + 14, anchor, labelColor, 11, 400, "apexcharts-unit-tick");
});
line(ax.plotL, ax.plotB, ax.plotR, ax.plotB, axisColor);
if (ax.xTitle) {
text(
String(ax.xTitle),
(ax.plotL + ax.plotR) / 2,
ax.plotB + 32,
"middle",
labelColor,
12,
600,
"apexcharts-unit-axis-title"
);
}
if (ax.plotL > 12) {
ax.lanes.forEach((lane) => {
const color = configuredLabelColor || w2.globals.colors[lane.i] || w2.globals.colors[0] || "#008FFB";
text(lane.name, ax.plotL - 8, lane.cy, "end", color, 12, 600, "apexcharts-unit-lane-label");
});
}
ret.add(g2);
}
/**
* Compact tick-value formatting: integers as-is, otherwise trimmed to a short
* decimal; large magnitudes get a k/M suffix.
* @param {number} v @returns {string}
*/
_formatTick(v2) {
if (!isFinite(v2)) return "";
const a = Math.abs(v2);
if (a >= 1e6) return `${+(v2 / 1e6).toFixed(1)}M`;
if (a >= 1e4) return `${+(v2 / 1e3).toFixed(1)}k`;
if (Number.isInteger(v2)) return String(v2);
return String(+v2.toFixed(2));
}
/**
* Distribute `total` whole cells across `counts` in proportion to each value,
* using the largest-remainder method so the parts sum to exactly `total`
* (used by the grid/waffle percentage mode).
* @param {number[]} counts @param {number} total @returns {number[]}
*/
_largestRemainder(counts, total) {
const sum = counts.reduce((a, b2) => a + b2, 0);
if (sum <= 0 || total <= 0) return counts.map(() => 0);
const exact = counts.map((c) => c / sum * total);
const floors = exact.map((v2) => Math.floor(v2));
const used = floors.reduce((a, b2) => a + b2, 0);
const remaining = Math.max(0, total - used);
const byFrac = exact.map((v2, i) => ({ i, frac: v2 - Math.floor(v2) })).sort((a, b2) => b2.frac - a.frac);
const out = floors.slice();
for (let n = 0; n < remaining && n < byFrac.length; n++) {
out[byFrac[n].i]++;
}
return out;
}
/**
* Phyllotaxis (sunflower) placement for `n` points around (cx, cy).
* @param {number} cx @param {number} cy @param {number} n
* @param {number} step @param {number} startIndex
* @returns {{x:number,y:number}[]}
*/
_spiral(cx, cy, n, step, startIndex) {
const pts = [];
for (let k = 0; k < n; k++) {
const idx = startIndex + k;
const r = step * Math.sqrt(idx + 0.5);
const theta = idx * GOLDEN_ANGLE;
pts.push({ x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) });
}
return pts;
}
/**
* A fixed dot radius, if the shape/size implies one: an explicit numeric
* `size`, or an `image` shape (sized by its own width/height). Returns null
* when dots should auto-size to fit the plot.
* @param {any} opts @returns {number | null}
*/
_fixedRadius(opts) {
if (opts.shape === "image" && opts.image) {
return Math.max(opts.image.width || 20, opts.image.height || 20) / 2;
}
if (this._bubbleActive(opts) && typeof opts.sizeByValue.maxRadius === "number") {
return opts.sizeByValue.maxRadius > 0 ? opts.sizeByValue.maxRadius : null;
}
if (typeof opts.size === "number" && opts.size > 0) return opts.size;
return null;
}
/**
* Whether opt-in bubble sizing applies: enabled, and the shape sizes per
* mark. Squares and images keep a uniform size; a pictogram does not, because
* its scale is derived per mark from the same radius a circle would use.
* @param {any} opts @returns {boolean}
*/
_bubbleActive(opts) {
const sbv = opts.sizeByValue;
return !!(sbv && sbv.enabled && opts.shape !== "image" && opts.shape !== "square");
}
/**
* This datum's numeric value for sizing / tooltip: the number itself, or an
* object's `value` / `y`. Null when there is no usable number.
* @param {any} d @returns {number | null}
*/
_unitValueOf(d) {
if (typeof d === "number") return d;
if (d && typeof d === "object") {
const v2 = d.value != null ? d.value : d.y;
return typeof v2 === "number" ? v2 : null;
}
return null;
}
/**
* Radius for one bubble given the value stats. Default 'area' scaling makes
* a bubble's AREA proportional to its value (radius grows with sqrt); 'linear'
* scales the radius directly. Missing values collapse to the min radius.
* @param {number|null} v
* @param {{min:number,max:number,minR:number,maxR:number,scale:string}} stats
* @returns {number}
*/
_radiusForValue(v2, stats) {
if (v2 == null || !isFinite(v2)) return stats.minR;
const t = stats.max > stats.min ? (v2 - stats.min) / (stats.max - stats.min) : 1;
const tc = Math.max(0, Math.min(1, t));
if (stats.scale === "linear") {
return stats.minR + tc * (stats.maxR - stats.minR);
}
const aMin = stats.minR * stats.minR;
const aMax = stats.maxR * stats.maxR;
return Math.sqrt(aMin + tc * (aMax - aMin));
}
/**
* Value stats + radius bounds for bubble sizing, or null when it does not
* apply (disabled, non-circle shape, or no per-unit values). `maxR` is the
* reference radius the layout already spaced the lattice for; `minR` defaults
* to ~35% of it.
* @param {any[][]} unitData @param {any} opts @param {number} refR
* @returns {{min:number,max:number,minR:number,maxR:number,scale:string}|null}
*/
_bubbleStats(unitData, opts, refR) {
if (!this._bubbleActive(opts)) return null;
let vmin = Infinity;
let vmax = -Infinity;
unitData.forEach((cat) => {
if (!cat) return;
cat.forEach((d) => {
const v2 = this._unitValueOf(d);
if (v2 != null && isFinite(v2)) {
if (v2 < vmin) vmin = v2;
if (v2 > vmax) vmax = v2;
}
});
});
if (vmin === Infinity || vmax < vmin) return null;
const sbv = opts.sizeByValue;
const maxR = refR;
const minR = Math.max(
1,
Math.min(
maxR,
typeof sbv.minRadius === "number" ? sbv.minRadius : maxR * 0.35
)
);
return {
min: vmin,
max: vmax,
minR,
maxR,
scale: sbv.scale === "linear" ? "linear" : "area"
};
}
/**
* Radial step between successive spiral shells. A fixed radius derives the
* step directly; 'auto' derives it so a cluster of `count` dots fits `availR`.
* @param {any} opts @param {number} availR @param {number} count
* @returns {number}
*/
_resolveStep(opts, availR, count) {
const spacing = opts.spacing > 0 ? opts.spacing : 1;
const fixed = this._fixedRadius(opts);
if (fixed) return 2 * fixed * spacing;
return availR / (Math.sqrt(Math.max(1, count)) + 0.5);
}
/**
* @param {number} step @param {any} opts
* @returns {number}
*/
_dotRadiusFromStep(step, opts) {
const spacing = opts.spacing > 0 ? opts.spacing : 1;
const fixed = this._fixedRadius(opts);
if (fixed) return fixed;
return Math.max(1, step / (2 * spacing));
}
/**
* Corner-anchored shapes (square, image) position by their top-left x/y;
* circles position by their centre cx/cy.
* @param {any} opts @returns {boolean}
*/
_isCorner(opts) {
return opts.shape === "square" || opts.shape === "image";
}
/**
* Half-width/height used to convert a centre point to a corner shape's x/y.
* @param {any} opts @param {number} [r] this mark's own radius; defaults to
* the chart-wide one (an image is sized by its own width/height either way)
* @returns {{hx:number, hy:number}}
*/
_halfExtent(opts, r) {
if (opts.shape === "image" && opts.image) {
return { hx: (opts.image.width || 20) / 2, hy: (opts.image.height || 20) / 2 };
}
const rr = r != null ? r : this._lastDotR;
return { hx: rr, hy: rr };
}
/**
* The draw + placement rule for ONE mark.
*
* Positioning used to be a chart-GLOBAL decision - `_isCorner(opts)` and a
* single `_halfExtent(opts)`, hoisted out of the gather loop - which held only
* while every mark in a render was the same element. Two things broke that:
* a pictogram render where dot 3 is a <circle> and dot 4 a <path>, and the
* plainer bug that a `square` sized from a per-position radius (`_drawDot`
* uses the dot's own `rj`) was still being CENTRED with the chart-wide
* `_lastDotR`, so a layout returning per-mark radii drew every square off its
* own slot by `_lastDotR - r`.
*
* So the rule travels with the mark. A spec is one frozen object per distinct
* (kind, size) - shared by every dot that uses it, resolved once per render -
* carrying an int the frame loop switches on. `_place` is the only writer.
*
* @typedef {object} UnitMarkSpec
* @property {number} pk PK_CIRCLE | PK_CORNER | PK_GLYPH
* @property {number} [hx] corner: half-width
* @property {number} [hy] corner: half-height
* @property {any} [mark] glyph: the resolved mark definition
* @property {string} [d] glyph: path data, in the mark's own viewBox units
* @property {string} [fillRule] glyph: 'evenodd' when the mark declares it
* @property {number} [s] glyph: uniform scale from viewBox units to px
* @property {number} [ox] glyph: pre-scaled x of the viewBox centre
* @property {number} [oy] glyph: pre-scaled y of the viewBox centre
* @property {string} [tail] glyph: the pre-built `) scale(s)` transform tail
* @property {number} [r] the radius this spec was fitted to
*/
/**
* Position one mark at (x, y), whatever element it is.
*
* Circles and corner shapes write byte-identically to what they wrote before
* this seam existed, so the morph capture and every existing test read the
* same DOM. A glyph writes ONE attribute where they write two.
*
* @param {SVGElement} node @param {UnitMarkSpec} spec
* @param {number} x @param {number} y
*/
_place(node, spec, x, y) {
const s = (
/** @type {any} */
spec
);
if (s.pk === PK_GLYPH) {
node.setAttribute(
"transform",
"translate(" + (x - s.ox) + "," + (y - s.oy) + s.tail
);
} else if (s.pk === PK_CORNER) {
node.setAttribute("x", String(x - s.hx));
node.setAttribute("y", String(y - s.hy));
} else {
node.setAttribute("cx", String(x));
node.setAttribute("cy", String(y));
}
}
/**
* The spec for the chart-wide shape (no pictogram, no per-mark radius).
* @param {any} opts @param {number} [r]
* @returns {UnitMarkSpec}
*/
_baseSpec(opts, r) {
const rr = r != null ? r : this._lastDotR;
if (!this._isCorner(opts)) return { pk: PK_CIRCLE, r: rr };
const { hx, hy } = this._halfExtent(opts, rr);
return { pk: PK_CORNER, hx, hy, r: rr };
}
/**
* Resolve whatever `pictogram.mark` / `datum.mark` held into a mark
* definition, or null. A name goes through the registry; an object or a bare
* path string is taken as-is.
*
* An unresolvable mark warns ONCE per name and falls back rather than
* dropping the unit: a typo should cost you the glyph, not the data point.
*
* @param {any} ref @returns {any|null}
*/
_resolveMark(ref) {
if (ref == null) return null;
if (typeof ref === "object") return normalizeUnitMark(ref);
if (typeof ref !== "string" || !ref) return null;
const s = ref.trim();
if (s[0] === "M" || s[0] === "m") return normalizeUnitMark(s);
const found = getUnitMark(s);
if (found) return found;
if (!this._markWarned) this._markWarned = /* @__PURE__ */ new Set();
if (!this._markWarned.has(s)) {
this._markWarned.add(s);
console.warn(
`[ApexCharts] unit chart: no mark named "${s}" is registered. Register one with ApexCharts.registerUnitMark("${s}", pathData), or import a catalog from 'apexcharts/pictograms'.`
);
}
return null;
}
/**
* The draw spec for one glyph at the current lattice pitch, cached per
* (mark, radius) for the render so thousands of units of one glyph resolve
* once and then share both the spec and the `d` STRING.
*
* The scale lives in the transform rather than being baked into `d`, for two
* reasons: baking needs a full path parser at runtime (the unit-shapes one
* lives in a separate optional module, and arcs cannot be scaled by naive
* number substitution), and a constant `scale(s)` costs the same single
* attribute write per frame that a bare translate would.
*
* Sizing is derived from `dotR` - the radius the LAYOUT chose - so a glyph
* occupies the box the dot itself would have. Swapping `circle` for a
* pictogram therefore never re-flows the chart: same pitch, same slots.
*
* @param {any} mark @param {number} dotR @param {any} pcfg
* @returns {UnitMarkSpec}
*/
_glyphSpec(mark, dotR, pcfg) {
const qr = Math.round(dotR * 10) / 10;
const key = mark.name + "|" + mark.path.length + "|" + qr;
const hit = this._specCache.get(key);
if (hit) return hit;
const vb = mark.viewBox || [0, 0, 100, 100];
const pad = Math.max(0, Math.min(0.9, pcfg.padding || 0));
const grow = typeof pcfg.scale === "number" && pcfg.scale > 0 ? pcfg.scale : 1;
const box = 2 * qr * (1 - pad) * grow;
const s = pcfg.fit === "width" ? box / vb[2] : pcfg.fit === "height" ? box / vb[3] : box / Math.max(vb[2], vb[3]);
const spec = Object.freeze({
pk: PK_GLYPH,
mark,
d: mark.path,
fillRule: mark.fillRule,
s,
ox: (vb[0] + vb[2] / 2) * s,
oy: (vb[1] + vb[3] / 2) * s,
tail: ") scale(" + s + ")",
r: qr
});
this._specCache.set(key, spec);
return spec;
}
/**
* Which mark THIS unit draws.
*
* Precedence mirrors how `datum.fillColor` already overrides the category
* colour: the datum's own `mark` first (a per-unit override, so one crowd can
* mix glyphs), then the per-series entry of a `mark` array, then the one
* chart-wide mark.
*
* @param {any} opts @param {any} datum @param {number} i @param {number} r
* @returns {UnitMarkSpec}
*/
_markSpecFor(opts, datum, i, r) {
if (opts.shape !== "pictogram") return this._baseSpec(opts, r);
const pcfg = opts.pictogram || {};
const own = datum && typeof datum === "object" ? datum.mark : void 0;
const cfg = Array.isArray(pcfg.mark) ? pcfg.mark[i % pcfg.mark.length] : pcfg.mark;
const mark = this._resolveMark(own != null ? own : cfg);
if (mark) return this._glyphSpec(mark, r, pcfg);
return this._baseSpec(
__spreadProps(__spreadValues({}, opts), { shape: pcfg.fallback === "square" ? "square" : "circle" }),
r
);
}
/**
* Draw one dot (circle, square, or image icon) with the category fill +
* stroke, tagged so the shared non-axis tooltip and hover reuse work.
* @param {Graphics} graphics @param {any} opts @param {number} dotR
* @param {string} color @param {number} i @param {number} j
* @param {UnitMarkSpec} [spec] this mark's resolved spec; defaults to the
* chart-wide shape
* @returns {any}
*/
_drawDot(graphics, opts, dotR, color, i, j, spec) {
const w2 = this.w;
const strokeW = w2.config.stroke.show ? w2.config.stroke.width : 0;
const strokeColor = Array.isArray(w2.globals.stroke.colors) ? w2.globals.stroke.colors[i] || "none" : "none";
const fillOpacity = typeof w2.config.fill.opacity === "number" ? w2.config.fill.opacity : 1;
let el;
if (spec && spec.pk === PK_GLYPH) {
el = w2.dom.Paper.path(spec.d);
el.node.setAttribute("fill", color);
if (spec.fillRule === "evenodd") {
el.node.setAttribute("fill-rule", "evenodd");
}
if (fillOpacity < 1) el.node.setAttribute("fill-opacity", String(fillOpacity));
el.node.setAttribute("data:r", String(spec.r));
} else if (opts.shape === "image" && opts.image && opts.image.src) {
const iw = opts.image.width || 20;
const ih = opts.image.height || 20;
el = w2.dom.Paper.image(opts.image.src);
el.node.setAttribute("width", String(iw));
el.node.setAttribute("height", String(ih));
el.node.setAttribute("preserveAspectRatio", "xMidYMid meet");
if (opts.image.tint) {
el.node.setAttribute("filter", `url(#${this._tintFilter(color)})`);
}
} else if (opts.shape === "square") {
const side = dotR * 2;
el = graphics.drawRect(0, 0, side, side, opts.borderRadius || 0, color, 1, strokeW, strokeColor);
el.node.setAttribute("fill", color);
if (fillOpacity < 1) el.node.setAttribute("fill-opacity", String(fillOpacity));
} else {
el = graphics.drawCircle(dotR, {
fill: color,
"stroke-width": strokeW,
stroke: strokeColor
});
el.node.setAttribute("fill", color);
if (fillOpacity < 1) el.node.setAttribute("fill-opacity", String(fillOpacity));
}
el.node.classList.add("apexcharts-unit-area");
el.node.setAttribute("i", String(i));
el.node.setAttribute("j", String(j));
return el;
}
/**
* Ensure (once per colour) an SVG recolour filter exists in the chart's defs
* and return its id. The filter floods `color` and clips it to the source
* graphic's alpha (feComposite operator="in"), so an `<image>` referencing a
* monochrome icon is repainted in `color` while keeping its silhouette. Reused
* across every dot of the same colour.
* @param {string} color @returns {string}
*/
_tintFilter(color) {
const w2 = this.w;
const NS = "http://www.w3.org/2000/svg";
const safe = String(color).replace(/[^a-zA-Z0-9]/g, "");
const id = `apexcharts-unit-tint-${w2.globals.chartID}-${safe}`;
const svg = w2.dom.Paper.node;
if (svg.querySelector(`#${id}`)) return id;
let defs = svg.querySelector("defs");
if (!defs) {
defs = BrowserAPIs.createElementNS(NS, "defs");
svg.insertBefore(defs, svg.firstChild);
}
const filter = BrowserAPIs.createElementNS(NS, "filter");
filter.setAttribute("id", id);
filter.setAttribute("x", "0%");
filter.setAttribute("y", "0%");
filter.setAttribute("width", "100%");
filter.setAttribute("height", "100%");
const flood = BrowserAPIs.createElementNS(NS, "feFlood");
flood.setAttribute("flood-color", color);
flood.setAttribute("result", "flood");
const comp = BrowserAPIs.createElementNS(NS, "feComposite");
comp.setAttribute("in", "flood");
comp.setAttribute("in2", "SourceAlpha");
comp.setAttribute("operator", "in");
filter.appendChild(flood);
filter.appendChild(comp);
defs.appendChild(filter);
return id;
}
/**
* Position a non-animated dot at (x, y). Circles use cx/cy at the centre;
* corner shapes (square, image) use x/y at the top-left; a pictogram rides a
* transform. Callers that already hold the mark's spec pass it; the rest get
* the chart-wide one.
* @param {SVGElement} node @param {any} opts @param {number} x @param {number} y
* @param {UnitMarkSpec} [spec]
*/
_placeDot(node, opts, x, y, spec) {
this._place(node, spec || this._baseSpec(opts), x, y);
}
/**
* Parse a `#rgb` / `#rrggbb` / `rgb()` / `rgba()` colour to `[r, g, b]`, or
* null if it cannot be parsed (the colour tween is then skipped).
* @param {string} str @returns {number[] | null}
*/
_rgb(str) {
if (typeof str !== "string") return null;
let s = str.trim();
if (s[0] === "#") {
if (s.length === 4) s = "#" + s[1] + s[1] + s[2] + s[2] + s[3] + s[3];
const n = parseInt(s.slice(1, 7), 16);
if (isNaN(n)) return null;
return [n >> 16 & 255, n >> 8 & 255, n & 255];
}
const m2 = s.match(/rgba?\(([^)]+)\)/);
if (m2) {
const p2 = m2[1].split(",").map((x) => parseFloat(x));
if (p2.length >= 3 && p2.every((v2) => !isNaN(v2))) return [p2[0], p2[1], p2[2]];
}
return null;
}
/**
* Whether to run the gather / transition animation. Runs on the initial mount
* and on data-driven updates (keyed old->new tween or cross-type burst).
* Skipped: in SSR, when animations are off, when the caller passed
* `animate:false` (shouldAnimate === false), on a PURE window resize (resized
* with no data change - re-gathering on every resize would be jarring), and
* when the user prefers reduced motion.
*
* Note: `w.globals.resized` is set true on every update (not just window
* resize), so it must be paired with `!dataChanged` to isolate a real resize.
* @returns {boolean}
*/
_shouldAnimate() {
const w2 = this.w;
const anim = w2.config.chart.animations;
if (!Environment.isBrowser()) return false;
if (!anim || anim.enabled === false) return false;
if (w2.globals.shouldAnimate === false) return false;
if (w2.globals.resized && !w2.globals.dataChanged) return false;
if (anim.respectReducedMotion && prefersReducedMotion()) return false;
return true;
}
/**
* Give every dot an x/y spring, reusing the live springs of a gather this
* render just cancelled.
*
* The reuse is the whole point of the spring path. A carried spring holds a
* dot's real on-screen position AND its velocity, so an interrupted gather
* resumes from there. Without it the dot restarts from `cx0`, which on an
* update is the slot it was still travelling towards - so every interruption
* teleports it forward and then re-animates from a standstill. A dragged
* slider or a scrubbed storyboard interrupts on almost every frame, which is
* where that reads worst.
*
* Springs left over from a completed gather are at rest on their targets, so
* carrying them is identical to making fresh ones. Only an interrupted flight
* carries anything.
*
* @param {UnitAnimDot[]} dots
* @param {any} gcfg plotOptions.unit.gather
* @param {number} speed chart.animations.speed, in ms
*/
_seedSprings(dots, gcfg, speed) {
const [stiffness, damping] = springParams(gcfg.spring, speed);
const live = this.ctx ? this.ctx._unitSprings : null;
const springs = /* @__PURE__ */ new Map();
for (let k = 0; k < dots.length; k++) {
const d = dots[k];
const carried = live && !d.isEnter && d.key != null ? live.get(d.key) : null;
const sx = carried ? carried.x : v(d.cx0, stiffness, damping);
const sy = carried ? carried.y : v(d.cy0, stiffness, damping);
if (carried) {
sx.stiffness = stiffness;
sy.stiffness = stiffness;
sx.damping = damping;
sy.damping = damping;
d.cx0 = sx.value;
d.cy0 = sy.value;
this._place(d.node, d.spec, d.cx0, d.cy0);
if (sx.velocity !== 0 || sy.velocity !== 0) d.delay = 0;
}
d.sx = sx;
d.sy = sy;
if (d.key != null) springs.set(d.key, { x: sx, y: sy });
}
if (this.ctx) this.ctx._unitSprings = springs;
}
/**
* One rAF loop that tweens every dot from its start (cx0/cy0 - either the
* cluster centre on first mount / for entering dots, or its previous slot on
* an update) to its target slot, staggered by index. Entering dots fade in;
* moving dots stay opaque. Dots whose group colour changed (a 'flow' regroup)
* cross-fade their fill from the old colour to the new one over the same ease;
* dots whose radius changed (bubble sizing) grow/shrink over it too (circles).
*
* Position travels on a spring by default (`gather.motion`), so a gather
* interrupted by the next render resumes from where the dots actually are,
* carrying their velocity, rather than restarting from a standstill. Colour,
* radius and opacity stay on a fixed-duration ease either way: those are 0..1
* quantities, and the shared solver's rest thresholds are absolute (0.05 in
* caller units), which is negligible for pixels but 5% of a unit interval.
* @param {UnitAnimDot[]} dots
*/
_runGather(dots) {
const w$1 = this.w;
const opts = w$1.config.plotOptions.unit;
const speed = Math.max(1, w$1.config.chart.animations.speed || 800);
const maxDelay = Math.min(speed * 0.6, 450);
const n = dots.length;
for (let k = 0; k < n; k++) {
dots[k].delay = n > 1 ? k / (n - 1) * maxDelay : 0;
}
const gcfg = opts.gather || {};
const motion = gcfg.motion || "auto";
const useSpring = motion === "spring" || motion === "auto" && (!gcfg.easing || gcfg.easing === "outCubic");
if (useSpring) this._seedSprings(dots, gcfg, speed);
else if (this.ctx) this.ctx._unitSprings = null;
for (let k = 0; k < n; k++) {
const d = dots[k];
if (d.spec.pk === PK_CIRCLE && d.r0 != null && d.r1 != null && d.r0 !== d.r1) {
d.node.setAttribute("r", String(d.r0));
}
}
for (let k = 0; k < n; k++) {
const d = dots[k];
if (d.fill0 && d.fill1 && d.fill0 !== d.fill1) {
d._c0 = this._rgb(d.fill0);
d._c1 = this._rgb(d.fill1);
}
}
const easePos = gcfg.easing === "outBack" ? easeOutBack(typeof gcfg.overshoot === "number" ? gcfg.overshoot : 1.70158) : gcfg.easing === "inOutCubic" ? easeInOutCubic : easeOutCubic;
if (this.w.globals.unitGatherRAF != null) {
BrowserAPIs.cancelAnimationFrame(this.w.globals.unitGatherRAF);
this.w.globals.unitGatherRAF = null;
}
const start = performance.now();
let last = start;
const stepFn = (now) => {
if (this.w.globals.isDestroyed) {
this.w.globals.unitGatherRAF = null;
this.w.globals.animationEnded = true;
return;
}
const dt = Math.min(MAX_FRAME_STEP, Math.max(0, (now - last) / 1e3));
last = now;
let done = true;
for (let k = 0; k < n; k++) {
const d = dots[k];
const elapsed = now - start - d.delay;
const t = Math.max(0, Math.min(1, elapsed / speed));
const ec = easeOutCubic(t);
let cx, cy;
if (d.sx && d.sy) {
if (elapsed >= 0 && !d.released) {
w(d.sx, d.x);
w(d.sy, d.y);
d.released = true;
}
const restX = b(d.sx, dt);
const restY = b(d.sy, dt);
if (!d.released || !restX || !restY) done = false;
cx = d.sx.value;
cy = d.sy.value;
} else {
const e = easePos(t);
cx = d.cx0 + (d.x - d.cx0) * e;
cy = d.cy0 + (d.y - d.cy0) * e;
}
this._place(d.node, d.spec, cx, cy);
if (d.isEnter) d.node.style.opacity = String(Math.min(1, t * 2.5));
if (d._c0 && d._c1) {
const cr = Math.round(d._c0[0] + (d._c1[0] - d._c0[0]) * ec);
const cg = Math.round(d._c0[1] + (d._c1[1] - d._c0[1]) * ec);
const cb = Math.round(d._c0[2] + (d._c1[2] - d._c0[2]) * ec);
d.node.setAttribute("fill", `rgb(${cr},${cg},${cb})`);
}
if (d.spec.pk === PK_CIRCLE && d.r0 != null && d.r1 != null && d.r0 !== d.r1) {
d.node.setAttribute("r", String(d.r0 + (d.r1 - d.r0) * ec));
}
if (t < 1) done = false;
}
if (done) {
for (let k = 0; k < n; k++) {
const d = dots[k];
d.node.style.opacity = "";
if (d._c1 && d.fill1) d.node.setAttribute("fill", d.fill1);
if (d.spec.pk === PK_CIRCLE && d.r0 != null && d.r1 != null && d.r0 !== d.r1) {
d.node.setAttribute("r", String(d.r1));
}
}
this.w.globals.unitGatherRAF = null;
this.w.globals.animationEnded = true;
} else {
this.w.globals.unitGatherRAF = BrowserAPIs.requestAnimationFrame(stepFn);
}
};
this.w.globals.unitGatherRAF = BrowserAPIs.requestAnimationFrame(stepFn);
}
/**
* Keys present in the previous render but not the current one, resolved back
* to their old slot {x, y, fill}. These are the dots that must animate out.
* @param {Map<string, {x:number,y:number,fill:string,r?:number,spec?:any}>} prev
* @param {Map<string, {x:number,y:number,fill:string,r?:number,spec?:any}>} nextPrev
* @param {any} opts
* @returns {{x:number,y:number,fill:string,r?:number,spec?:any}[]}
*/
_collectExits(prev, nextPrev, opts) {
const cap = Math.max(0, opts.maxUnits || 5e3);
const exits = [];
for (const [key, slot] of prev) {
if (!nextPrev.has(key)) {
exits.push(slot);
if (exits.length >= cap) break;
}
}
return exits;
}
/**
* Animate the exit ghosts out, then remove them. Layouts whose positions carry
* data (a waffle / grid lattice, or a scatter / beeswarm on real axes) fade
* their ghosts OUT IN PLACE - drifting them toward the plot centre would drag
* cells across tiles or bubbles across the plane, which reads as wrong. The
* blob / bar layouts keep the gentle inward collapse so a removal reads as
* motion rather than a pop.
* @param {any} group @param {{x:number,y:number,fill:string,r?:number,spec?:any}[]} exits @param {any} opts
*/
_runExits(group, exits, opts) {
const w2 = this.w;
const graphics = new Graphics(w2, this.ctx);
const dotR = this._lastDotR;
const cx = w2.layout.gridWidth / 2;
const cy = w2.layout.gridHeight / 2;
const drift = opts.layout === "grid" || opts.layout === "scatter" || opts.layout === "arc" || opts.layout === "custom" ? 0 : 0.35;
const ghosts = [];
exits.forEach((slot) => {
const r = slot.r != null ? slot.r : dotR;
const spec = slot.spec || this._baseSpec(opts, r);
const el = this._drawDot(graphics, opts, r, slot.fill, 0, 0, spec);
el.node.classList.add("apexcharts-unit-exit");
this._place(el.node, spec, slot.x, slot.y);
group.add(el);
ghosts.push({ node: el.node, x0: slot.x, y0: slot.y, spec });
});
if (!this._shouldAnimate()) {
ghosts.forEach((g2) => g2.node.remove());
return;
}
const speed = Math.max(1, w2.config.chart.animations.speed || 800);
if (this.w.globals.unitExitRAF != null) {
BrowserAPIs.cancelAnimationFrame(this.w.globals.unitExitRAF);
this.w.globals.unitExitRAF = null;
}
const start = performance.now();
const stepFn = (now) => {
if (this.w.globals.isDestroyed) {
this.w.globals.unitExitRAF = null;
return;
}
const t = Math.max(0, Math.min(1, (now - start) / speed));
const e = easeOutCubic(t);
for (let k = 0; k < ghosts.length; k++) {
const g2 = ghosts[k];
if (drift) {
const x = g2.x0 + (cx - g2.x0) * e * drift;
const y = g2.y0 + (cy - g2.y0) * e * drift;
this._place(g2.node, g2.spec, x, y);
}
g2.node.style.opacity = String(1 - e);
}
if (t < 1) {
this.w.globals.unitExitRAF = BrowserAPIs.requestAnimationFrame(stepFn);
} else {
this.w.globals.unitExitRAF = null;
group.node && group.node.remove();
}
};
this.w.globals.unitExitRAF = BrowserAPIs.requestAnimationFrame(stepFn);
}
/**
* Are outer (name) labels on? Only for `layout: 'custom'`: they name a colour
* BAND, so they need categories that occupy their own part of the shape. The
* generated layouts (`packed`, `grid`) interleave categories, and the blob /
* bar / arc layouts already carry a label of their own.
* @param {any} opts
*/
_outerLabelsOn(opts) {
const cfg = opts.clusterLabels;
return !!(opts.layout === "custom" && cfg && cfg.show !== false && cfg.external && cfg.external.show);
}
/**
* One outer label's text, as lines. Two lines by default (name, then share),
* which is what makes the label readable at a distance from the band it names.
* A `clusterLabels.formatter` may return "\n"-separated text to control the
* split, or a single line.
* @param {number} i @param {number} value @param {number} total @param {any} opts
* @returns {string[]}
*/
_outerLabelLines(i, value, total, opts) {
const w2 = this.w;
const name = w2.seriesData.seriesNames[i] || `series-${i + 1}`;
const percent = total > 0 ? value / total * 100 : 0;
const cfg = opts.clusterLabels;
const text = typeof cfg.formatter === "function" ? cfg.formatter(name, { seriesIndex: i, value, percent, w: w2 }) : `${name}
${percent.toFixed(1)}%`;
return String(text).split("\n");
}
/**
* Room one side has to give up: the widest label, plus the leader line, plus a
* little air. Capped at a quarter of the plot so one long category name shrinks
* its own label into the gutter instead of starving the shape.
* @param {number[]} counts @param {any} opts
* @returns {number}
*/
_outerLabelGutter(counts, opts) {
const w2 = this.w;
const cfg = opts.clusterLabels;
const conn = cfg.external.connector || {};
const total = counts.reduce((a, b2) => a + b2, 0);
const lines = [];
counts.forEach((c, i) => {
if (c > 0) lines.push(...this._outerLabelLines(i, c, total, opts));
});
if (!lines.length) return 0;
const width = measureLabelWidth(w2, lines, {
fontSize: cfg.fontSize,
fontFamily: cfg.fontFamily || w2.config.chart.fontFamily
});
const gap = conn.gap != null ? conn.gap : 8;
const length = conn.length != null ? conn.length : 22;
const room = width + gap + length + 8 + Math.abs(parseFloat(cfg.external.offsetX) || 0);
return Math.min(room, w2.layout.gridWidth * 0.25);
}
/**
* Plan and draw the outer labels. A band's anchor is one of its own dots - the
* outermost on the label's side, preferring dots near the band's middle - so
* the leader line lands on the crowd rather than on a bounding box the viewer
* cannot see.
*
* Sides: a silhouette ordered by rows stacks its categories vertically, so
* their centroids share an x and the labels have to alternate left/right down
* the shape. One ordered by columns spreads them horizontally, so each label
* goes to the side its band is already on.
*
* @param {any} ret @param {{ i:number, cx:number, cy:number, dots:{x:number,y:number,r?:number}[] }[]} clusters
* @param {number[]} counts @param {number} total @param {any} opts
* @param {boolean} gathering true only when the dots are flying in from the
* centre (first render / cross-type morph). On an update the crowd is already
* on screen, so the labels must not wait for anything.
*/
_drawOuterLabels(ret, clusters, counts, total, opts, gathering) {
const w2 = this.w;
if (!Environment.isBrowser()) return;
const cfg = opts.clusterLabels;
const ext = cfg.external;
const conn = ext.connector || {};
const gap = conn.gap != null ? conn.gap : 8;
const length = conn.length != null ? conn.length : 22;
const offsetX = parseFloat(ext.offsetX) || 0;
const offsetY = parseFloat(ext.offsetY) || 0;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
const dotR = this._lastDotR;
const live = clusters.filter((c) => c.dots.length > 0);
if (!live.length) return;
let spreadX = 0;
let spreadY = 0;
if (live.length > 1) {
const xs = live.map((c) => c.cx);
const ys = live.map((c) => c.cy);
spreadX = Math.max(...xs) - Math.min(...xs);
spreadY = Math.max(...ys) - Math.min(...ys);
}
const bandedByX = spreadX > spreadY;
const fontSize = parseFloat(cfg.fontSize) || 13;
const lineHeight = Math.round(fontSize * 1.35);
const items = [];
live.slice().sort((a, b2) => a.cy - b2.cy).forEach((c, k) => {
const lines = this._outerLabelLines(c.i, counts[c.i], total, opts);
if (!lines.some((l) => l !== "")) return;
const side = bandedByX ? c.cx >= gw / 2 ? "right" : "left" : k % 2 === 0 ? "right" : "left";
const dir = side === "right" ? 1 : -1;
let best = c.dots[0];
let bestScore = -Infinity;
c.dots.forEach((d) => {
const score = dir * d.x - 0.75 * Math.abs(d.y - c.cy);
if (score > bestScore) {
bestScore = score;
best = d;
}
});
const anchor = { x: best.x + dir * (best.r || dotR), y: best.y };
const elbow = { x: anchor.x + dir * gap, y: anchor.y };
items.push({
i: c.i,
lines,
anchor,
elbow,
labelX: elbow.x + dir * length + offsetX,
idealY: anchor.y + offsetY,
labelY: anchor.y + offsetY,
side
});
});
if (!items.length) return;
const maxLines = items.reduce((m2, it) => Math.max(m2, it.lines.length), 1);
const block = maxLines * lineHeight;
const half = block / 2;
["left", "right"].forEach((side) => {
spaceOutLabels(
items.filter((it) => it.side === side),
block + 2,
gh - half,
half
);
});
const group = new Graphics(w2, this.ctx).group({
class: "apexcharts-unit-outer-labels"
});
if (gathering) {
const speed = Math.max(1, w2.config.chart.animations.speed || 800);
group.node.classList.add("apexcharts-unit-label-delay");
group.node.style.animationDelay = `${Math.min(speed * 0.45, 600) / 1e3}s`;
}
items.forEach((it) => {
const color = w2.globals.colors[it.i] || w2.globals.colors[0] || "#008FFB";
group.add(
drawOuterLabel(w2, {
lines: it.lines,
lineHeight,
anchor: it.anchor,
elbow: it.elbow,
labelX: it.labelX,
labelY: it.labelY,
side: it.side,
connector: {
show: conn.show !== false,
width: conn.width != null ? conn.width : 1.5,
color: conn.color || color
},
style: {
fontSize: cfg.fontSize,
fontFamily: cfg.fontFamily || w2.config.chart.fontFamily,
fontWeight: cfg.fontWeight
},
foreColor: cfg.color || w2.config.chart.foreColor,
groupClass: "apexcharts-unit-outer-label-group",
textClass: "apexcharts-unit-outer-label",
connectorClass: "apexcharts-unit-label-connector"
})
);
});
ret.add(group);
}
/**
* A cluster label placed above (default) or below the cluster/bar. A TOP label
* over a wide grouped/packed blob rides a curved arc (invisible arc path +
* <textPath>, centred at 50% offset); a bottom label, a 'columns' bar, or a
* cluster too small for the arc gets a straight centred label instead.
* `clusterLabels.position` = 'top' | 'bottom'; `offsetY` pushes it further from
* the blob in either direction.
* @param {any} elSeries @param {{ i:number, cx:number, cy:number, outerR:number, flat?:boolean }} cluster
* @param {number} value @param {number} total @param {any} opts @param {string} color
*/
_drawClusterLabel(elSeries, cluster, value, total, opts, color) {
const w2 = this.w;
if (!Environment.isBrowser()) return;
const NS = "http://www.w3.org/2000/svg";
const name = w2.seriesData.seriesNames[cluster.i] || `series-${cluster.i + 1}`;
const percent = total > 0 ? value / total * 100 : 0;
const cfg = opts.clusterLabels;
const fontSize = parseFloat(cfg.fontSize) || 13;
let text;
if (typeof cfg.formatter === "function") {
text = cfg.formatter(name, {
seriesIndex: cluster.i,
value,
percent,
w: w2
});
} else {
text = `${name} (${percent.toFixed(1)}%)`;
}
const str = typeof text === "string" ? text : String(text);
const textEl = BrowserAPIs.createElementNS(NS, "text");
textEl.setAttribute("class", "apexcharts-unit-label");
textEl.setAttribute("text-anchor", "middle");
textEl.setAttribute("font-size", `${fontSize}px`);
textEl.setAttribute("font-family", cfg.fontFamily || w2.config.chart.fontFamily || "inherit");
textEl.setAttribute("font-weight", String(cfg.fontWeight || 600));
textEl.setAttribute("fill", cfg.color || color);
const bottom = cfg.position === "bottom";
const R = cluster.outerR + fontSize * 0.6 + 3 + (cfg.offsetY || 0);
const estWidth = str.length * fontSize * 0.55;
const curved = !bottom && !cluster.flat && cfg.curved !== false && estWidth <= Math.PI * R * 0.95;
if (curved) {
const yMid = cluster.cy;
const x1 = cluster.cx - R;
const x2 = cluster.cx + R;
const d = `M ${x1} ${yMid} A ${R} ${R} 0 0 1 ${x2} ${yMid}`;
const arcId = `apexcharts-unit-label-${w2.globals.chartID}-${cluster.i}`;
const pathEl = BrowserAPIs.createElementNS(NS, "path");
pathEl.setAttribute("id", arcId);
pathEl.setAttribute("d", d);
pathEl.setAttribute("fill", "none");
pathEl.setAttribute("stroke", "none");
const tp = BrowserAPIs.createElementNS(NS, "textPath");
tp.setAttribute("href", `#${arcId}`);
tp.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", `#${arcId}`);
tp.setAttribute("startOffset", "50%");
tp.textContent = str;
textEl.appendChild(tp);
elSeries.node.appendChild(pathEl);
} else {
textEl.setAttribute("x", String(cluster.cx));
const y = bottom ? cluster.cy + cluster.outerR + fontSize + 6 + (cfg.offsetY || 0) : cluster.cy - cluster.outerR - 6 - (cfg.offsetY || 0);
textEl.setAttribute("y", String(y));
textEl.textContent = str;
}
elSeries.node.appendChild(textEl);
}
}
const D2R = Math.PI / 180;
const R2D = 180 / Math.PI;
const SVGNS = "http://www.w3.org/2000/svg";
const XHTML = "http://www.w3.org/1999/xhtml";
const lerp = (a, b2, t) => a + (b2 - a) * t;
class SunburstChart {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w2, ctx) {
this.ctx = ctx;
this.w = w2;
const cnf = w2.config;
this.cfg = cnf.plotOptions.sunburst;
this.strokeWidth = cnf.stroke.show ? cnf.stroke.width : 0;
this.strokeColor = Array.isArray(cnf.stroke.colors) ? cnf.stroke.colors[0] : cnf.stroke.colors || "#fff";
this.startAngle = this.cfg.startAngle;
this.endAngle = this.cfg.endAngle;
this.maxDepth = 0;
this.centerX = 0;
this.centerY = 0;
this.maxRadius = 0;
this.total = 1;
this._focusMaxDepth = 0;
this._focus = null;
this._zoomGen = 0;
this._roots = [];
this._nodesAll = [];
this._innerR = () => 0;
this._outerR = () => 0;
this._tooltipEl = null;
this._lblSeq = 0;
this._morphLeafIndex = 0;
this._graphics = null;
this._ringsG = null;
this._labelsG = null;
}
/**
* @param {any[]} series flattened top-level values (geometry comes from the
* config hierarchy; kept for the standard draw(series) signature + noData)
* @returns {any} SVG group
*/
draw(series) {
const w2 = this.w;
const graphics = new Graphics(this.w);
this._graphics = graphics;
const g2 = graphics.group({ class: "apexcharts-sunburst" });
if (w2.globals.noData || !series || !series.length) return g2;
const gw = w2.layout.gridWidth;
const gh = w2.layout.gridHeight;
this.centerX = gw / 2 + (this.cfg.offsetX || 0);
this.centerY = gh / 2 + (this.cfg.offsetY || 0);
this.maxRadius = Math.min(gw, gh) / 2.05 - this.strokeWidth - (!w2.config.chart.sparkline.enabled ? w2.config.chart.dropShadow.blur : 0);
if (this.maxRadius < 5) return g2;
this._roots = this._buildHierarchy();
if (!this._roots.length) return g2;
this._roots.forEach((r) => this._fillValues(r));
this._validateStrict();
this._nodesAll = [];
const colors = w2.globals.colors || [];
this._roots.forEach((r, i) => {
this._colorPass(r, r.color || colors[i % colors.length] || "#008FFB");
});
this.total = this._roots.reduce((s, r) => s + Math.max(0, r.value), 0) || 1;
this._ringsG = graphics.group({ class: "apexcharts-sunburst-rings" });
this._labelsG = graphics.group({ class: "apexcharts-sunburst-labels" });
g2.add(this._ringsG);
g2.add(this._labelsG);
this._focus = null;
this._relayout(this._focus);
const anims = w2.config.chart.animations;
let mode = "none";
if (anims.enabled) {
if (w2.globals.dataChanged) {
if (anims.dynamicAnimation.enabled) mode = "update";
} else if (!w2.globals.resized) {
mode = "intro";
}
}
this._applyLayout(mode);
this._renderBreadcrumb();
return g2;
}
// ------------------------------------------------------------------ data
/**
* Resolve the config into root nodes `{ name, value, color?, children? }`.
* Each datum may carry `children` (native) or `drilldown: '<id>'` (adapter).
* Shared with the treemap - see charts/common/Hierarchy.
* @returns {any[]}
*/
_buildHierarchy() {
return buildHierarchy(this.w);
}
/**
* Fill a parent's value from its children when missing.
* @param {any} node
*/
_fillValues(node) {
fillValues(node);
}
/**
* With `partition: 'strict'`, warn (once) when a parent's value does not
* match the sum of its children. The angles are still normalized to fill the
* wedge (strict rendering is a P3 refinement); this just surfaces the data
* mismatch.
*/
_validateStrict() {
if (this.cfg.partition !== "strict") return;
let warned = false;
const walk = (node) => {
if (node.children && node.children.length) {
const sum = node.children.reduce(
(s, c) => s + Math.max(0, c.value || 0),
0
);
if (!warned && node.value != null && Math.abs(sum - node.value) > 0.5) {
console.warn(
`ApexCharts sunburst: partition 'strict' but "${node.name}" (${node.value}) != sum of its children (${sum}). Angles are normalized to fill the wedge.`
);
warned = true;
}
node.children.forEach(walk);
}
};
this._roots.forEach(walk);
}
/**
* Assign a colour to every node (explicit `color` wins, else the parent's
* colour tinted lighter). Done once so zoom preserves colours.
* @param {any} node
* @param {string} color
*/
_colorPass(node, color) {
node._color = node.color || color;
this._nodesAll.push(node);
if (node.children) {
node.children.forEach(
(c) => this._colorPass(c, c.color || this._lighten(node._color, this.cfg.tint))
);
}
}
// ---------------------------------------------------------------- layout
/**
* Recompute visibility + angles + radii for a focus node (null = whole tree).
* @param {any} focus
*/
_relayout(focus) {
this._nodesAll.forEach((n) => {
n._show = false;
});
this._focusMaxDepth = 0;
if (!focus) {
const total = this._roots.reduce((s, r) => s + Math.max(0, r.value), 0) || 1;
let a = this.startAngle;
this._roots.forEach((r) => {
const span = (this.endAngle - this.startAngle) * Math.max(0, r.value) / total;
this._placeVis(r, 0, a, a + span, null);
a += span;
});
} else {
this._placeVis(focus, 0, this.startAngle, this.endAngle, focus._parent);
}
const hole = this._parseSize(this.cfg.innerSize, this.maxRadius);
const ringCount = this._focusMaxDepth + 1;
const band = (this.maxRadius - hole) / ringCount;
const radialGap = ringCount > 1 ? 1 : 0;
this._innerR = (vd) => hole + vd * band + (vd > 0 ? radialGap / 2 : 0);
this._outerR = (vd) => hole + (vd + 1) * band - radialGap / 2;
this._nodesAll.forEach((n) => {
if (!n._show) return;
n._iR = this._innerR(n._vDepth);
n._oR = n._leaf && this.cfg.leaf === "extend" && n._vDepth < this._focusMaxDepth ? this.maxRadius : this._outerR(n._vDepth);
});
}
/**
* @param {any} node
* @param {number} vDepth
* @param {number} a0
* @param {number} a1
* @param {any} parent
*/
_placeVis(node, vDepth, a0, a1, parent) {
node._show = true;
node._vDepth = vDepth;
node._a0 = a0;
node._a1 = a1;
node._parent = parent;
node._leaf = !(node.children && node.children.length);
if (vDepth > this._focusMaxDepth) this._focusMaxDepth = vDepth;
if (!node._leaf) {
const total = node.children.reduce(
(s, c) => s + Math.max(0, c.value),
0
) || 1;
let a = a0;
node.children.forEach((c) => {
const span = (a1 - a0) * Math.max(0, c.value) / total;
this._placeVis(c, vDepth + 1, a, a + span, node);
a += span;
});
}
}
// --------------------------------------------------------------- render
/**
* Create / update / remove arc elements to match the current layout, with an
* animation appropriate to the transition:
* intro — pie/donut-style angular clock sweep from startAngle to endAngle
* (all rings reveal together as the sweep line passes them)
* update — morph every arc from its previous on-screen geometry (matched
* by node key across the re-render); new arcs unfurl in place
* zoom — tween angles + radii between focus layouts (same instance)
* @param {'intro'|'zoom'|'update'|'none'} mode
*/
_applyLayout(mode) {
var _a;
const w2 = this.w;
const anims = w2.config.chart.animations;
const dur = !anims.enabled ? 0 : mode === "none" ? 0 : mode === "update" ? anims.dynamicAnimation.speed || 350 : anims.speed || 500;
const gen = ++this._zoomGen;
const morph = (
/** @type {any} */
(_a = this.ctx) == null ? void 0 : _a.morphTypeChange
);
const morphActive = !!morph && typeof morph.isActive === "function" && morph.isActive() && typeof morph.getInitialPathAt === "function";
const morphDur = morphActive ? morph.getSpeed() : 0;
this._morphLeafIndex = 0;
const prev = mode === "update" ? (
/** @type {any} */
this.ctx._sunburstPrevGeoms
) : null;
this._nodesAll.forEach((node) => {
if (node._show) {
const target = {
a0: node._a0,
a1: node._a1,
iR: node._iR,
oR: node._oR
};
if (!node._el) node._el = this._createArcEl(node);
const morphFrom = morphActive ? this._morphSourceFor(node) : null;
if (morphFrom && morphDur > 0) {
this._morphArcFrom(node, morphFrom, target, morphDur, gen);
} else if ((mode === "intro" || morphActive) && dur > 0) {
this._sweepArc(node, target, morphActive ? morphDur : dur, gen);
} else {
let from;
let isNew = false;
if (node._cur) {
from = node._cur;
} else if (prev && prev.get(node._key)) {
from = prev.get(node._key);
} else {
const mid = (target.a0 + target.a1) / 2;
from = { a0: mid, a1: mid, iR: target.iR, oR: target.iR };
isNew = true;
}
this._animateArc(node, from, target, dur, false, isNew, gen);
}
} else if (node._el && node._cur) {
const mid = (node._cur.a0 + node._cur.a1) / 2;
const target = { a0: mid, a1: mid, iR: node._cur.iR, oR: node._cur.iR };
this._animateArc(node, node._cur, target, dur, true, false, gen);
}
});
const geoms = /* @__PURE__ */ new Map();
this._nodesAll.forEach((n) => {
if (n._show) {
geoms.set(n._key, { a0: n._a0, a1: n._a1, iR: n._iR, oR: n._oR });
}
});
this.ctx._sunburstPrevGeoms = geoms;
this._renderLabels(dur);
}
/**
* The captured mark this leaf should unroll from, or null when the node is
* the outgoing chart had nothing to give it.
*
* When both charts carry branch keys the pairing is by identity, so EVERY
* ring finds the tile that stood for the same branch and the inner rings
* unroll instead of appearing from nothing.
*
* Without keys (a flat treemap, or an older config) only leaves pair, and
* they consume the captured paths in draw order - the order the outgoing
* renderer laid its own marks out in, so tile k pairs with leaf k.
*
* @param {any} node
* @returns {string | null}
*/
_morphSourceFor(node) {
const ctx = (
/** @type {any} */
this.ctx
);
const morph = ctx && ctx.morphTypeChange;
if (!morph) return null;
if (typeof morph.hasKeyedMarks === "function" && morph.hasKeyedMarks() && typeof morph.getInitialPathForKey === "function") {
return morph.getInitialPathForKey(morphKey(node._key));
}
if (node.children && node.children.length) return null;
return morph.getInitialPathAt(this._morphLeafIndex++);
}
/**
* Unroll an arc from an arbitrary captured shape.
*
* `_animateArc` interpolates arc PARAMETERS (angles and radii), which cannot
* express a rectangle, so this one tweens the path data itself through
* Animations.morphSVG - the same call every other morphing renderer makes,
* which already selects the polygon-resample algorithm while a cross-type
* morph is active.
*
* @param {any} node
* @param {string} fromD
* @param {{a0:number,a1:number,iR:number,oR:number}} target
* @param {number} dur
* @param {number} gen
*/
_morphArcFrom(node, fromD, target, dur, gen) {
const el = node._el;
const toD = this._arcPath(
target.iR,
target.oR,
target.a0,
target.a1,
this.cfg.borderRadius
);
el.node.style.display = "";
el.attr({ d: fromD, opacity: 1 });
node._cur = target;
if (this._zoomGen !== gen) return;
new Animations(
this.w,
/** @type {any} */
this.ctx
).morphSVG(
el,
0,
// Not a (series, point) index: an arc has no j, and passing a number here
// would make morphSVG treat it as the last point of a series and fire the
// chart's animation-completed hook.
/** @type {any} */
null,
node._color,
fromD,
toD,
dur,
0
);
}
/**
* Pie/donut-style intro: a clock sweep from startAngle to endAngle. Each
* arc's end angle is clamped to the sweep line, so arcs appear in angular
* order and grow until complete — the whole hierarchy unwipes together.
* @param {any} node
* @param {{a0:number,a1:number,iR:number,oR:number}} target
* @param {number} dur
* @param {number} gen layout generation; frames stop once superseded
*/
_sweepArc(node, target, dur, gen) {
const el = node._el;
const br = this.cfg.borderRadius;
const s0 = this.startAngle;
const s1 = this.endAngle;
el.node.style.display = "";
el.attr({ d: "", opacity: 1 });
el.animate(dur).during((pos) => {
if (this._zoomGen !== gen) return;
const sweep = s0 + (s1 - s0) * pos;
if (sweep <= target.a0 + 0.01) {
el.attr({ d: "" });
return;
}
const a1 = Math.min(target.a1, sweep);
el.attr({ d: this._arcPath(target.iR, target.oR, target.a0, a1, br) });
node._cur = { a0: target.a0, a1, iR: target.iR, oR: target.oR };
}).after(() => {
if (this._zoomGen !== gen) return;
node._cur = target;
});
}
/**
* @param {any} node
* @returns {any} svg.js path element
*/
_createArcEl(node) {
const path = this._graphics.drawPath({
d: "",
fill: node._color,
stroke: this.strokeColor,
strokeWidth: this.strokeWidth,
fillOpacity: 1,
classes: "apexcharts-sunburst-arc"
});
const el = path.node;
el.setAttribute("data:name", node.name);
el.setAttribute("data:value", String(node.value));
el.setAttribute("data:key", morphKey(node._key));
el.setAttribute(
"data:leaf",
String(!(node.children && node.children.length))
);
this._attachTooltip(el, node);
if (Environment.isBrowser()) {
el.addEventListener("click", () => this._zoomTo(node));
el.style.cursor = "pointer";
}
this._ringsG.add(path);
return path;
}
/**
* @param {any} node
* @param {{a0:number,a1:number,iR:number,oR:number}} from
* @param {{a0:number,a1:number,iR:number,oR:number}} to
* @param {number} dur
* @param {boolean} hide shrink + fade out, then hide
* @param {boolean} fadeIn fade 0 -> 1 (new arcs only; morphs stay opaque)
* @param {number} gen layout generation; frames stop once superseded
*/
_animateArc(node, from, to, dur, hide, fadeIn, gen) {
const el = node._el;
const br = this.cfg.borderRadius;
el.attr({ fill: node._color });
if (dur === 0) {
el.attr({ d: this._arcPath(to.iR, to.oR, to.a0, to.a1, br), opacity: hide ? 0 : 1 });
el.node.style.display = hide ? "none" : "";
node._cur = hide ? null : to;
return;
}
el.node.style.display = "";
const startOp = hide ? Number(el.attr("opacity")) || 1 : fadeIn ? 0 : 1;
const endOp = hide ? 0 : 1;
el.attr({ opacity: startOp });
el.animate(dur).during((pos) => {
if (this._zoomGen !== gen) return;
const a0 = lerp(from.a0, to.a0, pos);
const a1 = lerp(from.a1, to.a1, pos);
const iR = lerp(from.iR, to.iR, pos);
const oR = lerp(from.oR, to.oR, pos);
el.attr({
d: this._arcPath(iR, oR, a0, a1, br),
opacity: lerp(startOp, endOp, pos)
});
node._cur = { a0, a1, iR, oR };
}).after(() => {
if (this._zoomGen !== gen) return;
if (hide) {
el.node.style.display = "none";
node._cur = null;
} else {
node._cur = to;
}
});
}
// ---------------------------------------------------------------- labels
/**
* Labels are overlays on animated paths, so they reveal gradually AFTER the
* arcs settle (repo convention: overlays never pop in over a moving path).
* @param {number} dur arc animation duration (0 = instant labels)
*/
_renderLabels(dur) {
const labelsG = this._labelsG;
while (labelsG.node.firstChild) labelsG.node.removeChild(labelsG.node.firstChild);
if (!this.cfg.dataLabels.show) return;
this._nodesAll.forEach((node) => {
if (!node._show) return;
if (node._a1 - node._a0 < this.cfg.dataLabels.minAngleToShow) return;
this._renderCurvedLabel(node);
});
if (dur > 0) {
labelsG.attr({ opacity: 0 });
labelsG.animate(250, dur).attr({ opacity: 1 });
} else {
labelsG.attr({ opacity: 1 });
}
}
/**
* Curved label along the arc's mid-radius (flipped on the bottom half so it
* stays upright). Raw SVG <textPath> — svg.js has no first-class textPath.
* @param {any} node
*/
_renderCurvedLabel(node) {
if (!Environment.isBrowser()) return;
const style = this.cfg.dataLabels.style;
const w2 = this.w;
const r = (node._iR + node._oR) / 2;
const mid = (node._a0 + node._a1) / 2;
const flip = mid > 90 && mid < 270;
const padDeg = Math.min(r > 0 ? 4 / r * R2D : 0, (node._a1 - node._a0) / 2);
const from = flip ? node._a1 - padDeg : node._a0 + padDeg;
const to = flip ? node._a0 + padDeg : node._a1 - padDeg;
const p1 = this._ptAt(r, from);
const p2 = this._ptAt(r, to);
const largeArc = Math.abs(to - from) > 180 ? 1 : 0;
const sweep = flip ? 0 : 1;
const id = `apx-sb-lbl-${w2.globals.cuid}-${this._lblSeq++}`;
const guide = BrowserAPIs.createElementNS(SVGNS, "path");
guide.setAttribute("id", id);
guide.setAttribute(
"d",
`M ${p1.x} ${p1.y} A ${r} ${r} 0 ${largeArc} ${sweep} ${p2.x} ${p2.y}`
);
guide.setAttribute("fill", "none");
guide.setAttribute("stroke", "none");
const colors = style.colors;
const fill = (Array.isArray(colors) ? colors[0] : colors) || "#fff";
const text = BrowserAPIs.createElementNS(SVGNS, "text");
text.setAttribute("font-size", style.fontSize || "12px");
if (style.fontFamily) text.setAttribute("font-family", style.fontFamily);
text.setAttribute("font-weight", String(style.fontWeight || 400));
text.setAttribute("fill", fill);
text.setAttribute("dominant-baseline", "central");
text.style.pointerEvents = "none";
const tp = BrowserAPIs.createElementNS(SVGNS, "textPath");
tp.setAttribute("href", "#" + id);
tp.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#" + id);
tp.setAttribute("startOffset", "50%");
tp.setAttribute("text-anchor", "middle");
tp.textContent = this._truncate(node.name, r, node._a1 - node._a0, style.fontSize);
text.appendChild(tp);
this._labelsG.node.appendChild(guide);
this._labelsG.node.appendChild(text);
}
/**
* Trim a label to the arc length available at its radius.
* @param {string} name
* @param {number} r
* @param {number} spanDeg
* @param {string} fontSize
* @returns {string}
*/
_truncate(name, r, spanDeg, fontSize) {
const arcLen = r * spanDeg * D2R - 8;
const charW = (parseFloat(fontSize) || 12) * 0.58;
const maxChars = Math.floor(arcLen / charW);
if (maxChars >= name.length) return name;
if (maxChars <= 1) return "";
return name.slice(0, Math.max(1, maxChars - 1)) + "…";
}
// ----------------------------------------------------------------- zoom
/**
* Focus a node (zoom in), or zoom out one level when the current focus (the
* innermost ring) is clicked.
* @param {any} node
*/
_zoomTo(node) {
if (this.cfg.zoomOnClick === false) return;
const nextFocus = node === this._focus ? node._parent || null : node;
if (nextFocus === this._focus) return;
this._focus = nextFocus && !(nextFocus.children && nextFocus.children.length) ? nextFocus._parent || null : nextFocus;
this._relayout(this._focus);
this._applyLayout("zoom");
this._renderBreadcrumb();
}
/** Root -> focus chain of nodes. */
_focusChain() {
const chain = [];
let n = this._focus;
while (n) {
chain.unshift(n);
n = n._parent;
}
return chain;
}
/**
* Minimal self-contained breadcrumb (reuses the shared `.apexcharts-breadcrumb`
* CSS classes, but does NOT depend on the drilldown feature).
*/
_renderBreadcrumb() {
if (!Environment.isBrowser()) return;
const w2 = this.w;
const elWrap = w2.dom.elWrap;
if (!elWrap) return;
const existing = elWrap.querySelector(".apexcharts-breadcrumb");
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
if (!this._focus) return;
const nav = BrowserAPIs.createElementNS(XHTML, "nav");
nav.setAttribute("class", "apexcharts-breadcrumb");
nav.setAttribute("aria-label", "Sunburst breadcrumb");
nav.style.position = "absolute";
nav.style.top = "0px";
nav.style.left = "0px";
const crumbs = [{ name: "All", node: null }].concat(
this._focusChain().map((n) => ({ name: n.name, node: n }))
);
crumbs.forEach((crumb, i) => {
if (i > 0) {
const sep = BrowserAPIs.createElementNS(XHTML, "span");
sep.setAttribute("class", "apexcharts-breadcrumb-separator");
sep.textContent = " / ";
nav.appendChild(sep);
}
const isCurrent = i === crumbs.length - 1;
if (isCurrent) {
const cur = BrowserAPIs.createElementNS(XHTML, "span");
cur.setAttribute(
"class",
"apexcharts-breadcrumb-item apexcharts-breadcrumb-current"
);
cur.textContent = crumb.name;
nav.appendChild(cur);
} else {
const btn = 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.textContent = "←";
btn.appendChild(arrow);
}
const text = BrowserAPIs.createElementNS(XHTML, "span");
text.setAttribute("class", "apexcharts-breadcrumb-label");
text.textContent = crumb.name;
btn.appendChild(text);
btn.addEventListener("click", () => {
this._focus = crumb.node;
this._relayout(this._focus);
this._applyLayout("zoom");
this._renderBreadcrumb();
});
nav.appendChild(btn);
}
});
elWrap.appendChild(nav);
this._avoidChromeOverlap(nav);
}
/**
* The breadcrumb is an absolute overlay at top-left, so it can sit on top of
* a left-aligned title (or subtitle). After mounting, push it below any chart
* chrome it intersects. (Duplicated from drilldown's Breadcrumb on purpose —
* sunburst must not import the drilldown feature.)
* @param {any} nav
*/
/**
* Shared with the treemap - see charts/common/Breadcrumb.
* @param {any} nav
*/
_avoidChromeOverlap(nav) {
avoidChromeOverlap(this.w, nav);
}
// ------------------------------------------------------------ geometry
/**
* @param {number} r
* @param {number} deg 0 = top, clockwise
* @returns {{x: number, y: number}}
*/
_ptAt(r, deg) {
return {
x: this.centerX + r * Math.cos((deg - 90) * D2R),
y: this.centerY + r * Math.sin((deg - 90) * D2R)
};
}
/**
* Rounded donut-segment path (inner radius always > 0). Applies `spacing`
* (angular gap) and `borderRadius` (corner rounding), both clamped so a thin
* arc never inverts.
* @param {number} iR
* @param {number} oR
* @param {number} a0
* @param {number} a1
* @param {number} borderRadius
* @returns {string}
*/
_arcPath(iR, oR, a0, a1, borderRadius) {
if (oR <= iR + 0.01) return "";
let spanDeg = a1 - a0;
const spacing = this.cfg.spacing;
if (spacing > 0 && spanDeg > 0 && oR > 0) {
const gapDeg = spacing / oR * R2D;
const inset = Math.min(gapDeg / 2, Math.max(0, spanDeg / 2 - 0.25));
a0 += inset;
a1 -= inset;
spanDeg = a1 - a0;
}
if (spanDeg <= 0) return "";
const spanRad = spanDeg * D2R;
const cx = this.centerX;
const cy = this.centerY;
let r = borderRadius;
r = Math.min(r, spanRad * iR / 2, spanRad * oR / 2, (oR - iR) / 2);
if (!(r > 0.5)) {
return sharpDonutSegmentPath({ cx, cy, rIn: iR, rOut: oR, a0, a1, spanDeg });
}
return roundedDonutSegmentPath({ cx, cy, rIn: iR, rOut: oR, a0, a1, r, spanDeg });
}
// ------------------------------------------------------------- tooltip
/** @returns {any} */
_tip() {
if (!this._tooltipEl) {
this._tooltipEl = this.w.dom.baseEl.querySelector(".apexcharts-tooltip");
}
return this._tooltipEl;
}
/**
* @param {any} el
* @param {any} node
*/
_attachTooltip(el, node) {
if (!this.w.config.tooltip.enabled || !Environment.isBrowser()) return;
el.addEventListener(
"mouseenter",
(e) => this._showTooltip(e, node)
);
el.addEventListener(
"mousemove",
(e) => this._positionTooltip(e)
);
el.addEventListener("mouseleave", () => this._hideTooltip());
}
/**
* @param {MouseEvent} e
* @param {any} node
*/
_showTooltip(e, node) {
const t = this._tip();
if (!t) return;
const w2 = this.w;
const pctTotal = (node.value / this.total * 100).toFixed(1);
const parentVal = node._parent ? node._parent.value : this.total;
const pctParent = parentVal > 0 ? (node.value / parentVal * 100).toFixed(1) : pctTotal;
const groupBg = w2.config.tooltip.fillSeriesColor ? `background-color:${node._color};` : "";
t.innerHTML = `<div class="apexcharts-tooltip-series-group apexcharts-active" style="display:flex;${groupBg}"><span class="apexcharts-tooltip-marker" style="background-color:${node._color}"></span><div class="apexcharts-tooltip-text"><div class="apexcharts-tooltip-y-group"><span class="apexcharts-tooltip-text-y-label">${node.name}: </span><span class="apexcharts-tooltip-text-y-value">${node.value} (${pctParent}% of parent, ${pctTotal}% of total)</span></div></div></div>`;
t.classList.add("apexcharts-active");
t.style.opacity = "1";
this._positionTooltip(e);
}
/**
* Position beside the cursor, flipping to the opposite side when the box
* would overflow the chart wrap, and clamping inside it either way.
* @param {MouseEvent} e
*/
_positionTooltip(e) {
const t = this._tip();
if (!t) return;
const rect = this.w.dom.elWrap.getBoundingClientRect();
const tw = t.offsetWidth;
const th = t.offsetHeight;
const pad = 12;
let x = e.clientX - rect.left + pad;
if (x + tw > rect.width) x = e.clientX - rect.left - tw - pad;
x = Math.max(0, Math.min(x, rect.width - tw));
let y = e.clientY - rect.top + pad;
if (y + th > rect.height) y = e.clientY - rect.top - th - pad;
y = Math.max(0, Math.min(y, rect.height - th));
t.style.left = x + "px";
t.style.top = y + "px";
}
_hideTooltip() {
const t = this._tip();
if (!t) return;
t.classList.remove("apexcharts-active");
t.style.opacity = "0";
}
// --------------------------------------------------------------- utils
/**
* @param {string|number} size
* @param {number} max
* @returns {number}
*/
_parseSize(size, max) {
if (typeof size === "number") return size;
const s = String(size).trim();
if (s.endsWith("%")) return parseFloat(s) / 100 * max;
const n = parseFloat(s);
return isNaN(n) ? 0.15 * max : n;
}
/**
* Blend a hex colour toward white by `amount` (0..1). Non-hex returned as-is.
* @param {string} color
* @param {number} amount
* @returns {string}
*/
_lighten(color, amount) {
if (typeof color !== "string" || color[0] !== "#") return color;
let hex = color.slice(1);
if (hex.length === 3) {
hex = hex.split("").map((c) => c + c).join("");
}
if (hex.length !== 6) return color;
const num = parseInt(hex, 16);
if (isNaN(num)) return color;
let rC = num >> 16 & 255;
let gC = num >> 8 & 255;
let bC = num & 255;
rC = Math.round(rC + (255 - rC) * amount);
gC = Math.round(gC + (255 - gC) * amount);
bC = Math.round(bC + (255 - bC) * amount);
return "#" + ((1 << 24) + (rC << 16) + (gC << 8) + bC).toString(16).slice(1);
}
}
_core__default.use({
line: Line,
area: Line,
scatter: Line,
bubble: Line,
rangeArea: Line,
bar: Bar,
column: Bar,
barStacked: BarStacked,
rangeBar: RangeBar,
candlestick: BoxCandleStick,
boxPlot: BoxCandleStick,
violin: Violin,
pie: Pie,
donut: Pie,
polarArea: Pie,
radialBar: Radial,
radar: Radar,
heatmap: HeatMap,
treemap: TreemapChart,
unit: Unit,
sunburst: SunburstChart
});
Object.defineProperty(_core__default, "__internals", {
value: coreInternals,
enumerable: false,
writable: false,
configurable: false
});
export {
default2 as default
};