UNPKG

apexcharts

Version:

A JavaScript Chart Library

988 lines (987 loc) 34.4 kB
/*! * 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 Graphics = _core.__apex_Graphics; const BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs; const Environment = _core.__apex_Environment_Environment; 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 = (p) => `${p.x} ${p.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 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 Animations = _core.__apex_Animations; 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 buildHierarchy(w, opts = {}) { const cfgSeries = ( /** @type {any} */ w.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(w, d, i, null, "", null, opts) ); } 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 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`; } } 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, b, t) => a + (b - a) * t; class SunburstChart { /** * @param {import('../types/internal').ChartStateW} w * @param {import('../types/internal').ChartContext} ctx */ constructor(w, ctx) { this.ctx = ctx; this.w = w; const cnf = w.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 w = this.w; const graphics = new Graphics(this.w); this._graphics = graphics; const g = graphics.group({ class: "apexcharts-sunburst" }); if (w.globals.noData || !series || !series.length) return g; const gw = w.layout.gridWidth; const gh = w.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 - (!w.config.chart.sparkline.enabled ? w.config.chart.dropShadow.blur : 0); if (this.maxRadius < 5) return g; this._roots = this._buildHierarchy(); if (!this._roots.length) return g; this._roots.forEach((r) => this._fillValues(r)); this._validateStrict(); this._nodesAll = []; const colors = w.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" }); g.add(this._ringsG); g.add(this._labelsG); this._focus = null; this._relayout(this._focus); const anims = w.config.chart.animations; let mode = "none"; if (anims.enabled) { if (w.globals.dataChanged) { if (anims.dynamicAnimation.enabled) mode = "update"; } else if (!w.globals.resized) { mode = "intro"; } } this._applyLayout(mode); this._renderBreadcrumb(); return g; } // ------------------------------------------------------------------ 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 w = this.w; const anims = w.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 w = 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-${w.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 w = this.w; const elWrap = w.dom.elWrap; if (!elWrap) return; const existing = elWrap.querySelector(".apexcharts-breadcrumb"); if (existing && existing.parentNode) existing.parentNode.removeChild(existing); 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 w = 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 = w.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({ sunburst: SunburstChart }); export { default2 as default };