apexcharts
Version:
A JavaScript Chart Library
2,091 lines • 71.8 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, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
/*!
* 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";
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 w = row[i] / areaHeight;
coords.push([subX, yoffset, subX + w, yoffset + areaHeight]);
subX += w;
}
}
}
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 v = Number(node.value);
const a = isNaN(v) ? 0 : Math.abs(v);
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 };
const Graphics = _core.__apex_Graphics;
const Animations = _core.__apex_Animations;
const Fill = _core.__apex_Fill;
const Utils = _core.__apex_Utils;
const DataLabels = _core.__apex_DataLabels;
const resolveDataLabelOffset = (value, w, seriesIndex, dataPointIndex) => {
if (typeof value !== "function") return value;
const resolved = value({
series: w.seriesData.series,
seriesIndex,
dataPointIndex,
w
});
return Number.isFinite(resolved) ? resolved : 0;
};
class TreemapHelpers {
/**
* @param {import('../../../types/internal').ChartStateW} w
* @param {import('../../../types/internal').ChartContext} ctx
*/
constructor(w, ctx) {
this.ctx = ctx;
this.w = w;
}
checkColorRange() {
const w = this.w;
let negRange = false;
const chartOpts = w.config.plotOptions[w.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 w = this.w;
let colorShadePercent = 1;
const shadeIntensity = w.config.plotOptions[chartType].shadeIntensity;
const colorProps = this.determineColor(chartType, i, j);
if (
/** @type {any} */
w.globals.hasNegs || negRange
) {
if (w.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 (w.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),
w.config.fill.opacity
);
} else {
const shadeColor = utils.shadeColor(colorShadePercent, colorProps.color);
color = Utils.hexToRgba(
Utils.isColorHex(shadeColor) ? shadeColor : Utils.rgb2hex(shadeColor),
w.config.fill.opacity
);
}
}
return { color, colorProps };
}
/**
* @param {string} chartType
* @param {number} i
* @param {number} j
*/
determineColor(chartType, i, j) {
const w = this.w;
const val = w.seriesData.series[i][j];
const chartOpts = w.config.plotOptions[chartType];
let seriesNumber = chartOpts.colorScale.inverse ? j : i;
if (chartOpts.distributed && w.config.chart.type === "treemap") {
seriesNumber = j;
}
let color = w.globals.colors[seriesNumber];
let foreColor = null;
let min;
let max;
if (!chartOpts.distributed && chartType === "heatmap") {
min = w.globals.minY;
max = w.globals.maxY;
} else {
const row = w.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 < w.globals.minY ? csMin : w.globals.minY;
max = csMax > w.globals.maxY ? csMax : w.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 w = this.w;
const dataLabelsConfig = w.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, w, i, j);
const offY = resolveDataLabelOffset(dataLabelsConfig.offsetY, w, 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;
}
}
const Filters = _core.__apex_Filters;
const Environment = _core.__apex_Environment_Environment;
function drilldownById(w, id) {
const dd = w.config.drilldown;
const list = dd && Array.isArray(dd.series) ? dd.series : [];
return list.find((s) => s && s.id === id);
}
function toNode(w, 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(w, 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(w, 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(w, c, j, palette, node._key, nextSeen, opts)
);
}
}
}
return node;
}
function buildSeriesRoots(w, series, opts = {}) {
const cfgSeries = (
/** @type {any} */
series || w.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(w, 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(w) {
const stashed = w.globals.treemapRoots;
if (stashed && stashed.length) {
return {
roots: stashed,
maxDepth: w.globals.treemapMaxDepth || 1,
nested: true
};
}
const roots = buildSeriesRoots(w, w.config.series, {
keepDatum: true,
expandDrilldown: false
});
roots.forEach(fillValues);
const { maxDepth } = annotate(roots);
return { roots, maxDepth, nested: false };
}
const DEFAULT_DIVERGING = ["#cf4d3f", "#8f9499", "#26a75b"];
const lerp = (a, b, t) => a + (b - a) * t;
function toHexPair(n) {
const v = Math.max(0, Math.min(255, Math.round(n)));
return v.toString(16).padStart(2, "0");
}
function mixColors(c1, c2, t) {
const a = Utils.parseHex(normalizeHex(c1));
const b = Utils.parseHex(normalizeHex(c2));
if (!a || !b) return c1;
return "#" + toHexPair(lerp(a[0], b[0], t)) + toHexPair(lerp(a[1], b[1], t)) + toHexPair(lerp(a[2], b[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(w, i, j) {
const series = (
/** @type {any} */
w.config.series[i]
);
const datum = series && Array.isArray(series.data) ? series.data[j] : null;
return colorValueOfDatum(w, datum, i, j);
}
function colorValueOfDatum(w, datum, i, j) {
var _a, _b, _c;
if (!datum || typeof datum !== "object") return null;
const accessor = (_c = (_b = (_a = w.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 });
} 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, b) => a.value - b.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(min, midpoint, k / mid), color: colors[k] });
}
for (let k = mid + 1; k < n; k++) {
out.push({
value: lerp(midpoint, max, (k - mid) / (n - 1 - mid)),
color: colors[k]
});
}
return out;
}
return colors.map((c, k) => ({
value: lerp(min, max, k / (n - 1)),
color: c
}));
}
function buildContinuousScale(w) {
var _a, _b, _c;
const cs = (_c = (_b = (_a = w.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} */
w.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 v = colorValueOfDatum(w, data[j], i, j);
if (v == null) continue;
found = true;
if (v < dataMin) dataMin = v;
if (v > dataMax) dataMax = v;
}
}
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 = (v) => {
if (!Number.isFinite(v)) return stops[Math.floor(stops.length / 2)].color;
if (v <= stops[0].value) return stops[0].color;
const last = stops[stops.length - 1];
if (v >= last.value) return last.color;
for (let k = 1; k < stops.length; k++) {
const hi = stops[k];
if (v <= hi.value) {
const lo = stops[k - 1];
const span2 = hi.value - lo.value;
const t = span2 === 0 ? 0 : (v - 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 BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs;
const XHTML = "http://www.w3.org/1999/xhtml";
const BREADCRUMB_HEIGHT = 18;
function breadcrumbConfig(w, localCfg) {
const shared = w.config.drilldown && w.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(w, nav) {
const chrome = (
/** @type {Element[]} */
[".apexcharts-title-text", ".apexcharts-subtitle-text"].map((s) => w.dom.baseEl.querySelector(s)).filter((el) => el !== null)
);
if (!chrome.length) return;
const wrapTop = w.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 clearBreadcrumb(w) {
const elWrap = w.dom.elWrap;
if (!elWrap) return;
const existing = elWrap.querySelector(".apexcharts-breadcrumb");
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
}
function renderBreadcrumb(w, opts) {
if (!Environment.isBrowser()) return null;
const elWrap = w.dom.elWrap;
if (!elWrap) return null;
clearBreadcrumb(w);
const cfg = opts.config || breadcrumbConfig(w);
if (cfg.show === false) return null;
const crumbs = opts.crumbs || [];
if (crumbs.length < 2) return null;
const nav = BrowserAPIs.createElementNS(XHTML, "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, "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, "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, "button")
);
btn.setAttribute("type", "button");
btn.setAttribute("class", "apexcharts-breadcrumb-item");
if (i === 0) {
const arrow = BrowserAPIs.createElementNS(XHTML, "span");
arrow.setAttribute("class", "apexcharts-breadcrumb-arrow");
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "←";
btn.appendChild(arrow);
}
const text = BrowserAPIs.createElementNS(XHTML, "span");
text.setAttribute("class", "apexcharts-breadcrumb-label");
text.textContent = 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 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(w, ctx) {
this.ctx = ctx;
this.w = w;
this.strokeWidth = this.w.config.stroke.width;
this.helpers = new TreemapHelpers(w, 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 w = this.w;
const graphics = new Graphics(this.w, this.ctx);
const fill = new Fill(this.w);
const ret = graphics.group({
class: "apexcharts-treemap"
});
if (w.globals.noData) return ret;
series.forEach((s) => {
s.map((v) => {
return Math.abs(v);
});
});
this.negRange = this.helpers.checkColorRange();
w.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(w);
this.nested = tree.nested;
this.roots = tree.roots;
this.scale = buildContinuousScale(w);
const drawn = tree.roots.length === 1 ? tree.roots[0].children || [] : tree.roots;
this.drawn = drawn;
const parentsCfg = w.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,
w.layout.gridWidth,
w.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, w.config.series.length);
const parentsBySeries = this.showParents ? this._parentsBySeries(layoutRoots, w.config.series.length) : [];
leavesBySeries.forEach((node, i) => {
var _a2;
const elSeries = graphics.group({
class: `apexcharts-series apexcharts-treemap-series`,
seriesName: Utils.escapeString(w.seriesData.seriesNames[i]),
rel: i + 1,
"data:realIndex": i
});
graphics.setupEventDelegation(elSeries, ".apexcharts-treemap-rect");
if (w.config.chart.dropShadow.enabled) {
const shadow = w.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((p) => {
this._drawParent(elSeries, p, i);
});
}
const animCfg = w.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, b) => b.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: w.config.plotOptions.treemap.useFillColorAsStroke ? color : w.globals.stroke.colors[i],
strokeWidth: this.strokeWidth,
fillOpacity: 1
}) : graphics.drawRect(
x1,
y1,
x2 - x1,
y2 - y1,
w.config.plotOptions.treemap.borderRadius,
"#fff",
1,
this.strokeWidth,
w.config.plotOptions.treemap.useFillColorAsStroke ? color : w.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 (w.config.chart.animations.enabled && !w.globals.dataChanged) {
let speed = 1;
if (!w.globals.resized) {
speed = w.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 (w.globals.dataChanged) {
let speed = 1;
if (this.dynamicAnim.enabled && w.globals.shouldAnimate) {
speed = this.dynamicAnim.speed;
if (w.globals.previousPaths[i] && /** @type {Record<string,any>} */
w.globals.previousPaths[i][j] && /** @type {Record<string,any>} */
w.globals.previousPaths[i][j].rect) {
fromRect = /** @type {Record<string,any>} */
w.globals.previousPaths[i][j].rect;
}
this.animateTreemap(elRect, fromRect, toRect, speed);
}
}
let fontSize = this.getFontSize(r);
if (w.config.plotOptions.treemap.dataLabels.format === "truncate") {
fontSize = parseInt(String(w.config.dataLabels.style.fontSize), 10);
}
let dataLabels = null;
if (w.config.dataLabels.enabled && this._labelCanShow(fontSize, x2 - x1, y2 - y1)) {
let formattedText = w.config.dataLabels.formatter(this.labels[i][j], {
value: w.seriesData.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w
});
if (w.config.plotOptions.treemap.dataLabels.format === "truncate") {
formattedText = this.truncateLabels(
String(formattedText),
fontSize,
x1,
y1,
x2,
y2
);
}
if (w.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 (w.config.dataLabels.enabled && dataLabels) {
this.rotateToFitLabel(
dataLabels,
fontSize,
formattedText,
x1,
y1,
x2,
y2
);
}
}
elSeries.add(elRect);
if (dataLabels !== null) {
elSeries.add(dataLabels);
}
});
const seriesTitle = w.config.plotOptions.treemap.seriesTitle;
if (!this.showParents && w.config.series.length > 1 && seriesTitle && seriesTitle.show) {
const sName = (
/** @type {Record<string,any>} */
w.config.series[i].name || ""
);
if (sName && bounds.xMin < Infinity && bounds.yMin < Infinity) {
const {
offsetX,
offsetY,
borderColor,
borderWidth,
borderRadius,
style
} = seriesTitle;
const textColor = style.color || w.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, b) => (a.depth || 0) - (b.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 w = this.w;
if (this.scale) {
const cv = colorValueOf(w, 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(
w.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 w = this.w;
const cfg = this._levelCfg(depth);
const dark = w.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 ? w.config.plotOptions.treemap.borderRadius : cfg.borderRadius,
headerBg: hstyle.background || `rgba(${rgb},${dark ? 0.1 : 0.07})`,
headerColor: hstyle.color || (dark ? "#e8e8e8" : w.config.chart.foreColor),
headerFontSize: hstyle.fontSize || "12px",
headerFontFamily: hstyle.fontFamily || w.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 w = 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
})
);
} 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 w = 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 (w.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 w = this.w;
const z = w.config.plotOptions.treemap.zoom;
if (!z || !z.enabled || !this.showParents) return false;
const dd = w.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 w = this.w;
if (!node || !node.children || !node.children.length) return;
const next = w.globals.treemapFocusKey === node._key ? null : node._key;
w.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} */
w.dom.baseEl.querySelector(
".apexcharts-breadcrumb .apexcharts-breadcrumb-item"
)
);
if (crumb && crumb.focus) {
crumb.focus();
return;
}
const header = (
/** @type {any} */
w.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 w = this.w;
if (!w.dom.elWrap) return;
clearBreadcrumb(w);
if (!this._zoomEnabled()) return;
const chain = this._focusChain();
if (!chain.length) return;
const nav = renderBreadcrumb(w, {
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) => {
w.globals.treemapFocusKey = crumb.data ? crumb.data._key : null;
this._hideParentTooltip();
this.ctx.update();
}
});
if (!nav) return;
this._placeBreadcrumb(nav);
avoidChromeOverlap(w, 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 w = this.w;
const gridTop = w.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 = w.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 w = 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
});
} else {
const marker = this._parentChrome(node.depth || 0).headerBg;
const groupBg = w.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 v = Number(node.value);
return isNaN(v) ? 0 : Math.abs(v);
}
/**
* @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(v) {
var _a, _b, _c, _d, _e;
const w = this.w;
const fmt = ((_b = (_a = w.config.tooltip) == null ? void 0 : _a.y) == null ? void 0 : _b.formatter) || ((_e = (_d = (_c = w.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(v, { seriesIndex: 0, dataPointIndex: 0, w }));
} catch (_) {
}
}
return String(v);
}
/**
* 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 w = 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(w.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 m = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange;
return !!(m && typeof m.hasKeyedMarks === "function" && m.hasKeyedMarks() && typeof m.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
);
}
}
_core__default.use({
treemap: TreemapChart
});
export {
default2 as default
};