UNPKG

apexcharts

Version:

A JavaScript Chart Library

2,796 lines 110 kB
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b2) => {
  for (var prop in b2 || (b2 = {}))
    if (__hasOwnProp.call(b2, prop))
      __defNormalProp(a, prop, b2[prop]);
  if (__getOwnPropSymbols)
    for (var prop of __getOwnPropSymbols(b2)) {
      if (__propIsEnum.call(b2, prop))
        __defNormalProp(a, prop, b2[prop]);
    }
  return a;
};
var __spreadProps = (a, b2) => __defProps(a, __getOwnPropDescs(b2));
/*!
 * 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 p = 0.05, m = 0.05, g = 1 / 45;
function v(e, t, s, i) {
  const n = { value: e, velocity: 0, target: e, stiffness: t, damping: s };
  return n;
}
function b(e, t) {
  if (t <= 0) return M(e);
  let s = t;
  for (; s > 0; ) {
    const t2 = Math.min(s, g), i = -e.stiffness * (e.value - e.target) - e.damping * e.velocity;
    e.velocity += i * t2, e.value += e.velocity * t2, s -= t2;
  }
  return !!M(e) && (e.value = e.target, e.velocity = 0, true);
}
function w(e, t) {
  e.target = t;
}
function M(e) {
  var _a, _b;
  const t = (_a = e.restVelocity) != null ? _a : p, s = (_b = e.restDisplacement) != null ? _b : m;
  return Math.abs(e.velocity) < t && Math.abs(e.value - e.target) < s;
}
const S = { crisp: [210, 26], gentle: [120, 20], snappy: [320, 30] };
function L(e) {
  var _a;
  return (_a = S[e != null ? e : "crisp"]) != null ? _a : S.crisp;
}
const Graphics = _core.__apex_Graphics;
const LAYOUT_KEY = "__apexcharts_unit_layouts__";
if (!/** @type {any} */
globalThis[LAYOUT_KEY]) {
  globalThis[LAYOUT_KEY] = {};
}
function getLayouts() {
  return (
    /** @type {any} */
    globalThis[LAYOUT_KEY]
  );
}
function getUnitLayout(name) {
  if (!name) return null;
  return getLayouts()[name] || null;
}
const MARK_KEY = "__apexcharts_unit_marks__";
if (!/** @type {any} */
globalThis[MARK_KEY]) {
  globalThis[MARK_KEY] = {};
}
function getMarks() {
  return (
    /** @type {any} */
    globalThis[MARK_KEY]
  );
}
function normalizeUnitMark(def, name) {
  if (typeof def === "string") {
    const d = def.trim();
    if (!d) return null;
    return Object.freeze({
      name: "anonymous",
      path: d,
      viewBox: (
        /** @type {[number,number,number,number]} */
        [0, 0, 100, 100]
      )
    });
  }
  if (!def || typeof def !== "object") return null;
  if (typeof def.path !== "string" || !def.path.trim()) return null;
  const vb = Array.isArray(def.viewBox) && def.viewBox.length === 4 ? def.viewBox.map(Number) : [0, 0, 100, 100];
  if (!vb.every((n) => isFinite(n)) || vb[2] <= 0 || vb[3] <= 0) {
    return null;
  }
  return Object.freeze(__spreadProps(__spreadValues({}, def), {
    name: def.name || "anonymous",
    path: def.path.trim(),
    viewBox: (
      /** @type {[number,number,number,number]} */
      /** @type {any} */
      vb
    ),
    fillRule: def.fillRule === "evenodd" ? "evenodd" : void 0
  }));
}
function getUnitMark(name) {
  if (!name) return null;
  return getMarks()[name] || null;
}
const Utils = _core.__apex_Utils;
const Environment = _core.__apex_Environment_Environment;
const BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs;
const prefersReducedMotion = _core.__apex_Animations_prefersReducedMotion;
function drawOuterLabel(w2, spec) {
  const {
    lines,
    lineHeight,
    anchor,
    elbow,
    labelX,
    labelY,
    side,
    connector,
    style,
    foreColor
  } = spec;
  const graphics = new Graphics(w2);
  const group = graphics.group({
    class: spec.groupClass || "apexcharts-outer-label-group"
  });
  if (connector.show) {
    const d = `M ${anchor.x} ${anchor.y} L ${elbow.x} ${elbow.y} L ${labelX} ${labelY}`;
    const line = graphics.drawPath({
      d,
      stroke: connector.color,
      strokeWidth: connector.width,
      fill: "none",
      strokeLinecap: "round"
    });
    line.node.classList.add(spec.connectorClass || "apexcharts-outer-label-connector");
    group.add(line);
  }
  const textX = side === "right" ? labelX + 4 : labelX - 4;
  const n = lines.length;
  const startY = labelY - (n - 1) * lineHeight / 2;
  const elText = graphics.drawText({
    x: textX,
    y: startY,
    text: n === 1 ? lines[0] : lines,
    textAnchor: side === "right" ? "start" : "end",
    fontSize: style.fontSize,
    fontFamily: style.fontFamily,
    fontWeight: style.fontWeight,
    foreColor,
    dominantBaseline: "central",
    cssClass: spec.textClass || "apexcharts-outer-label"
  });
  if (n > 1) {
    const tspans = elText.node.getElementsByTagName("tspan");
    for (let li = 0; li < tspans.length; li++) {
      tspans[li].setAttribute("x", `${textX}`);
      tspans[li].setAttribute("dy", li === 0 ? "0" : `${lineHeight}`);
    }
  }
  group.add(elText);
  return group;
}
function measureLabelWidth(w2, labels, style = {}) {
  const graphics = new Graphics(w2);
  const fontSize = style.fontSize || "12px";
  const px = parseFloat(fontSize) || 12;
  let max = 0;
  labels.forEach((text) => {
    if (text == null || text === "") return;
    const str = `${text}`;
    const measured = graphics.getTextRects(str, fontSize, style.fontFamily, "").width;
    max = Math.max(max, measured > 0 ? measured : str.length * px * 0.58);
  });
  return max;
}
function spaceOutLabels(items, minGap, maxY, minY) {
  const col = items.slice().sort((a, b2) => a.idealY - b2.idealY);
  col.forEach((l) => {
    l.labelY = l.idealY;
  });
  for (let k = 1; k < col.length; k++) {
    if (col[k].labelY - col[k - 1].labelY < minGap) {
      col[k].labelY = col[k - 1].labelY + minGap;
    }
  }
  const last = col[col.length - 1];
  const overflow = last ? last.labelY - maxY : 0;
  if (overflow > 0) {
    for (let k = col.length - 1; k >= 0; k--) {
      col[k].labelY -= overflow;
      if (k < col.length - 1 && col[k + 1].labelY - col[k].labelY < minGap) {
        col[k].labelY = col[k + 1].labelY - minGap;
      }
    }
  }
  if (minY != null && col.length && col[0].labelY < minY) {
    const shift = minY - col[0].labelY;
    for (let k = 0; k < col.length; k++) {
      col[k].labelY += shift;
    }
  }
}
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
function easeOutCubic(t) {
  return 1 - Math.pow(1 - t, 3);
}
function easeOutBack(s) {
  return (t) => 1 + (s + 1) * Math.pow(t - 1, 3) + s * Math.pow(t - 1, 2);
}
function easeInOutCubic(t) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
const SPRING_REFERENCE_SPEED = 800;
const MAX_FRAME_STEP = 0.25;
const PK_CIRCLE = 0;
const PK_CORNER = 1;
const PK_GLYPH = 2;
function springParams(preset, speed) {
  const [stiffness, damping] = L(
    /** @type {import('apex-commons').SpringPreset|undefined} */
    preset
  );
  const scale = SPRING_REFERENCE_SPEED / Math.max(1, speed);
  return [stiffness * scale * scale, damping * scale];
}
class Unit {
  /**
   * @param {import('../types/internal').ChartStateW} w
   * @param {import('../types/internal').ChartContext} ctx
   */
  constructor(w2, ctx) {
    this.ctx = ctx;
    this.w = w2;
    this._lastDotR = 1;
    this._gridTrack = null;
    this._gridDenom = 1;
    this._scatterAxis = null;
    this._specCache = /* @__PURE__ */ new Map();
    this._markWarned = null;
  }
  /**
   * @param {any[]} series - flat count array (non-axis / pie-shaped data)
   * @returns {any} the chart's root group element
   */
  draw(series) {
    const w2 = this.w;
    const graphics = new Graphics(w2, this.ctx);
    const ret = graphics.group({ class: "apexcharts-unit" });
    if (w2.globals.noData || !Array.isArray(series) || series.length === 0) {
      return ret;
    }
    const opts = w2.config.plotOptions.unit;
    const layout = opts.layout === "packed" ? "packed" : opts.layout === "columns" ? "columns" : opts.layout === "grid" ? "grid" : opts.layout === "scatter" ? "scatter" : opts.layout === "arc" ? "arc" : opts.layout === "custom" ? "custom" : "grouped";
    const transition = opts.transition;
    const flow = transition === "flow";
    const identity = transition === "identity";
    const unitValue = opts.unitValue > 0 ? opts.unitValue : 1;
    let counts = series.map((v2) => {
      const n = Math.abs(Utils.parseNumber(v2)) / unitValue;
      return n > 0 ? Math.max(1, Math.round(n)) : 0;
    });
    counts = this._applyMaxUnits(counts, opts.maxUnits);
    const total = counts.reduce((a, b2) => a + b2, 0);
    const clusters = layout === "packed" ? this._layoutPacked(counts, opts) : layout === "columns" ? this._layoutColumns(counts, opts) : layout === "grid" ? this._layoutGrid(counts, opts) : layout === "scatter" ? this._layoutScatter(opts) : layout === "arc" ? this._layoutArc(counts, opts) : layout === "custom" ? this._layoutCustom(counts, opts) : this._layoutGrouped(counts, opts);
    const gridSplit = layout === "grid" && !!(opts.grid && opts.grid.split);
    if (gridSplit) this._drawGridTrack(ret, graphics, opts);
    if (layout === "scatter") this._drawScatterAxes(ret, graphics);
    const dotR = this._lastDotR;
    const animate = this._shouldAnimate();
    const morph = this.ctx && this.ctx.morphTypeChange;
    const morphActive = animate && !!morph && typeof morph.isActive === "function" && morph.isActive() && typeof morph.getInitialCenterFor === "function";
    const perRowBurst = morphActive && typeof morph.getInitialSlotFor === "function";
    const pieceTakeover = morphActive && typeof morph.usesPieceTakeover === "function" && morph.usesPieceTakeover();
    const prev = animate && !morphActive && this.ctx ? this.ctx._unitPrevDots : null;
    const nextPrev = /* @__PURE__ */ new Map();
    const animDots = [];
    const unitData = w2.seriesData.unitData || [];
    const sizeStats = this._bubbleStats(unitData, opts, dotR);
    let gIndex = 0;
    clusters.forEach((cluster) => {
      const color = w2.globals.colors[cluster.i] || w2.globals.colors[0] || "#008FFB";
      const elSeries = graphics.group({
        class: "apexcharts-series",
        seriesName: Utils.escapeString(
          w2.seriesData.seriesNames[cluster.i] || `series-${cluster.i + 1}`
        ),
        rel: cluster.i + 1,
        "data:realIndex": cluster.i
      });
      const burst = morphActive && !perRowBurst ? morph.getInitialCenterFor(cluster.i) : null;
      const burstCount = cluster.dots.length;
      const catData = unitData[cluster.i];
      cluster.dots.forEach((d, jj) => {
        const j = d.j != null ? d.j : jj;
        const datum = catData ? catData[j] : void 0;
        const dotFill = datum && typeof datum === "object" && datum.fillColor ? datum.fillColor : color;
        const rj = d.r != null ? d.r : sizeStats ? this._radiusForValue(this._unitValueOf(datum), sizeStats) : dotR;
        const spec = this._markSpecFor(opts, datum, cluster.i, rj);
        const el = this._drawDot(graphics, opts, rj, dotFill, cluster.i, j, spec);
        elSeries.add(el);
        let key;
        if (identity) {
          const id = datum && typeof datum === "object" ? datum.id != null ? datum.id : datum.name : void 0;
          key = id != null ? `id:${id}` : `g:${gIndex}`;
        } else if (flow) {
          key = String(gIndex);
        } else if (d.slot != null) {
          key = `slot:${d.slot}`;
        } else {
          key = `${cluster.i}:${j}`;
        }
        gIndex++;
        nextPrev.set(key, { x: d.x, y: d.y, fill: dotFill, r: rj, spec });
        if (pieceTakeover) {
          this._place(el.node, spec, d.x, d.y);
          el.node.setAttribute("opacity", "0");
          el.node.setAttribute("data-piece-hidden", "1");
        } else if (animate) {
          const from = prev && prev.get(key);
          const anchor = from || (perRowBurst ? morph.getInitialSlotFor(cluster.i, j, burstCount) : burst);
          const enter = opts.gather && opts.gather.enter || "burst";
          const inPlace = gridSplit || enter === "fade" || enter === "rise";
          const cx0 = anchor ? anchor.x : inPlace ? d.x : cluster.cx;
          const cy0 = anchor ? anchor.y : enter === "rise" && !gridSplit ? d.y + 14 : inPlace ? d.y : cluster.cy;
          el.node.style.opacity = anchor ? "1" : "0";
          this._place(el.node, spec, cx0, cy0);
          animDots.push({
            node: el.node,
            spec,
            x: d.x,
            y: d.y,
            cx0,
            cy0,
            // Carries live spring state (position AND velocity) forward when
            // this render interrupted one still in flight.
            key,
            // Radius tween: an identity-kept dot grows/shrinks from its previous
            // size to its new one (e.g. bubble sizing turning on) instead of
            // snapping. Enters/uniform updates keep r0 === r1 (no-op).
            r0: from && from.r != null ? from.r : rj,
            r1: rj,
            // Colour tween: a dot that flows into a differently coloured group
            // recolours as it travels rather than snapping at the first frame.
            fill0: from ? from.fill : dotFill,
            fill1: dotFill,
            delay: 0,
            // assigned below (staggered by global order)
            isEnter: !anchor
          });
        } else {
          this._place(el.node, spec, d.x, d.y);
        }
      });
      if ((layout === "grouped" || layout === "columns" || gridSplit) && opts.clusterLabels && opts.clusterLabels.show && counts[cluster.i] > 0) {
        const labelTotal = gridSplit ? this._gridDenom : total;
        this._drawClusterLabel(elSeries, cluster, counts[cluster.i], labelTotal, opts, color);
      }
      ret.add(elSeries);
    });
    if (this._outerLabelsOn(opts)) {
      this._drawOuterLabels(ret, clusters, counts, total, opts, animate && !prev);
    }
    if (prev) {
      const exits = this._collectExits(prev, nextPrev, opts);
      if (exits.length) {
        const exitGroup = graphics.group({ class: "apexcharts-unit-exits" });
        ret.add(exitGroup);
        this._runExits(exitGroup, exits, opts);
      }
    }
    if (this.ctx) this.ctx._unitPrevDots = nextPrev;
    if (this.ctx && (!animate || morphActive)) this.ctx._unitSprings = null;
    if (animate && animDots.length) {
      this._runGather(animDots);
    } else {
      w2.globals.animationEnded = true;
    }
    return ret;
  }
  /**
   * Cap total dots to `maxUnits`, scaling every category down proportionally
   * (a non-zero category keeps at least one dot). Warns once when it clips.
   * @param {number[]} counts
   * @param {number} maxUnits
   * @returns {number[]}
   */
  _applyMaxUnits(counts, maxUnits) {
    const total = counts.reduce((a, b2) => a + b2, 0);
    if (!maxUnits || maxUnits <= 0 || total <= maxUnits) return counts;
    const scale = maxUnits / total;
    console.warn(
      `[ApexCharts] unit chart: ${total} dots exceeds maxUnits (${maxUnits}); counts were scaled down proportionally. Raise plotOptions.unit.maxUnits or use plotOptions.unit.unitValue to represent more units per dot.`
    );
    return counts.map((c) => c > 0 ? Math.max(1, Math.round(c * scale)) : 0);
  }
  /**
   * `layout: 'custom'`. Positions come from a caller-supplied provider rather
   * than from a generator in this file.
   *
   * The provider is the whole extension point: `(objects, rect) => [{id, x, y,
   * r?}]`. Everything downstream is unchanged, which is the point - the engine
   * already tweens position, radius and colour, and already keeps a mark's
   * identity across a relayout, so an arbitrary new arrangement needs no new
   * transition code. A silhouette, a hex grid, a timeline, or a projection
   * handed over by ApexMaps are all just this function.
   *
   * Marks the provider omits are dropped, so they animate out through the
   * existing exit path. Ids matching no mark are ignored.
   *
   * @param {number[]} counts
   * @param {any} opts
   */
  _layoutCustom(counts, opts) {
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const total = counts.reduce((a, b2) => a + b2, 0);
    const provider = this._resolveLayoutProvider(opts);
    if (!provider) {
      console.warn(
        `[ApexCharts] unit chart: layout 'custom' needs plotOptions.unit.positions (a function, or the name of a layout registered with ApexCharts.registerUnitLayout). Falling back to 'grouped'.`
      );
      return this._layoutGrouped(counts, opts);
    }
    const gutter = this._outerLabelsOn(opts) ? this._outerLabelGutter(counts, opts) : 0;
    this._lastOuterGutter = gutter;
    const rect = {
      x: gutter,
      y: 0,
      width: Math.max(1, gw - gutter * 2),
      height: gh
    };
    const availR = Math.sqrt(rect.width * rect.height / Math.PI);
    const step = this._resolveStep(opts, availR, total);
    this._lastDotR = this._dotRadiusFromStep(step, opts);
    const dotR = this._lastDotR;
    const objects = this._layoutObjects(counts, dotR);
    let placed;
    try {
      placed = provider(objects, rect);
    } catch (e) {
      console.warn(
        "[ApexCharts] unit chart: the layout provider threw; falling back to 'grouped'.",
        e
      );
      return this._layoutGrouped(counts, opts);
    }
    if (!Array.isArray(placed)) {
      console.warn(
        "[ApexCharts] unit chart: the layout provider must return an array of {id, x, y}; falling back to 'grouped'."
      );
      return this._layoutGrouped(counts, opts);
    }
    const byId = /* @__PURE__ */ new Map();
    placed.forEach((p2) => {
      if (!p2 || !isFinite(p2.x) || !isFinite(p2.y)) return;
      byId.set(String(p2.id), {
        x: p2.x,
        y: p2.y,
        r: typeof p2.r === "number" && p2.r > 0 ? p2.r : void 0
      });
    });
    const clusters = counts.map((_, i) => ({
      i,
      cx: gw / 2,
      cy: gh / 2,
      outerR: dotR,
      dots: []
    }));
    objects.forEach((o) => {
      const hit = byId.get(o.id);
      if (!hit) return;
      clusters[o.seriesIndex].dots.push({
        x: hit.x,
        y: hit.y,
        r: hit.r,
        j: o.dataPointIndex
      });
    });
    clusters.forEach((c) => {
      if (!c.dots.length) return;
      let sx = 0;
      let sy = 0;
      c.dots.forEach((d) => {
        sx += d.x;
        sy += d.y;
      });
      c.cx = sx / c.dots.length;
      c.cy = sy / c.dots.length;
      let far = 0;
      c.dots.forEach((d) => {
        far = Math.max(far, Math.hypot(d.x - c.cx, d.y - c.cy));
      });
      c.outerR = far + dotR;
    });
    return clusters;
  }
  /**
   * One entry per mark, in global draw order, for a layout provider.
   *
   * `id` is the datum's own id/name where the per-unit object form supplies
   * one, so a provider can address a specific unit ("Texas", "employee 41")
   * rather than a positional slot. It falls back to `"<category>:<index>"`.
   *
   * @param {number[]} counts
   * @param {number} dotR the radius the engine would use, so a provider that
   *   packs by size does not have to rediscover it
   * @returns {{id:string,index:number,seriesIndex:number,dataPointIndex:number,label:string,value:number|undefined,datum:any,r:number}[]}
   */
  _layoutObjects(counts, dotR) {
    const w2 = this.w;
    const unitData = w2.seriesData.unitData || [];
    const names = w2.seriesData.seriesNames || [];
    const objects = [];
    let index = 0;
    counts.forEach((n, i) => {
      var _a;
      const catData = unitData[i];
      for (let j = 0; j < n; j++) {
        const datum = catData ? catData[j] : void 0;
        const id = datum && typeof datum === "object" && (datum.id != null || datum.name != null) ? String(datum.id != null ? datum.id : datum.name) : `${i}:${j}`;
        objects.push({
          id,
          index,
          seriesIndex: i,
          dataPointIndex: j,
          label: names[i],
          // Normalised at the boundary: internally "no value" is null, but the
          // public object shape uses an absent property.
          value: (_a = this._unitValueOf(datum)) != null ? _a : void 0,
          datum,
          r: dotR
        });
        index++;
      }
    });
    return objects;
  }
  /**
   * Resolve `plotOptions.unit.positions` to a provider function: either the
   * function itself, or the name of one registered through
   * `ApexCharts.registerUnitLayout`.
   * @param {any} opts
   * @returns {Function|null}
   */
  _resolveLayoutProvider(opts) {
    const positions = opts.positions;
    if (typeof positions === "function") return positions;
    if (typeof positions === "string" && positions) {
      const found = getUnitLayout(positions);
      if (found) return found;
      console.warn(
        `[ApexCharts] unit chart: no layout named "${positions}" is registered. Register one with ApexCharts.registerUnitLayout("${positions}", fn).`
      );
    }
    return null;
  }
  /**
   * Lay out each category as its own cluster in a horizontal row. All clusters
   * share one dot radius (so dot size is comparable across clusters); the blob
   * radius encodes the count.
   * @param {number[]} counts
   * @param {any} opts
   */
  _layoutGrouped(counts, opts) {
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const labelSpace = opts.clusterLabels && opts.clusterLabels.show ? 30 : 6;
    const visible = counts.map((_, i) => i).filter((i) => counts[i] > 0);
    const Kv = Math.max(1, visible.length);
    const slotOf = new Array(counts.length).fill(-1);
    visible.forEach((i, s) => slotOf[i] = s);
    const cellW = gw / Kv;
    const availH = gh - labelSpace;
    const maxCount = Math.max(1, ...counts);
    const pad = Math.min(cellW, availH) * 0.08;
    const availR = Math.max(4, Math.min(cellW, availH) / 2 - pad);
    const step = this._resolveStep(opts, availR, maxCount);
    this._lastDotR = this._dotRadiusFromStep(step, opts);
    const dotR = this._lastDotR;
    const cy = labelSpace + availH / 2;
    const outerRs = counts.map((n) => step * Math.sqrt(Math.max(1, n)) + dotR);
    const cellCentre = (i) => slotOf[i] >= 0 ? cellW * (slotOf[i] + 0.5) : gw / 2;
    let centers = counts.map((_, i) => cellCentre(i));
    const visOuter = visible.map((i) => outerRs[i]);
    let overlap = false;
    for (let s = 1; s < Kv; s++) {
      if (centers[visible[s]] - centers[visible[s - 1]] < visOuter[s] + visOuter[s - 1]) {
        overlap = true;
        break;
      }
    }
    if (overlap) {
      const gap = Math.max(2 * dotR, 8);
      const totalW = visOuter.reduce((a, r) => a + 2 * r, 0) + gap * (Kv - 1);
      let visCenters;
      if (totalW <= gw) {
        let x = (gw - totalW) / 2;
        visCenters = visOuter.map((r) => {
          const c = x + r;
          x += 2 * r + gap;
          return c;
        });
      } else if (Kv === 1) {
        visCenters = [gw / 2];
      } else {
        const lo = visOuter[0];
        const hi = gw - visOuter[Kv - 1];
        visCenters = visOuter.map((_, s) => lo + (hi - lo) * s / (Kv - 1));
      }
      centers = counts.map(
        (_, i) => slotOf[i] >= 0 ? visCenters[slotOf[i]] : gw / 2
      );
    }
    return counts.map((n, i) => ({
      i,
      cx: centers[i],
      cy,
      outerR: outerRs[i],
      dots: this._spiral(centers[i], cy, n, step, 0)
    }));
  }
  /**
   * Lay out all categories into ONE packed blob. Dots are assigned spiral
   * indices in category order (smallest-first when sortByGroup), so the
   * minority group nests in the centre.
   * @param {number[]} counts
   * @param {any} opts
   */
  _layoutPacked(counts, opts) {
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const labelSpace = 6;
    const total = Math.max(1, counts.reduce((a, b2) => a + b2, 0));
    const availR = Math.max(
      4,
      Math.min(gw, gh - labelSpace) / 2 - Math.min(gw, gh) * 0.06
    );
    const step = this._resolveStep(opts, availR, total);
    this._lastDotR = this._dotRadiusFromStep(step, opts);
    const cx = gw / 2;
    const cy = labelSpace + (gh - labelSpace) / 2;
    const order = counts.map((_, i) => i);
    if (opts.sortByGroup !== false) {
      order.sort((a, b2) => counts[a] - counts[b2]);
    }
    const clusters = counts.map((_, i) => ({
      i,
      cx,
      cy,
      outerR: step * Math.sqrt(total) + this._lastDotR,
      /** @type {{x:number,y:number,slot?:number}[]} */
      dots: []
    }));
    let gi = 0;
    order.forEach((catI) => {
      for (let j = 0; j < counts[catI]; j++) {
        const r = step * Math.sqrt(gi + 0.5);
        const theta = gi * GOLDEN_ANGLE;
        clusters[catI].dots.push({
          x: cx + r * Math.cos(theta),
          y: cy + r * Math.sin(theta),
          slot: gi
        });
        gi++;
      }
    });
    return clusters;
  }
  /**
   * Lay out all marks as a PARLIAMENT / hemicycle: seats in concentric arced
   * rows across an annulus, filled in category (party) order so each category
   * forms a contiguous angular wedge (the classic seating chart). `arc` controls
   * the sweep (`startAngle`/`endAngle`, radialBar convention: 0 = top, clockwise;
   * default a top semicircle), the donut hole (`innerRadiusRatio`) and the row
   * count (`rows`, or 'auto'). Like `packed` this is ONE shared shape coloured by
   * category, so seats key by physical slot: a seat-count change recolours the
   * party boundary in place and only the rim adds / removes seats.
   * @param {number[]} counts
   * @param {any} opts
   */
  _layoutArc(counts, opts) {
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const total = Math.max(1, counts.reduce((a, b3) => a + b3, 0));
    const acfg = opts.arc || {};
    const startDeg = typeof acfg.startAngle === "number" ? acfg.startAngle : -90;
    const endDeg = typeof acfg.endAngle === "number" ? acfg.endAngle : 90;
    const a0 = startDeg * Math.PI / 180;
    const a1 = endDeg * Math.PI / 180;
    const span = a1 - a0 || Math.PI;
    const innerRatio = Math.max(
      0,
      Math.min(0.95, typeof acfg.innerRadiusRatio === "number" ? acfg.innerRadiusRatio : 0.4)
    );
    const ux = (a) => Math.sin(a);
    const uy = (a) => -Math.cos(a);
    const b2 = this._arcBounds(a0, a1);
    const pad = Math.min(gw, gh) * 0.04;
    const boxW = Math.max(1e-6, b2.maxX - b2.minX);
    const boxH = Math.max(1e-6, b2.maxY - b2.minY);
    const r1 = Math.max(4, Math.min((gw - 2 * pad) / boxW, (gh - 2 * pad) / boxH));
    const r0 = r1 * innerRatio;
    const cx = gw / 2 - (b2.minX + b2.maxX) / 2 * r1;
    const cy = gh / 2 - (b2.minY + b2.maxY) / 2 * r1;
    const alloc = this._arcAllocate(total, r0, r1, span, opts);
    this._lastDotR = alloc.dotR;
    const seats = [];
    for (let r = 0; r < alloc.R; r++) {
      const rho = alloc.radii[r];
      const n = alloc.seatsPerRow[r];
      for (let k = 0; k < n; k++) {
        const a = n === 1 ? (a0 + a1) / 2 : a0 + span * (k + 0.5) / n;
        seats.push({ a, x: cx + rho * ux(a), y: cy + rho * uy(a) });
      }
    }
    seats.sort((s1, s2) => s1.a - s2.a);
    const clusters = counts.map((_, i) => ({
      i,
      cx,
      cy,
      outerR: r1,
      /** @type {{x:number,y:number,slot?:number}[]} */
      dots: []
    }));
    let ci = 0;
    let used = 0;
    seats.forEach((s, slot) => {
      while (ci < counts.length && used >= counts[ci]) {
        ci++;
        used = 0;
      }
      if (ci >= counts.length) return;
      clusters[ci].dots.push({ x: s.x, y: s.y, slot });
      used++;
    });
    return clusters;
  }
  /**
   * Bounding box of the outer arc (radius 1) over [a0, a1], including the centre
   * and every cardinal angle (multiple of 90deg) inside the range, so a
   * semicircle / full circle / arbitrary sweep is all bounded correctly.
   * @param {number} a0 @param {number} a1
   * @returns {{minX:number,maxX:number,minY:number,maxY:number}}
   */
  _arcBounds(a0, a1) {
    const ux = (a) => Math.sin(a);
    const uy = (a) => -Math.cos(a);
    const lo = Math.min(a0, a1);
    const hi = Math.max(a0, a1);
    const xs = [0, ux(a0), ux(a1)];
    const ys = [0, uy(a0), uy(a1)];
    const q = Math.PI / 2;
    for (let k = Math.ceil(lo / q); k * q <= hi; k++) {
      xs.push(ux(k * q));
      ys.push(uy(k * q));
    }
    return {
      minX: Math.min(...xs),
      maxX: Math.max(...xs),
      minY: Math.min(...ys),
      maxY: Math.max(...ys)
    };
  }
  /**
   * Allocate `total` seats across concentric rows of the annulus [r0, r1] sweeping
   * `span` radians: seats per row are proportional to the row radius (a longer arc
   * holds more), summed EXACTLY to total by largest remainder. Row count is
   * `arc.rows` if given, else derived from a fixed dot size, else auto-searched to
   * maximise the dot radius (the largest dots that still pack without overlap,
   * mirroring `size:'auto'` elsewhere).
   * @param {number} total @param {number} r0 @param {number} r1 @param {number} span @param {any} opts
   * @returns {{R:number, radii:number[], seatsPerRow:number[], dotR:number}}
   */
  _arcAllocate(total, r0, r1, span, opts) {
    const spacing = opts.spacing > 0 ? opts.spacing : 1.05;
    const absSpan = Math.abs(span) || Math.PI;
    const fixed = this._fixedRadius(opts);
    const evalR = (R) => {
      R = Math.max(1, Math.round(R));
      const radii = [];
      for (let r = 0; r < R; r++) {
        radii.push(R === 1 ? (r0 + r1) / 2 : r0 + (r1 - r0) * (r / (R - 1)));
      }
      const weightSum = radii.reduce((a, x) => a + x, 0) || 1;
      const raw = radii.map((rho) => total * rho / weightSum);
      const seatsPerRow = raw.map((x) => Math.floor(x));
      let left = total - seatsPerRow.reduce((a, x) => a + x, 0);
      raw.map((x, idx) => ({ idx, frac: x - Math.floor(x) })).sort((p2, qq) => qq.frac - p2.frac).forEach((o) => {
        if (left > 0) {
          seatsPerRow[o.idx]++;
          left--;
        }
      });
      while (left > 0) {
        seatsPerRow[R - 1]++;
        left--;
      }
      const radialPitch = R === 1 ? r1 - r0 || r1 : (r1 - r0) / (R - 1);
      let minArcPitch = Infinity;
      for (let r = 0; r < R; r++) {
        const n = seatsPerRow[r];
        if (n <= 0) continue;
        const arcPitch = radii[r] * absSpan / n;
        if (arcPitch < minArcPitch) minArcPitch = arcPitch;
      }
      const pitch = Math.min(radialPitch, minArcPitch);
      return { R, radii, seatsPerRow, dotR: Math.max(1, pitch / (2 * spacing)) };
    };
    const arcRows = opts.arc && opts.arc.rows;
    let res;
    if (typeof arcRows === "number" && arcRows >= 1) {
      res = evalR(arcRows);
    } else if (fixed) {
      const pitch = 2 * fixed * spacing;
      res = evalR((r1 - r0) / pitch + 1);
    } else {
      const maxR = Math.max(1, Math.min(40, Math.ceil(Math.sqrt(total)) + 6));
      res = evalR(1);
      for (let R = 2; R <= maxR; R++) {
        const cand = evalR(R);
        if (cand.dotR > res.dotR) res = cand;
      }
    }
    if (fixed) res.dotR = fixed;
    return res;
  }
  /**
   * Lay out each category as a vertical BAR built from stacked dots (a unit /
   * waffle column). Every bar shares one dot size and one width (the same
   * number of dot columns); the bar's HEIGHT encodes its count. Dots fill each
   * bar bottom-up, row by row. This is the "dot bar" state the circle layouts
   * morph into: with `transition:'flow'` the dots glide straight from their
   * circle slots into these bar slots (see the storyboard sample).
   * @param {number[]} counts
   * @param {any} opts
   */
  _layoutColumns(counts, opts) {
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const labelsOn = !!(opts.clusterLabels && opts.clusterLabels.show);
    const labelsBelow = labelsOn && opts.clusterLabels.position === "bottom";
    const topPad = labelsOn && !labelsBelow ? 30 : 6;
    const visible = counts.map((_, i) => i).filter((i) => counts[i] > 0);
    const Kv = Math.max(1, visible.length);
    const slotOf = new Array(counts.length).fill(-1);
    visible.forEach((i, s) => slotOf[i] = s);
    const cellW = gw / Kv;
    const barW = cellW * 0.62;
    const bottomPad = Math.max(8, gh * 0.04) + (labelsBelow ? 30 : 0);
    const availH = Math.max(4, gh - topPad - bottomPad);
    const maxCount = Math.max(1, ...counts);
    const spacing = opts.spacing > 0 ? opts.spacing : 1;
    const colSize = opts.columns ? opts.columns.size : void 0;
    let fixed;
    if (opts.shape !== "image" && colSize === "auto") {
      fixed = null;
    } else if (opts.shape !== "image" && typeof colSize === "number" && colSize > 0) {
      fixed = colSize;
    } else {
      fixed = this._fixedRadius(opts);
    }
    let cols = 1;
    let pitch = 0;
    if (fixed) {
      pitch = 2 * fixed * spacing;
      this._lastDotR = fixed;
      const rowsCap = Math.max(1, Math.floor(availH / pitch));
      const maxColsByWidth = Math.max(1, Math.floor(barW / pitch));
      cols = Math.max(1, Math.min(maxColsByWidth, Math.ceil(maxCount / rowsCap)));
    } else {
      let best = 0;
      const maxCols = Math.max(1, Math.min(40, Math.round(barW / 4)));
      for (let c = 1; c <= maxCols; c++) {
        const rows = Math.ceil(maxCount / c);
        const d = Math.min(barW / c, availH / rows);
        if (d > best) {
          best = d;
          cols = c;
        }
      }
      pitch = best;
      this._lastDotR = Math.max(1, pitch / (2 * spacing));
    }
    const r = this._lastDotR;
    const maxRows = Math.ceil(maxCount / cols);
    const tallestBarH = Math.min(availH, maxRows * pitch);
    const bottom = topPad + (availH + tallestBarH) / 2;
    return counts.map((n, i) => {
      const cx = slotOf[i] >= 0 ? cellW * (slotOf[i] + 0.5) : gw / 2;
      const rows = Math.ceil(Math.max(1, n) / cols);
      const barH = rows * pitch;
      const left = cx - cols * pitch / 2 + pitch / 2;
      const dots = [];
      for (let j = 0; j < n; j++) {
        const rowIdx = Math.floor(j / cols);
        const colIdx = j % cols;
        dots.push({
          x: left + colIdx * pitch,
          y: bottom - r - rowIdx * pitch
        });
      }
      return {
        i,
        cx,
        cy: bottom - barH / 2,
        outerR: barH / 2,
        // Flag read by _drawClusterLabel: a bar takes a straight label (above
        // or below per clusterLabels.position), never a curved arc.
        flat: true,
        dots
      };
    });
  }
  /**
   * Lay out ALL categories into ONE regular lattice - a waffle / grid. Dots take
   * sequential slots in DECLARED category order and fill row-major, `columns`
   * wide, so each category owns a contiguous band of cells: a part-to-whole
   * square "pie". `grid.total` (optional) re-allocates the cells to a fixed
   * budget (e.g. 100) by largest remainder, so the grid reads as exact
   * percentages regardless of the raw totals; without it there is one cell per
   * unit (respecting unitValue / maxUnits). `grid.fillFrom` picks the first row.
   * The category bands follow the legend order (no smallest-first sort), and
   * each physical slot is keyed so a proportion change recolours boundary cells
   * in place rather than reshuffling the whole grid.
   * @param {number[]} counts
   * @param {any} opts
   */
  _layoutGrid(counts, opts) {
    if (opts.grid && opts.grid.split) return this._layoutGridSplit(counts, opts);
    this._gridTrack = null;
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const gcfg = opts.grid || {};
    const cols = Math.max(1, Math.round(gcfg.columns > 0 ? gcfg.columns : 10));
    const fillFrom = gcfg.fillFrom === "top" ? "top" : "bottom";
    const cells = gcfg.total > 0 ? this._largestRemainder(counts, Math.round(gcfg.total)) : counts.slice();
    const totalCells = cells.reduce((a, b2) => a + b2, 0);
    const rows = Math.max(1, Math.ceil(Math.max(1, totalCells) / cols));
    const labelSpace = 6;
    const spacing = opts.spacing > 0 ? opts.spacing : 1;
    const availW = Math.max(4, gw);
    const availH = Math.max(4, gh - labelSpace);
    const fixed = this._fixedRadius(opts);
    let pitch = 0;
    if (fixed) {
      pitch = 2 * fixed * spacing;
      this._lastDotR = fixed;
    } else {
      pitch = Math.min(availW / cols, availH / rows);
      this._lastDotR = Math.max(1, pitch / (2 * spacing));
    }
    const blockW = cols * pitch;
    const blockH = rows * pitch;
    const originX = (gw - blockW) / 2 + pitch / 2;
    const topY = labelSpace + (availH - blockH) / 2;
    const rowY = (rowIdx) => fillFrom === "bottom" ? topY + blockH - pitch / 2 - rowIdx * pitch : topY + pitch / 2 + rowIdx * pitch;
    const clusters = counts.map((_, i) => ({
      i,
      cx: gw / 2,
      cy: labelSpace + availH / 2,
      outerR: Math.max(blockW, blockH) / 2,
      /** @type {{x:number,y:number,slot?:number}[]} */
      dots: []
    }));
    let k = 0;
    for (let ci = 0; ci < cells.length; ci++) {
      for (let j = 0; j < cells[ci]; j++) {
        const col = k % cols;
        const rowIdx = Math.floor(k / cols);
        clusters[ci].dots.push({
          x: originX + col * pitch,
          y: rowY(rowIdx),
          slot: k
        });
        k++;
      }
    }
    return clusters;
  }
  /**
   * Small-multiple ("trellis") waffles: ONE mini-waffle per category, laid out
   * in a near-square grid of tiles. Each tile has `grid.total` cells (default
   * 100 -> a 10x10 tile) and fills a fraction of them equal to the category's
   * value over a denominator (`grid.max`, else the largest count so the leader
   * fills its tile and every other tile stays proportionally full - no empty
   * tiles for arbitrary data). The unfilled cells are drawn as a faint TRACK
   * backdrop (see _drawGridTrack) so each tile reads as a part-to-whole "of N".
   * Only VISIBLE (non-zero) categories claim a tile, so a legend hide drops the
   * tile and the rest re-flow. Each filled cell is keyed by a physical
   * `tile*cells + localCell` slot, so a value change grows/shrinks a tile's fill
   * in place instead of reshuffling.
   * @param {number[]} counts @param {any} opts
   */
  _layoutGridSplit(counts, opts) {
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const gcfg = opts.grid || {};
    const cols = Math.max(1, Math.round(gcfg.columns > 0 ? gcfg.columns : 10));
    const fillFrom = gcfg.fillFrom === "top" ? "top" : "bottom";
    const cellsPerTile = Math.max(1, Math.round(gcfg.total > 0 ? gcfg.total : 100));
    const rowsPerTile = Math.max(1, Math.ceil(cellsPerTile / cols));
    const visible = counts.map((_, i) => i).filter((i) => counts[i] > 0);
    const K = Math.max(1, visible.length);
    const denom = gcfg.max > 0 ? gcfg.max : Math.max(1, ...counts);
    const tileCols = Math.max(
      1,
      Math.round(gcfg.tileColumns > 0 ? gcfg.tileColumns : Math.ceil(Math.sqrt(K)))
    );
    const tileRows = Math.max(1, Math.ceil(K / tileCols));
    const labelsOn = !(opts.clusterLabels && opts.clusterLabels.show === false);
    const labelsBelow = labelsOn && opts.clusterLabels && opts.clusterLabels.position === "bottom";
    const topBand = labelsOn && !labelsBelow ? 22 : 4;
    const botBand = labelsOn && labelsBelow ? 22 : 4;
    const tileW = gw / tileCols;
    const tileH = gh / tileRows;
    const availTileW = Math.max(4, tileW * 0.86);
    const availTileH = Math.max(4, tileH - topBand - botBand);
    const spacing = opts.spacing > 0 ? opts.spacing : 1;
    const fixed = this._fixedRadius(opts);
    let pitch = 0;
    if (fixed) {
      pitch = 2 * fixed * spacing;
      this._lastDotR = fixed;
    } else {
      pitch = Math.min(availTileW / cols, availTileH / rowsPerTile);
      this._lastDotR = Math.max(1, pitch / (2 * spacing));
    }
    const blockW = cols * pitch;
    const blockH = rowsPerTile * pitch;
    const rowY = (topY, rowIdx) => fillFrom === "bottom" ? topY + blockH - pitch / 2 - rowIdx * pitch : topY + pitch / 2 + rowIdx * pitch;
    const track = [];
    const clusters = [];
    visible.forEach((ci, t) => {
      const tc = t % tileCols;
      const tr = Math.floor(t / tileCols);
      const tileX = tc * tileW;
      const tileYtop = tr * tileH;
      const originX = tileX + (tileW - blockW) / 2 + pitch / 2;
      const topY = tileYtop + topBand + (availTileH - blockH) / 2;
      const cellXY = (k) => ({
        x: originX + k % cols * pitch,
        y: rowY(topY, Math.floor(k / cols))
      });
      for (let k = 0; k < cellsPerTile; k++) track.push(cellXY(k));
      const filled = Math.max(
        0,
        Math.min(cellsPerTile, Math.round(counts[ci] / denom * cellsPerTile))
      );
      const dots = [];
      for (let k = 0; k < filled; k++) {
        const p2 = cellXY(k);
        dots.push({ x: p2.x, y: p2.y, slot: t * cellsPerTile + k });
      }
      clusters.push({
        i: ci,
        cx: tileX + tileW / 2,
        cy: topY + blockH / 2,
        outerR: blockH / 2,
        // Straight per-tile label (never a curved arc), placed by position.
        flat: true,
        split: true,
        dots
      });
    });
    this._gridDenom = denom;
    this._gridTrack = { cells: track };
    return clusters;
  }
  /**
   * Draw the faint "track" backdrop for the small-multiple grid: every cell of
   * every tile's full lattice, so the filled (coloured) cells drawn on top read
   * as a fraction of the whole. Static (redrawn each render, never animated);
   * painted BEHIND the series groups. `grid.trackColor` overrides the default
   * theme-neutral grey.
   * @param {any} ret @param {Graphics} graphics @param {any} opts
   */
  _drawGridTrack(ret, graphics, opts) {
    const track = this._gridTrack;
    if (!track || !track.cells || !track.cells.length) return;
    const r = this._lastDotR;
    const gcfg = opts.grid || {};
    const trackColor = gcfg.trackColor || "rgba(128,128,128,0.14)";
    const g2 = graphics.group({ class: "apexcharts-unit-track" });
    track.cells.forEach((c) => {
      let el;
      if (opts.shape === "square") {
        const side = r * 2;
        el = graphics.drawRect(0, 0, side, side, opts.borderRadius || 0, trackColor, 1, 0, "none");
        el.node.setAttribute("fill", trackColor);
        el.node.setAttribute("x", String(c.x - r));
        el.node.setAttribute("y", String(c.y - r));
      } else {
        el = graphics.drawCircle(r, { fill: trackColor, "stroke-width": 0, stroke: "none" });
        el.node.setAttribute("fill", trackColor);
        el.node.setAttribute("cx", String(c.x));
        el.node.setAttribute("cy", String(c.y));
      }
      el.node.classList.add("apexcharts-unit-track-cell");
      g2.add(el);
    });
    ret.add(g2);
  }
  /**
   * Scatter / beeswarm layout: position every unit on a real numeric X value
   * axis by its own value (`_unitValueOf`), laned by category on Y. Within a
   * lane an anti-overlap "swarm" pack (or a random jitter) spreads the dots off
   * the centre line so equal / close values do not stack on top of each other.
   * This is the unit chart's answer to "put these on axes": one dot per datum,
   * placed by data, with a drawn value axis + category lanes (see
   * _drawScatterAxes). Needs the per-unit object form (each datum a numeric
   * `value`/`y`); flat counts have no per-unit value, so their lanes stay empty.
   * @param {any} opts
   */
  _layoutScatter(opts) {
    const w2 = this.w;
    const scfg = opts.scatter || {};
    if (scfg.y === "value") return this._layoutScatter2D(opts);
    if (scfg.orientation === "vertical") return this._layoutScatterVertical(opts);
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const unitData = w2.seriesData.unitData || [];
    const names = w2.seriesData.seriesNames || [];
    const valueOf = (d) => this._unitValueOf(d);
    const sizeStats = this._scatterSizeStats(scfg, unitData);
    const catVals = unitData.map(
      (cat) => Array.isArray(cat) ? cat.map(valueOf) : []
    );
    const isNum = (v2) => v2 != null && isFinite(v2);
    const visible = catVals.map((_, i) => i).filter((i) => catVals[i].some(isNum));
    const Kv = Math.max(1, visible.length);
    let vmin = Infinity;
    let vmax = -Infinity;
    catVals.forEach(
      (vs) => vs.forEach((v2) => {
        if (v2 != null && isFinite(v2)) {
          if (v2 < vmin) vmin = v2;
          if (v2 > vmax) vmax = v2;
        }
      })
    );
    if (vmin === Infinity) {
      vmin = 0;
      vmax = 1;
    }
    const tickAmount = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5));
    const domain = this._scatterValueDomain(scfg, vmin, vmax, tickAmount);
    const xMin = domain.min;
    const xMax = domain.max;
    const xSpan = xMax - xMin || 1;
    const laneW = scfg.laneLabelWidth != null ? Math.max(0, scfg.laneLabelWidth) : Kv > 1 ? 92 : 8;
    const bottomGutter = 30 + (scfg.xTitle ? 20 : 0);
    const plotL = laneW;
    const plotR = gw - 8;
    const plotT = 10;
    const plotB = gh - bottomGutter;
    const plotW = Math.max(4, plotR - plotL);
    const plotH = Math.max(4, plotB - plotT);
    const plotX = (v2) => plotL + (v2 - xMin) / xSpan * plotW;
    const laneH = plotH / Kv;
    const laneCy = (slot) => plotT + laneH * (slot + 0.5);
    let r = 0;
    const fixed = this._fixedRadius(opts);
    if (fixed) {
      r = fixed;
    } else {
      const maxLane = Math.max(
        1,
        ...visible.map((i) => catVals[i].filter(isNum).length)
      );
      r = Math.max(
        2,
        Math.min(6, laneH * 0.12, plotW / (2.5 * Math.sqrt(maxLane)))
      );
    }
    this._lastDotR = r;
    const spacing = opts.spacing > 0 ? opts.spacing : 1;
    const step = Math.max(0.5, r * spacing);
    const jitter = scfg.spread === "jitter";
    const clusters = [];
    const lanes = [];
    const maxR = sizeStats ? sizeStats.rMax : r;
    visible.forEach((ci, slot) => {
      const cy = laneCy(slot);
      lanes.push({ i: ci, cy, name: names[ci] || `series-${ci + 1}` });
      const cat = unitData[ci] || [];
      const pts = cat.map((d, j) => {
        const v2 = valueOf(d);
        const p2 = { j, px: plotX(isNum(v2) ? v2 : xMin), y: cy };
        if (sizeStats) p2.r = this._scatterRadius(d, sizeStats, r);
        return p2;
      });
      if (jitter) {
        const halfLane = Math.max(maxR, laneH / 2 - maxR);
        pts.forEach((p2, k) => {
          const t = (k * 9301 + 49297) % 233280 / 233280;
          p2.y = cy + (t * 2 - 1) * halfLane;
        });
      } else {
        this._beeswarm(pts, cy, r, step, maxR);
      }
      clusters.push({
        i: ci,
        cx: (plotL + plotR) / 2,
        cy,
        outerR: laneH / 2,
        dots: pts.map((p2) => ({ x: p2.px, y: p2.y, r: p2.r }))
      });
    });
    const ticks = domain.ticks;
    this._scatterAxis = {
      mode: "1d",
      plotL,
      plotR,
      plotT,
      plotB,
      xMin,
      xMax,
      plotX,
      ticks,
      lanes,
      xTitle: scfg.xTitle,
      formatter: typeof scfg.xFormatter === "function" ? scfg.xFormatter : null,
      gridlines: scfg.gridlines !== false
    };
    return clusters;
  }
  /**
   * Vertical beeswarm: the transpose of _layoutScatter. The value runs UP the Y
   * axis and each category is a column (lane) across X; the swarm pack spreads
   * dots horizontally off each column's centre line. The value-axis config keys
   * (`xMin`/`xMax`/`xTitle`/`xFormatter`/`tickAmount`) still describe the value
   * axis (now Y), so flipping `orientation` keeps the same value settings.
   * @param {any} opts
   */
  _layoutScatterVertical(opts) {
    const w2 = this.w;
    const scfg = opts.scatter || {};
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const unitData = w2.seriesData.unitData || [];
    const names = w2.seriesData.seriesNames || [];
    const valueOf = (d) => this._unitValueOf(d);
    const sizeStats = this._scatterSizeStats(scfg, unitData);
    const catVals = unitData.map(
      (cat) => Array.isArray(cat) ? cat.map(valueOf) : []
    );
    const isNum = (v2) => v2 != null && isFinite(v2);
    const visible = catVals.map((_, i) => i).filter((i) => catVals[i].some(isNum));
    const Kv = Math.max(1, visible.length);
    let vmin = Infinity;
    let vmax = -Infinity;
    catVals.forEach(
      (vs) => vs.forEach((v2) => {
        if (v2 != null && isFinite(v2)) {
          if (v2 < vmin) vmin = v2;
          if (v2 > vmax) vmax = v2;
        }
      })
    );
    if (vmin === Infinity) {
      vmin = 0;
      vmax = 1;
    }
    const tickAmount = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5));
    const domain = this._scatterValueDomain(scfg, vmin, vmax, tickAmount);
    const vMin = domain.min;
    const vMax = domain.max;
    const vSpan = vMax - vMin || 1;
    const leftGutter = 46 + (scfg.xTitle ? 18 : 0);
    const bottomGutter = Kv > 1 ? 26 : 10;
    const plotL = leftGutter;
    const plotR = gw - 10;
    const plotT = 10;
    const plotB = gh - bottomGutter;
    const plotW = Math.max(4, plotR - plotL);
    const plotH = Math.max(4, plotB - plotT);
    const plotY = (v2) => plotB - (v2 - vMin) / vSpan * plotH;
    const laneW = plotW / Kv;
    const laneCx = (slot) => plotL + laneW * (slot + 0.5);
    let r = 0;
    const fixed = this._fixedRadius(opts);
    if (fixed) {
      r = fixed;
    } else {
      const maxLane = Math.max(
        1,
        ...visible.map((i) => catVals[i].filter(isNum).length)
      );
      r = Math.max(
        2,
        Math.min(6, laneW * 0.12, plotH / (2.5 * Math.sqrt(maxLane)))
      );
    }
    this._lastDotR = r;
    const spacing = opts.spacing > 0 ? opts.spacing : 1;
    const step = Math.max(0.5, r * spacing);
    const jitter = scfg.spread === "jitter";
    const clusters = [];
    const lanes = [];
    const maxR = sizeStats ? sizeStats.rMax : r;
    visible.forEach((ci, slot) => {
      const cx = laneCx(slot);
      lanes.push({ i: ci, cx, name: names[ci] || `series-${ci + 1}` });
      const cat = unitData[ci] || [];
      const pts = cat.map((d, j) => {
        const v2 = valueOf(d);
        const p2 = { j, py: plotY(isNum(v2) ? v2 : vMin), x: cx };
        if (sizeStats) p2.r = this._scatterRadius(d, sizeStats, r);
        return p2;
      });
      if (jitter) {
        const halfLane = Math.max(maxR, laneW / 2 - maxR);
        pts.forEach((p2, k) => {
          const t = (k * 9301 + 49297) % 233280 / 233280;
          p2.x = cx + (t * 2 - 1) * halfLane;
        });
      } else {
        this._beeswarm(pts, cx, r, step, maxR, true);
      }
      clusters.push({
        i: ci,
        cx,
        cy: (plotT + plotB) / 2,
        outerR: laneW / 2,
        dots: pts.map((p2) => ({ x: p2.x, y: p2.py, r: p2.r }))
      });
    });
    const ticks = domain.ticks;
    this._scatterAxis = {
      mode: "1d",
      orientation: "vertical",
      plotL,
      plotR,
      plotT,
      plotB,
      vMin,
      vMax,
      plotY,
      ticks,
      lanes,
      valueTitle: scfg.xTitle,
      formatter: typeof scfg.xFormatter === "function" ? scfg.xFormatter : null,
      gridlines: scfg.gridlines !== false
    };
    return clusters;
  }
  /**
   * 2D value-value scatter: each datum is a point at (`x`, `y`) on two numeric
   * axes (a scatter / bubble plot in the unit family - premium, keyed
   * transitions, per-unit colour/tooltip). Category = colour (one series group
   * per category). With `scatter.sizeRange` set, each dot is a BUBBLE scaled (by
   * area) from its `sizeField` (default 'z'). Needs the object form with numeric
   * `x` + `y`.
   * @param {any} opts
   */
  _layoutScatter2D(opts) {
    const w2 = this.w;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const scfg = opts.scatter || {};
    const unitData = w2.seriesData.unitData || [];
    const isNum = (v2) => typeof v2 === "number" && isFinite(v2);
    const xOf = (d) => d && typeof d === "object" ? d.x : null;
    const yOf = (d) => d && typeof d === "object" ? d.y != null ? d.y : d.value : null;
    const visible = unitData.map((_, i) => i).filter(
      (i) => (unitData[i] || []).some((d) => isNum(xOf(d)) && isNum(yOf(d)))
    );
    let xmn = Infinity;
    let xmx = -Infinity;
    let ymn = Infinity;
    let ymx = -Infinity;
    unitData.forEach(
      (cat) => (cat || []).forEach((d) => {
        const x = xOf(d);
        const y = yOf(d);
        if (isNum(x) && isNum(y)) {
          if (x < xmn) xmn = x;
          if (x > xmx) xmx = x;
          if (y < ymn) ymn = y;
          if (y > ymx) ymx = y;
        }
      })
    );
    if (xmn === Infinity) {
      xmn = 0;
      xmx = 1;
      ymn = 0;
      ymx = 1;
    }
    const xTicksN = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5));
    const yTicksN = Math.max(2, Math.round(scfg.yTickAmount > 0 ? scfg.yTickAmount : 5));
    const nx = this._niceScale(
      scfg.xMin != null ? scfg.xMin : xmn,
      scfg.xMax != null ? scfg.xMax : xmx,
      xTicksN
    );
    const ny = this._niceScale(
      scfg.yMin != null ? scfg.yMin : ymn,
      scfg.yMax != null ? scfg.yMax : ymx,
      yTicksN
    );
    const xMin = scfg.xMin != null ? scfg.xMin : nx.min;
    const xMax = scfg.xMax != null ? scfg.xMax : nx.max;
    const yMin = scfg.yMin != null ? scfg.yMin : ny.min;
    const yMax = scfg.yMax != null ? scfg.yMax : ny.max;
    const xSpan = xMax - xMin || 1;
    const ySpan = yMax - yMin || 1;
    const leftGutter = 46 + (scfg.yTitle ? 18 : 0);
    const bottomGutter = 30 + (scfg.xTitle ? 20 : 0);
    const plotL = leftGutter;
    const plotR = gw - 12;
    const plotT = 10;
    const plotB = gh - bottomGutter;
    const plotW = Math.max(4, plotR - plotL);
    const plotH = Math.max(4, plotB - plotT);
    const plotX = (v2) => plotL + (v2 - xMin) / xSpan * plotW;
    const plotY = (v2) => plotB - (v2 - yMin) / ySpan * plotH;
    const sizeStats = this._scatterSizeStats(scfg, unitData);
    const baseR = this._fixedRadius(opts) || 5;
    this._lastDotR = baseR;
    const clusters = [];
    visible.forEach((ci) => {
      const cat = unitData[ci] || [];
      const dots = cat.map((d) => {
        const x = xOf(d);
        const y = yOf(d);
        return {
          x: plotX(isNum(x) ? x : xMin),
          y: plotY(isNum(y) ? y : yMin),
          r: sizeStats ? this._scatterRadius(d, sizeStats, baseR) : void 0
        };
      });
      clusters.push({
        i: ci,
        cx: (plotL + plotR) / 2,
        cy: (plotT + plotB) / 2,
        outerR: plotH / 2,
        dots
      });
    });
    const mkTicks = (lo, hi, span, spacing, pinned, n) => {
      const out = [];
      if (pinned) {
        for (let k = 0; k < n; k++) out.push(lo + span * k / (n - 1));
      } else {
        const sp = spacing || span / Math.max(1, n - 1);
        for (let v2 = lo; v2 <= hi + sp * 0.5; v2 += sp) {
          out.push(Math.abs(v2) < sp * 1e-9 ? 0 : v2);
        }
      }
      return out;
    };
    this._scatterAxis = {
      mode: "2d",
      plotL,
      plotR,
      plotT,
      plotB,
      plotX,
      plotY,
      xTicks: mkTicks(
        xMin,
        xMax,
        xSpan,
        nx.spacing,
        scfg.xMin != null || scfg.xMax != null,
        xTicksN
      ),
      yTicks: mkTicks(
        yMin,
        yMax,
        ySpan,
        ny.spacing,
        scfg.yMin != null || scfg.yMax != null,
        yTicksN
      ),
      xTitle: scfg.xTitle,
      yTitle: scfg.yTitle,
      xFormatter: typeof scfg.xFormatter === "function" ? scfg.xFormatter : null,
      yFormatter: typeof scfg.yFormatter === "function" ? scfg.yFormatter : null,
      gridlines: scfg.gridlines !== false
    };
    return clusters;
  }
  /**
   * Bubble size stats for the scatter layout, or null when `scatter.sizeRange`
   * is not a `[minR, maxR]` pair. Reads the global range of each datum's
   * `sizeField` (default 'z') so a value maps to a radius (area scale) in
   * _scatterRadius.
   * @param {any} scfg @param {any[][]} unitData
   * @returns {{zmin:number,zmax:number,rMin:number,rMax:number,field:string}|null}
   */
  _scatterSizeStats(scfg, unitData) {
    const range = scfg && scfg.sizeRange;
    if (!Array.isArray(range) || range.length < 2) return null;
    const rMin = Math.max(0.5, +range[0]);
    const rMax = Math.max(rMin, +range[1]);
    const field = scfg.sizeField || "z";
    let zmin = Infinity;
    let zmax = -Infinity;
    unitData.forEach(
      (cat) => (cat || []).forEach((d) => {
        const z = d && typeof d === "object" ? d[field] : null;
        if (typeof z === "number" && isFinite(z)) {
          if (z < zmin) zmin = z;
          if (z > zmax) zmax = z;
        }
      })
    );
    if (zmin === Infinity) return null;
    return { zmin, zmax, rMin, rMax, field };
  }
  /**
   * Radius for one datum under the bubble size stats: area proportional to the
   * `sizeField` value (so radius grows with sqrt), between rMin and rMax. A
   * missing value collapses to rMin.
   * @param {any} d
   * @param {{zmin:number,zmax:number,rMin:number,rMax:number,field:string}} st
   * @param {number} fallback @returns {number}
   */
  _scatterRadius(d, st, fallback) {
    if (!st) return fallback;
    const z = d && typeof d === "object" ? d[st.field] : null;
    if (typeof z !== "number" || !isFinite(z)) return st.rMin;
    const t = st.zmax > st.zmin ? (z - st.zmin) / (st.zmax - st.zmin) : 1;
    const tc = Math.max(0, Math.min(1, t));
    const aMin = st.rMin * st.rMin;
    const aMax = st.rMax * st.rMax;
    return Math.sqrt(aMin + tc * (aMax - aMin));
  }
  /**
   * One-dimensional anti-overlap "beeswarm" pack: given points with a fixed x
   * (`px`) and a lane centre `cy`, assign each a y so no two dots overlap (centre
   * distance >= r_i + r_j). Greedy in ascending-x order, trying offsets 0, +step,
   * -step, +2step ... and taking the SMALLEST that clears every already-placed
   * neighbour still within reach in x. No-overlap always wins: a very dense lane
   * grows a taller swarm rather than stacking dots (offsets are not hard-clamped
   * to the lane). Each point may carry its own radius `r` (bubble beeswarm),
   * else `rFallback` applies; `maxR` bounds the value-window break. Deterministic
   * (no physics, no randomness).
   *
   * Orientation-agnostic: the "fixed" axis is the value axis and the "spread"
   * axis is the lane thickness. Horizontal (default): fixed = `px`, spread = `y`
   * (mutates `.y`). Vertical: fixed = `py`, spread = `x` (mutates `.x`).
   * @param {any[]} pts @param {number} center lane centre on the spread axis
   * @param {number} rFallback @param {number} step @param {number} [maxR]
   * @param {boolean} [vertical]
   */
  _beeswarm(pts, center, rFallback, step, maxR, vertical = false) {
    const fk = vertical ? "py" : "px";
    const sk = vertical ? "x" : "y";
    const order = pts.slice().sort((a, b2) => a[fk] - b2[fk]);
    const placed = [];
    const rCap = maxR != null ? maxR : rFallback;
    order.forEach((p2) => {
      const pr = p2.r != null ? p2.r : rFallback;
      let chosen = 0;
      for (let k = 0; k < 2e3; k++) {
        const off = k === 0 ? 0 : Math.ceil(k / 2) * step * (k % 2 ? 1 : -1);
        const s = center + off;
        let ok = true;
        for (let m2 = placed.length - 1; m2 >= 0; m2--) {
          const q = placed[m2];
          const df = p2[fk] - q.f;
          if (df > pr + rCap) break;
          const need = pr + q.r;
          const ds = s - q.s;
          if (df * df + ds * ds < need * need) {
            ok = false;
            break;
          }
        }
        if (ok) {
          chosen = off;
          break;
        }
      }
      p2[sk] = center + chosen;
      placed.push({ f: p2[fk], s: p2[sk], r: pr });
    });
  }
  /**
   * Value-axis domain + ticks for a 1D beeswarm. The domain ALWAYS contains
   * every datum: a swarm that clips a dot outside the plot box is a bug, so an
   * explicit `xMin`/`xMax` only FRAMES the axis and is extended by whole
   * tick-steps whenever the data would otherwise overflow. Orientation-agnostic:
   * the same value axis is X for a horizontal swarm and Y for a vertical one.
   * @param {any} scfg scatter config
   * @param {number} vmin data minimum @param {number} vmax data maximum
   * @param {number} tickAmount desired tick count
   * @returns {{ min:number, max:number, ticks:number[] }}
   */
  _scatterValueDomain(scfg, vmin, vmax, tickAmount) {
    const buildTicks = (min, max, spacing2) => {
      const ticks = [];
      for (let v2 = min; v2 <= max + spacing2 * 0.5; v2 += spacing2) {
        ticks.push(Math.abs(v2) < spacing2 * 1e-9 ? 0 : v2);
      }
      return ticks;
    };
    if (scfg.xMin != null || scfg.xMax != null) {
      let min = scfg.xMin != null ? scfg.xMin : vmin;
      let max = scfg.xMax != null ? scfg.xMax : vmax;
      if (!(max > min)) max = min + 1;
      const spacing2 = (max - min) / Math.max(1, tickAmount - 1);
      if (vmin < min) min -= Math.ceil((min - vmin) / spacing2) * spacing2;
      if (vmax > max) max += Math.ceil((vmax - max) / spacing2) * spacing2;
      return { min, max, ticks: buildTicks(min, max, spacing2) };
    }
    const nice = this._niceScale(vmin, vmax, tickAmount);
    const spacing = nice.spacing || (nice.max - nice.min) / Math.max(1, tickAmount - 1);
    return { min: nice.min, max: nice.max, ticks: buildTicks(nice.min, nice.max, spacing) };
  }
  /**
   * A "nice" numeric scale [min, max] + tick spacing covering [dataMin, dataMax]
   * with about `ticks` ticks, using rounded 1/2/5 x 10^n steps. Homegrown (no
   * dependency) - lean-core.
   * @param {number} dataMin @param {number} dataMax @param {number} ticks
   * @returns {{min:number,max:number,spacing:number}}
   */
  _niceScale(dataMin, dataMax, ticks) {
    const lo = dataMin;
    let hi = dataMax;
    if (!(hi > lo)) hi = lo + 1;
    const range = this._niceNum(hi - lo, false);
    const spacing = this._niceNum(range / Math.max(1, ticks - 1), true);
    return {
      min: Math.floor(lo / spacing) * spacing,
      max: Math.ceil(hi / spacing) * spacing,
      spacing
    };
  }
  /**
   * Round a range to a "nice" 1/2/5 x 10^n number (Heckbert's loose/round label
   * algorithm).
   * @param {number} range @param {boolean} round @returns {number}
   */
  _niceNum(range, round) {
    const rng = range > 0 ? range : 1;
    const exp = Math.floor(Math.log(rng) / Math.LN10);
    const frac = rng / Math.pow(10, exp);
    let nf;
    if (round) {
      nf = frac < 1.5 ? 1 : frac < 3 ? 2 : frac < 7 ? 5 : 10;
    } else {
      nf = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 5 ? 5 : 10;
    }
    return nf * Math.pow(10, exp);
  }
  /**
   * Draw the scatter chrome behind the dots, from the geometry the layout
   * stashed on `this._scatterAxis`. 1D (beeswarm): vertical X gridlines +
   * baseline + tick labels (+ x title) + a per-lane category label in the
   * category colour. 2D: both X + Y gridlines, both axes' tick labels, and
   * rotated/placed axis titles (no lane labels - category is colour). Browser-
   * only (SSR renders the dots without the chrome, as with cluster labels).
   * @param {any} ret @param {Graphics} graphics
   */
  _drawScatterAxes(ret, graphics) {
    const w2 = this.w;
    if (!Environment.isBrowser()) return;
    const ax = this._scatterAxis;
    if (!ax) return;
    const NS = "http://www.w3.org/2000/svg";
    const g2 = graphics.group({ class: "apexcharts-unit-axis" });
    const gridColor = w2.config.grid && w2.config.grid.borderColor || "rgba(128,128,128,0.18)";
    const axisColor = "rgba(128,128,128,0.5)";
    const cfgColors = w2.config.xaxis && w2.config.xaxis.labels && w2.config.xaxis.labels.style && w2.config.xaxis.labels.style.colors;
    const configuredLabelColor = Array.isArray(cfgColors) ? cfgColors[0] : cfgColors;
    const labelColor = configuredLabelColor || "rgba(120,130,140,0.9)";
    const line = (x1, y1, x2, y2, stroke) => {
      const l = BrowserAPIs.createElementNS(NS, "line");
      l.setAttribute("x1", String(x1));
      l.setAttribute("y1", String(y1));
      l.setAttribute("x2", String(x2));
      l.setAttribute("y2", String(y2));
      l.setAttribute("stroke", stroke);
      l.setAttribute("shape-rendering", "crispEdges");
      g2.node.appendChild(l);
    };
    const text = (str, x, y, anchor, fill, size, weight, cls) => {
      const t = BrowserAPIs.createElementNS(NS, "text");
      t.setAttribute("class", cls);
      t.setAttribute("x", String(x));
      t.setAttribute("y", String(y));
      t.setAttribute("text-anchor", anchor);
      t.setAttribute("dominant-baseline", "middle");
      t.setAttribute("font-size", `${size}px`);
      t.setAttribute("font-family", w2.config.chart.fontFamily || "inherit");
      t.setAttribute("font-weight", String(weight));
      t.setAttribute("fill", fill);
      t.textContent = str;
      g2.node.appendChild(t);
    };
    if (ax.mode === "2d") {
      ax.yTicks.forEach((v2) => {
        const y = ax.plotY(v2);
        if (ax.gridlines) line(ax.plotL, y, ax.plotR, y, gridColor);
        const label = ax.yFormatter ? String(ax.yFormatter(v2)) : this._formatTick(v2);
        text(label, ax.plotL - 8, y, "end", labelColor, 11, 400, "apexcharts-unit-tick");
      });
      ax.xTicks.forEach((v2) => {
        const x = ax.plotX(v2);
        if (ax.gridlines) line(x, ax.plotT, x, ax.plotB, gridColor);
        const label = ax.xFormatter ? String(ax.xFormatter(v2)) : this._formatTick(v2);
        text(label, x, ax.plotB + 14, "middle", labelColor, 11, 400, "apexcharts-unit-tick");
      });
      line(ax.plotL, ax.plotB, ax.plotR, ax.plotB, axisColor);
      line(ax.plotL, ax.plotT, ax.plotL, ax.plotB, axisColor);
      if (ax.xTitle) {
        text(
          String(ax.xTitle),
          (ax.plotL + ax.plotR) / 2,
          ax.plotB + 32,
          "middle",
          labelColor,
          12,
          600,
          "apexcharts-unit-axis-title"
        );
      }
      if (ax.yTitle) {
        const yt = BrowserAPIs.createElementNS(NS, "text");
        yt.setAttribute("class", "apexcharts-unit-axis-title");
        const tx = 14;
        const ty = (ax.plotT + ax.plotB) / 2;
        yt.setAttribute("x", String(tx));
        yt.setAttribute("y", String(ty));
        yt.setAttribute("text-anchor", "middle");
        yt.setAttribute("font-size", "12px");
        yt.setAttribute("font-family", w2.config.chart.fontFamily || "inherit");
        yt.setAttribute("font-weight", "600");
        yt.setAttribute("fill", labelColor);
        yt.setAttribute("transform", `rotate(-90 ${tx} ${ty})`);
        yt.textContent = String(ax.yTitle);
        g2.node.appendChild(yt);
      }
      ret.add(g2);
      return;
    }
    if (ax.orientation === "vertical") {
      ax.ticks.forEach((v2) => {
        const y = ax.plotY(v2);
        if (ax.gridlines) line(ax.plotL, y, ax.plotR, y, gridColor);
        const label = ax.formatter ? String(ax.formatter(v2)) : this._formatTick(v2);
        text(label, ax.plotL - 8, y, "end", labelColor, 11, 400, "apexcharts-unit-tick");
      });
      line(ax.plotL, ax.plotT, ax.plotL, ax.plotB, axisColor);
      if (ax.valueTitle) {
        const yt = BrowserAPIs.createElementNS(NS, "text");
        yt.setAttribute("class", "apexcharts-unit-axis-title");
        const tx = 14;
        const ty = (ax.plotT + ax.plotB) / 2;
        yt.setAttribute("x", String(tx));
        yt.setAttribute("y", String(ty));
        yt.setAttribute("text-anchor", "middle");
        yt.setAttribute("font-size", "12px");
        yt.setAttribute("font-family", w2.config.chart.fontFamily || "inherit");
        yt.setAttribute("font-weight", "600");
        yt.setAttribute("fill", labelColor);
        yt.setAttribute("transform", `rotate(-90 ${tx} ${ty})`);
        yt.textContent = String(ax.valueTitle);
        g2.node.appendChild(yt);
      }
      ax.lanes.forEach((lane) => {
        const color = configuredLabelColor || w2.globals.colors[lane.i] || w2.globals.colors[0] || "#008FFB";
        text(lane.name, lane.cx, ax.plotB + 16, "middle", color, 12, 600, "apexcharts-unit-lane-label");
      });
      ret.add(g2);
      return;
    }
    ax.ticks.forEach((v2, idx) => {
      const x = ax.plotX(v2);
      if (ax.gridlines) line(x, ax.plotT, x, ax.plotB, gridColor);
      const label = ax.formatter ? String(ax.formatter(v2)) : this._formatTick(v2);
      const anchor = idx === 0 ? "start" : idx === ax.ticks.length - 1 ? "end" : "middle";
      text(label, x, ax.plotB + 14, anchor, labelColor, 11, 400, "apexcharts-unit-tick");
    });
    line(ax.plotL, ax.plotB, ax.plotR, ax.plotB, axisColor);
    if (ax.xTitle) {
      text(
        String(ax.xTitle),
        (ax.plotL + ax.plotR) / 2,
        ax.plotB + 32,
        "middle",
        labelColor,
        12,
        600,
        "apexcharts-unit-axis-title"
      );
    }
    if (ax.plotL > 12) {
      ax.lanes.forEach((lane) => {
        const color = configuredLabelColor || w2.globals.colors[lane.i] || w2.globals.colors[0] || "#008FFB";
        text(lane.name, ax.plotL - 8, lane.cy, "end", color, 12, 600, "apexcharts-unit-lane-label");
      });
    }
    ret.add(g2);
  }
  /**
   * Compact tick-value formatting: integers as-is, otherwise trimmed to a short
   * decimal; large magnitudes get a k/M suffix.
   * @param {number} v @returns {string}
   */
  _formatTick(v2) {
    if (!isFinite(v2)) return "";
    const a = Math.abs(v2);
    if (a >= 1e6) return `${+(v2 / 1e6).toFixed(1)}M`;
    if (a >= 1e4) return `${+(v2 / 1e3).toFixed(1)}k`;
    if (Number.isInteger(v2)) return String(v2);
    return String(+v2.toFixed(2));
  }
  /**
   * Distribute `total` whole cells across `counts` in proportion to each value,
   * using the largest-remainder method so the parts sum to exactly `total`
   * (used by the grid/waffle percentage mode).
   * @param {number[]} counts @param {number} total @returns {number[]}
   */
  _largestRemainder(counts, total) {
    const sum = counts.reduce((a, b2) => a + b2, 0);
    if (sum <= 0 || total <= 0) return counts.map(() => 0);
    const exact = counts.map((c) => c / sum * total);
    const floors = exact.map((v2) => Math.floor(v2));
    const used = floors.reduce((a, b2) => a + b2, 0);
    const remaining = Math.max(0, total - used);
    const byFrac = exact.map((v2, i) => ({ i, frac: v2 - Math.floor(v2) })).sort((a, b2) => b2.frac - a.frac);
    const out = floors.slice();
    for (let n = 0; n < remaining && n < byFrac.length; n++) {
      out[byFrac[n].i]++;
    }
    return out;
  }
  /**
   * Phyllotaxis (sunflower) placement for `n` points around (cx, cy).
   * @param {number} cx @param {number} cy @param {number} n
   * @param {number} step @param {number} startIndex
   * @returns {{x:number,y:number}[]}
   */
  _spiral(cx, cy, n, step, startIndex) {
    const pts = [];
    for (let k = 0; k < n; k++) {
      const idx = startIndex + k;
      const r = step * Math.sqrt(idx + 0.5);
      const theta = idx * GOLDEN_ANGLE;
      pts.push({ x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) });
    }
    return pts;
  }
  /**
   * A fixed dot radius, if the shape/size implies one: an explicit numeric
   * `size`, or an `image` shape (sized by its own width/height). Returns null
   * when dots should auto-size to fit the plot.
   * @param {any} opts @returns {number | null}
   */
  _fixedRadius(opts) {
    if (opts.shape === "image" && opts.image) {
      return Math.max(opts.image.width || 20, opts.image.height || 20) / 2;
    }
    if (this._bubbleActive(opts) && typeof opts.sizeByValue.maxRadius === "number") {
      return opts.sizeByValue.maxRadius > 0 ? opts.sizeByValue.maxRadius : null;
    }
    if (typeof opts.size === "number" && opts.size > 0) return opts.size;
    return null;
  }
  /**
   * Whether opt-in bubble sizing applies: enabled, and the shape sizes per
   * mark. Squares and images keep a uniform size; a pictogram does not, because
   * its scale is derived per mark from the same radius a circle would use.
   * @param {any} opts @returns {boolean}
   */
  _bubbleActive(opts) {
    const sbv = opts.sizeByValue;
    return !!(sbv && sbv.enabled && opts.shape !== "image" && opts.shape !== "square");
  }
  /**
   * This datum's numeric value for sizing / tooltip: the number itself, or an
   * object's `value` / `y`. Null when there is no usable number.
   * @param {any} d @returns {number | null}
   */
  _unitValueOf(d) {
    if (typeof d === "number") return d;
    if (d && typeof d === "object") {
      const v2 = d.value != null ? d.value : d.y;
      return typeof v2 === "number" ? v2 : null;
    }
    return null;
  }
  /**
   * Radius for one bubble given the value stats. Default 'area' scaling makes
   * a bubble's AREA proportional to its value (radius grows with sqrt); 'linear'
   * scales the radius directly. Missing values collapse to the min radius.
   * @param {number|null} v
   * @param {{min:number,max:number,minR:number,maxR:number,scale:string}} stats
   * @returns {number}
   */
  _radiusForValue(v2, stats) {
    if (v2 == null || !isFinite(v2)) return stats.minR;
    const t = stats.max > stats.min ? (v2 - stats.min) / (stats.max - stats.min) : 1;
    const tc = Math.max(0, Math.min(1, t));
    if (stats.scale === "linear") {
      return stats.minR + tc * (stats.maxR - stats.minR);
    }
    const aMin = stats.minR * stats.minR;
    const aMax = stats.maxR * stats.maxR;
    return Math.sqrt(aMin + tc * (aMax - aMin));
  }
  /**
   * Value stats + radius bounds for bubble sizing, or null when it does not
   * apply (disabled, non-circle shape, or no per-unit values). `maxR` is the
   * reference radius the layout already spaced the lattice for; `minR` defaults
   * to ~35% of it.
   * @param {any[][]} unitData @param {any} opts @param {number} refR
   * @returns {{min:number,max:number,minR:number,maxR:number,scale:string}|null}
   */
  _bubbleStats(unitData, opts, refR) {
    if (!this._bubbleActive(opts)) return null;
    let vmin = Infinity;
    let vmax = -Infinity;
    unitData.forEach((cat) => {
      if (!cat) return;
      cat.forEach((d) => {
        const v2 = this._unitValueOf(d);
        if (v2 != null && isFinite(v2)) {
          if (v2 < vmin) vmin = v2;
          if (v2 > vmax) vmax = v2;
        }
      });
    });
    if (vmin === Infinity || vmax < vmin) return null;
    const sbv = opts.sizeByValue;
    const maxR = refR;
    const minR = Math.max(
      1,
      Math.min(
        maxR,
        typeof sbv.minRadius === "number" ? sbv.minRadius : maxR * 0.35
      )
    );
    return {
      min: vmin,
      max: vmax,
      minR,
      maxR,
      scale: sbv.scale === "linear" ? "linear" : "area"
    };
  }
  /**
   * Radial step between successive spiral shells. A fixed radius derives the
   * step directly; 'auto' derives it so a cluster of `count` dots fits `availR`.
   * @param {any} opts @param {number} availR @param {number} count
   * @returns {number}
   */
  _resolveStep(opts, availR, count) {
    const spacing = opts.spacing > 0 ? opts.spacing : 1;
    const fixed = this._fixedRadius(opts);
    if (fixed) return 2 * fixed * spacing;
    return availR / (Math.sqrt(Math.max(1, count)) + 0.5);
  }
  /**
   * @param {number} step @param {any} opts
   * @returns {number}
   */
  _dotRadiusFromStep(step, opts) {
    const spacing = opts.spacing > 0 ? opts.spacing : 1;
    const fixed = this._fixedRadius(opts);
    if (fixed) return fixed;
    return Math.max(1, step / (2 * spacing));
  }
  /**
   * Corner-anchored shapes (square, image) position by their top-left x/y;
   * circles position by their centre cx/cy.
   * @param {any} opts @returns {boolean}
   */
  _isCorner(opts) {
    return opts.shape === "square" || opts.shape === "image";
  }
  /**
   * Half-width/height used to convert a centre point to a corner shape's x/y.
   * @param {any} opts @param {number} [r] this mark's own radius; defaults to
   *   the chart-wide one (an image is sized by its own width/height either way)
   * @returns {{hx:number, hy:number}}
   */
  _halfExtent(opts, r) {
    if (opts.shape === "image" && opts.image) {
      return { hx: (opts.image.width || 20) / 2, hy: (opts.image.height || 20) / 2 };
    }
    const rr = r != null ? r : this._lastDotR;
    return { hx: rr, hy: rr };
  }
  /**
   * The draw + placement rule for ONE mark.
   *
   * Positioning used to be a chart-GLOBAL decision - `_isCorner(opts)` and a
   * single `_halfExtent(opts)`, hoisted out of the gather loop - which held only
   * while every mark in a render was the same element. Two things broke that:
   * a pictogram render where dot 3 is a <circle> and dot 4 a <path>, and the
   * plainer bug that a `square` sized from a per-position radius (`_drawDot`
   * uses the dot's own `rj`) was still being CENTRED with the chart-wide
   * `_lastDotR`, so a layout returning per-mark radii drew every square off its
   * own slot by `_lastDotR - r`.
   *
   * So the rule travels with the mark. A spec is one frozen object per distinct
   * (kind, size) - shared by every dot that uses it, resolved once per render -
   * carrying an int the frame loop switches on. `_place` is the only writer.
   *
   * @typedef {object} UnitMarkSpec
   * @property {number} pk PK_CIRCLE | PK_CORNER | PK_GLYPH
   * @property {number} [hx] corner: half-width
   * @property {number} [hy] corner: half-height
   * @property {any} [mark] glyph: the resolved mark definition
   * @property {string} [d] glyph: path data, in the mark's own viewBox units
   * @property {string} [fillRule] glyph: 'evenodd' when the mark declares it
   * @property {number} [s] glyph: uniform scale from viewBox units to px
   * @property {number} [ox] glyph: pre-scaled x of the viewBox centre
   * @property {number} [oy] glyph: pre-scaled y of the viewBox centre
   * @property {string} [tail] glyph: the pre-built `) scale(s)` transform tail
   * @property {number} [r] the radius this spec was fitted to
   */
  /**
   * Position one mark at (x, y), whatever element it is.
   *
   * Circles and corner shapes write byte-identically to what they wrote before
   * this seam existed, so the morph capture and every existing test read the
   * same DOM. A glyph writes ONE attribute where they write two.
   *
   * @param {SVGElement} node @param {UnitMarkSpec} spec
   * @param {number} x @param {number} y
   */
  _place(node, spec, x, y) {
    const s = (
      /** @type {any} */
      spec
    );
    if (s.pk === PK_GLYPH) {
      node.setAttribute(
        "transform",
        "translate(" + (x - s.ox) + "," + (y - s.oy) + s.tail
      );
    } else if (s.pk === PK_CORNER) {
      node.setAttribute("x", String(x - s.hx));
      node.setAttribute("y", String(y - s.hy));
    } else {
      node.setAttribute("cx", String(x));
      node.setAttribute("cy", String(y));
    }
  }
  /**
   * The spec for the chart-wide shape (no pictogram, no per-mark radius).
   * @param {any} opts @param {number} [r]
   * @returns {UnitMarkSpec}
   */
  _baseSpec(opts, r) {
    const rr = r != null ? r : this._lastDotR;
    if (!this._isCorner(opts)) return { pk: PK_CIRCLE, r: rr };
    const { hx, hy } = this._halfExtent(opts, rr);
    return { pk: PK_CORNER, hx, hy, r: rr };
  }
  /**
   * Resolve whatever `pictogram.mark` / `datum.mark` held into a mark
   * definition, or null. A name goes through the registry; an object or a bare
   * path string is taken as-is.
   *
   * An unresolvable mark warns ONCE per name and falls back rather than
   * dropping the unit: a typo should cost you the glyph, not the data point.
   *
   * @param {any} ref @returns {any|null}
   */
  _resolveMark(ref) {
    if (ref == null) return null;
    if (typeof ref === "object") return normalizeUnitMark(ref);
    if (typeof ref !== "string" || !ref) return null;
    const s = ref.trim();
    if (s[0] === "M" || s[0] === "m") return normalizeUnitMark(s);
    const found = getUnitMark(s);
    if (found) return found;
    if (!this._markWarned) this._markWarned = /* @__PURE__ */ new Set();
    if (!this._markWarned.has(s)) {
      this._markWarned.add(s);
      console.warn(
        `[ApexCharts] unit chart: no mark named "${s}" is registered. Register one with ApexCharts.registerUnitMark("${s}", pathData), or import a catalog from 'apexcharts/pictograms'.`
      );
    }
    return null;
  }
  /**
   * The draw spec for one glyph at the current lattice pitch, cached per
   * (mark, radius) for the render so thousands of units of one glyph resolve
   * once and then share both the spec and the `d` STRING.
   *
   * The scale lives in the transform rather than being baked into `d`, for two
   * reasons: baking needs a full path parser at runtime (the unit-shapes one
   * lives in a separate optional module, and arcs cannot be scaled by naive
   * number substitution), and a constant `scale(s)` costs the same single
   * attribute write per frame that a bare translate would.
   *
   * Sizing is derived from `dotR` - the radius the LAYOUT chose - so a glyph
   * occupies the box the dot itself would have. Swapping `circle` for a
   * pictogram therefore never re-flows the chart: same pitch, same slots.
   *
   * @param {any} mark @param {number} dotR @param {any} pcfg
   * @returns {UnitMarkSpec}
   */
  _glyphSpec(mark, dotR, pcfg) {
    const qr = Math.round(dotR * 10) / 10;
    const key = mark.name + "|" + mark.path.length + "|" + qr;
    const hit = this._specCache.get(key);
    if (hit) return hit;
    const vb = mark.viewBox || [0, 0, 100, 100];
    const pad = Math.max(0, Math.min(0.9, pcfg.padding || 0));
    const grow = typeof pcfg.scale === "number" && pcfg.scale > 0 ? pcfg.scale : 1;
    const box = 2 * qr * (1 - pad) * grow;
    const s = pcfg.fit === "width" ? box / vb[2] : pcfg.fit === "height" ? box / vb[3] : box / Math.max(vb[2], vb[3]);
    const spec = Object.freeze({
      pk: PK_GLYPH,
      mark,
      d: mark.path,
      fillRule: mark.fillRule,
      s,
      ox: (vb[0] + vb[2] / 2) * s,
      oy: (vb[1] + vb[3] / 2) * s,
      tail: ") scale(" + s + ")",
      r: qr
    });
    this._specCache.set(key, spec);
    return spec;
  }
  /**
   * Which mark THIS unit draws.
   *
   * Precedence mirrors how `datum.fillColor` already overrides the category
   * colour: the datum's own `mark` first (a per-unit override, so one crowd can
   * mix glyphs), then the per-series entry of a `mark` array, then the one
   * chart-wide mark.
   *
   * @param {any} opts @param {any} datum @param {number} i @param {number} r
   * @returns {UnitMarkSpec}
   */
  _markSpecFor(opts, datum, i, r) {
    if (opts.shape !== "pictogram") return this._baseSpec(opts, r);
    const pcfg = opts.pictogram || {};
    const own = datum && typeof datum === "object" ? datum.mark : void 0;
    const cfg = Array.isArray(pcfg.mark) ? pcfg.mark[i % pcfg.mark.length] : pcfg.mark;
    const mark = this._resolveMark(own != null ? own : cfg);
    if (mark) return this._glyphSpec(mark, r, pcfg);
    return this._baseSpec(
      __spreadProps(__spreadValues({}, opts), { shape: pcfg.fallback === "square" ? "square" : "circle" }),
      r
    );
  }
  /**
   * Draw one dot (circle, square, or image icon) with the category fill +
   * stroke, tagged so the shared non-axis tooltip and hover reuse work.
   * @param {Graphics} graphics @param {any} opts @param {number} dotR
   * @param {string} color @param {number} i @param {number} j
   * @param {UnitMarkSpec} [spec] this mark's resolved spec; defaults to the
   *   chart-wide shape
   * @returns {any}
   */
  _drawDot(graphics, opts, dotR, color, i, j, spec) {
    const w2 = this.w;
    const strokeW = w2.config.stroke.show ? w2.config.stroke.width : 0;
    const strokeColor = Array.isArray(w2.globals.stroke.colors) ? w2.globals.stroke.colors[i] || "none" : "none";
    const fillOpacity = typeof w2.config.fill.opacity === "number" ? w2.config.fill.opacity : 1;
    let el;
    if (spec && spec.pk === PK_GLYPH) {
      el = w2.dom.Paper.path(spec.d);
      el.node.setAttribute("fill", color);
      if (spec.fillRule === "evenodd") {
        el.node.setAttribute("fill-rule", "evenodd");
      }
      if (fillOpacity < 1) el.node.setAttribute("fill-opacity", String(fillOpacity));
      el.node.setAttribute("data:r", String(spec.r));
    } else if (opts.shape === "image" && opts.image && opts.image.src) {
      const iw = opts.image.width || 20;
      const ih = opts.image.height || 20;
      el = w2.dom.Paper.image(opts.image.src);
      el.node.setAttribute("width", String(iw));
      el.node.setAttribute("height", String(ih));
      el.node.setAttribute("preserveAspectRatio", "xMidYMid meet");
      if (opts.image.tint) {
        el.node.setAttribute("filter", `url(#${this._tintFilter(color)})`);
      }
    } else if (opts.shape === "square") {
      const side = dotR * 2;
      el = graphics.drawRect(0, 0, side, side, opts.borderRadius || 0, color, 1, strokeW, strokeColor);
      el.node.setAttribute("fill", color);
      if (fillOpacity < 1) el.node.setAttribute("fill-opacity", String(fillOpacity));
    } else {
      el = graphics.drawCircle(dotR, {
        fill: color,
        "stroke-width": strokeW,
        stroke: strokeColor
      });
      el.node.setAttribute("fill", color);
      if (fillOpacity < 1) el.node.setAttribute("fill-opacity", String(fillOpacity));
    }
    el.node.classList.add("apexcharts-unit-area");
    el.node.setAttribute("i", String(i));
    el.node.setAttribute("j", String(j));
    return el;
  }
  /**
   * Ensure (once per colour) an SVG recolour filter exists in the chart's defs
   * and return its id. The filter floods `color` and clips it to the source
   * graphic's alpha (feComposite operator="in"), so an `<image>` referencing a
   * monochrome icon is repainted in `color` while keeping its silhouette. Reused
   * across every dot of the same colour.
   * @param {string} color @returns {string}
   */
  _tintFilter(color) {
    const w2 = this.w;
    const NS = "http://www.w3.org/2000/svg";
    const safe = String(color).replace(/[^a-zA-Z0-9]/g, "");
    const id = `apexcharts-unit-tint-${w2.globals.chartID}-${safe}`;
    const svg = w2.dom.Paper.node;
    if (svg.querySelector(`#${id}`)) return id;
    let defs = svg.querySelector("defs");
    if (!defs) {
      defs = BrowserAPIs.createElementNS(NS, "defs");
      svg.insertBefore(defs, svg.firstChild);
    }
    const filter = BrowserAPIs.createElementNS(NS, "filter");
    filter.setAttribute("id", id);
    filter.setAttribute("x", "0%");
    filter.setAttribute("y", "0%");
    filter.setAttribute("width", "100%");
    filter.setAttribute("height", "100%");
    const flood = BrowserAPIs.createElementNS(NS, "feFlood");
    flood.setAttribute("flood-color", color);
    flood.setAttribute("result", "flood");
    const comp = BrowserAPIs.createElementNS(NS, "feComposite");
    comp.setAttribute("in", "flood");
    comp.setAttribute("in2", "SourceAlpha");
    comp.setAttribute("operator", "in");
    filter.appendChild(flood);
    filter.appendChild(comp);
    defs.appendChild(filter);
    return id;
  }
  /**
   * Position a non-animated dot at (x, y). Circles use cx/cy at the centre;
   * corner shapes (square, image) use x/y at the top-left; a pictogram rides a
   * transform. Callers that already hold the mark's spec pass it; the rest get
   * the chart-wide one.
   * @param {SVGElement} node @param {any} opts @param {number} x @param {number} y
   * @param {UnitMarkSpec} [spec]
   */
  _placeDot(node, opts, x, y, spec) {
    this._place(node, spec || this._baseSpec(opts), x, y);
  }
  /**
   * Parse a `#rgb` / `#rrggbb` / `rgb()` / `rgba()` colour to `[r, g, b]`, or
   * null if it cannot be parsed (the colour tween is then skipped).
   * @param {string} str @returns {number[] | null}
   */
  _rgb(str) {
    if (typeof str !== "string") return null;
    let s = str.trim();
    if (s[0] === "#") {
      if (s.length === 4) s = "#" + s[1] + s[1] + s[2] + s[2] + s[3] + s[3];
      const n = parseInt(s.slice(1, 7), 16);
      if (isNaN(n)) return null;
      return [n >> 16 & 255, n >> 8 & 255, n & 255];
    }
    const m2 = s.match(/rgba?\(([^)]+)\)/);
    if (m2) {
      const p2 = m2[1].split(",").map((x) => parseFloat(x));
      if (p2.length >= 3 && p2.every((v2) => !isNaN(v2))) return [p2[0], p2[1], p2[2]];
    }
    return null;
  }
  /**
   * Whether to run the gather / transition animation. Runs on the initial mount
   * and on data-driven updates (keyed old->new tween or cross-type burst).
   * Skipped: in SSR, when animations are off, when the caller passed
   * `animate:false` (shouldAnimate === false), on a PURE window resize (resized
   * with no data change - re-gathering on every resize would be jarring), and
   * when the user prefers reduced motion.
   *
   * Note: `w.globals.resized` is set true on every update (not just window
   * resize), so it must be paired with `!dataChanged` to isolate a real resize.
   * @returns {boolean}
   */
  _shouldAnimate() {
    const w2 = this.w;
    const anim = w2.config.chart.animations;
    if (!Environment.isBrowser()) return false;
    if (!anim || anim.enabled === false) return false;
    if (w2.globals.shouldAnimate === false) return false;
    if (w2.globals.resized && !w2.globals.dataChanged) return false;
    if (anim.respectReducedMotion && prefersReducedMotion()) return false;
    return true;
  }
  /**
   * Give every dot an x/y spring, reusing the live springs of a gather this
   * render just cancelled.
   *
   * The reuse is the whole point of the spring path. A carried spring holds a
   * dot's real on-screen position AND its velocity, so an interrupted gather
   * resumes from there. Without it the dot restarts from `cx0`, which on an
   * update is the slot it was still travelling towards - so every interruption
   * teleports it forward and then re-animates from a standstill. A dragged
   * slider or a scrubbed storyboard interrupts on almost every frame, which is
   * where that reads worst.
   *
   * Springs left over from a completed gather are at rest on their targets, so
   * carrying them is identical to making fresh ones. Only an interrupted flight
   * carries anything.
   *
   * @param {UnitAnimDot[]} dots
   * @param {any} gcfg plotOptions.unit.gather
   * @param {number} speed chart.animations.speed, in ms
   */
  _seedSprings(dots, gcfg, speed) {
    const [stiffness, damping] = springParams(gcfg.spring, speed);
    const live = this.ctx ? this.ctx._unitSprings : null;
    const springs = /* @__PURE__ */ new Map();
    for (let k = 0; k < dots.length; k++) {
      const d = dots[k];
      const carried = live && !d.isEnter && d.key != null ? live.get(d.key) : null;
      const sx = carried ? carried.x : v(d.cx0, stiffness, damping);
      const sy = carried ? carried.y : v(d.cy0, stiffness, damping);
      if (carried) {
        sx.stiffness = stiffness;
        sy.stiffness = stiffness;
        sx.damping = damping;
        sy.damping = damping;
        d.cx0 = sx.value;
        d.cy0 = sy.value;
        this._place(d.node, d.spec, d.cx0, d.cy0);
        if (sx.velocity !== 0 || sy.velocity !== 0) d.delay = 0;
      }
      d.sx = sx;
      d.sy = sy;
      if (d.key != null) springs.set(d.key, { x: sx, y: sy });
    }
    if (this.ctx) this.ctx._unitSprings = springs;
  }
  /**
   * One rAF loop that tweens every dot from its start (cx0/cy0 - either the
   * cluster centre on first mount / for entering dots, or its previous slot on
   * an update) to its target slot, staggered by index. Entering dots fade in;
   * moving dots stay opaque. Dots whose group colour changed (a 'flow' regroup)
   * cross-fade their fill from the old colour to the new one over the same ease;
   * dots whose radius changed (bubble sizing) grow/shrink over it too (circles).
   *
   * Position travels on a spring by default (`gather.motion`), so a gather
   * interrupted by the next render resumes from where the dots actually are,
   * carrying their velocity, rather than restarting from a standstill. Colour,
   * radius and opacity stay on a fixed-duration ease either way: those are 0..1
   * quantities, and the shared solver's rest thresholds are absolute (0.05 in
   * caller units), which is negligible for pixels but 5% of a unit interval.
   * @param {UnitAnimDot[]} dots
   */
  _runGather(dots) {
    const w$1 = this.w;
    const opts = w$1.config.plotOptions.unit;
    const speed = Math.max(1, w$1.config.chart.animations.speed || 800);
    const maxDelay = Math.min(speed * 0.6, 450);
    const n = dots.length;
    for (let k = 0; k < n; k++) {
      dots[k].delay = n > 1 ? k / (n - 1) * maxDelay : 0;
    }
    const gcfg = opts.gather || {};
    const motion = gcfg.motion || "auto";
    const useSpring = motion === "spring" || motion === "auto" && (!gcfg.easing || gcfg.easing === "outCubic");
    if (useSpring) this._seedSprings(dots, gcfg, speed);
    else if (this.ctx) this.ctx._unitSprings = null;
    for (let k = 0; k < n; k++) {
      const d = dots[k];
      if (d.spec.pk === PK_CIRCLE && d.r0 != null && d.r1 != null && d.r0 !== d.r1) {
        d.node.setAttribute("r", String(d.r0));
      }
    }
    for (let k = 0; k < n; k++) {
      const d = dots[k];
      if (d.fill0 && d.fill1 && d.fill0 !== d.fill1) {
        d._c0 = this._rgb(d.fill0);
        d._c1 = this._rgb(d.fill1);
      }
    }
    const easePos = gcfg.easing === "outBack" ? easeOutBack(typeof gcfg.overshoot === "number" ? gcfg.overshoot : 1.70158) : gcfg.easing === "inOutCubic" ? easeInOutCubic : easeOutCubic;
    if (this.w.globals.unitGatherRAF != null) {
      BrowserAPIs.cancelAnimationFrame(this.w.globals.unitGatherRAF);
      this.w.globals.unitGatherRAF = null;
    }
    const start = performance.now();
    let last = start;
    const stepFn = (now) => {
      if (this.w.globals.isDestroyed) {
        this.w.globals.unitGatherRAF = null;
        this.w.globals.animationEnded = true;
        return;
      }
      const dt = Math.min(MAX_FRAME_STEP, Math.max(0, (now - last) / 1e3));
      last = now;
      let done = true;
      for (let k = 0; k < n; k++) {
        const d = dots[k];
        const elapsed = now - start - d.delay;
        const t = Math.max(0, Math.min(1, elapsed / speed));
        const ec = easeOutCubic(t);
        let cx, cy;
        if (d.sx && d.sy) {
          if (elapsed >= 0 && !d.released) {
            w(d.sx, d.x);
            w(d.sy, d.y);
            d.released = true;
          }
          const restX = b(d.sx, dt);
          const restY = b(d.sy, dt);
          if (!d.released || !restX || !restY) done = false;
          cx = d.sx.value;
          cy = d.sy.value;
        } else {
          const e = easePos(t);
          cx = d.cx0 + (d.x - d.cx0) * e;
          cy = d.cy0 + (d.y - d.cy0) * e;
        }
        this._place(d.node, d.spec, cx, cy);
        if (d.isEnter) d.node.style.opacity = String(Math.min(1, t * 2.5));
        if (d._c0 && d._c1) {
          const cr = Math.round(d._c0[0] + (d._c1[0] - d._c0[0]) * ec);
          const cg = Math.round(d._c0[1] + (d._c1[1] - d._c0[1]) * ec);
          const cb = Math.round(d._c0[2] + (d._c1[2] - d._c0[2]) * ec);
          d.node.setAttribute("fill", `rgb(${cr},${cg},${cb})`);
        }
        if (d.spec.pk === PK_CIRCLE && d.r0 != null && d.r1 != null && d.r0 !== d.r1) {
          d.node.setAttribute("r", String(d.r0 + (d.r1 - d.r0) * ec));
        }
        if (t < 1) done = false;
      }
      if (done) {
        for (let k = 0; k < n; k++) {
          const d = dots[k];
          d.node.style.opacity = "";
          if (d._c1 && d.fill1) d.node.setAttribute("fill", d.fill1);
          if (d.spec.pk === PK_CIRCLE && d.r0 != null && d.r1 != null && d.r0 !== d.r1) {
            d.node.setAttribute("r", String(d.r1));
          }
        }
        this.w.globals.unitGatherRAF = null;
        this.w.globals.animationEnded = true;
      } else {
        this.w.globals.unitGatherRAF = BrowserAPIs.requestAnimationFrame(stepFn);
      }
    };
    this.w.globals.unitGatherRAF = BrowserAPIs.requestAnimationFrame(stepFn);
  }
  /**
   * Keys present in the previous render but not the current one, resolved back
   * to their old slot {x, y, fill}. These are the dots that must animate out.
   * @param {Map<string, {x:number,y:number,fill:string,r?:number,spec?:any}>} prev
   * @param {Map<string, {x:number,y:number,fill:string,r?:number,spec?:any}>} nextPrev
   * @param {any} opts
   * @returns {{x:number,y:number,fill:string,r?:number,spec?:any}[]}
   */
  _collectExits(prev, nextPrev, opts) {
    const cap = Math.max(0, opts.maxUnits || 5e3);
    const exits = [];
    for (const [key, slot] of prev) {
      if (!nextPrev.has(key)) {
        exits.push(slot);
        if (exits.length >= cap) break;
      }
    }
    return exits;
  }
  /**
   * Animate the exit ghosts out, then remove them. Layouts whose positions carry
   * data (a waffle / grid lattice, or a scatter / beeswarm on real axes) fade
   * their ghosts OUT IN PLACE - drifting them toward the plot centre would drag
   * cells across tiles or bubbles across the plane, which reads as wrong. The
   * blob / bar layouts keep the gentle inward collapse so a removal reads as
   * motion rather than a pop.
   * @param {any} group @param {{x:number,y:number,fill:string,r?:number,spec?:any}[]} exits @param {any} opts
   */
  _runExits(group, exits, opts) {
    const w2 = this.w;
    const graphics = new Graphics(w2, this.ctx);
    const dotR = this._lastDotR;
    const cx = w2.layout.gridWidth / 2;
    const cy = w2.layout.gridHeight / 2;
    const drift = opts.layout === "grid" || opts.layout === "scatter" || opts.layout === "arc" || opts.layout === "custom" ? 0 : 0.35;
    const ghosts = [];
    exits.forEach((slot) => {
      const r = slot.r != null ? slot.r : dotR;
      const spec = slot.spec || this._baseSpec(opts, r);
      const el = this._drawDot(graphics, opts, r, slot.fill, 0, 0, spec);
      el.node.classList.add("apexcharts-unit-exit");
      this._place(el.node, spec, slot.x, slot.y);
      group.add(el);
      ghosts.push({ node: el.node, x0: slot.x, y0: slot.y, spec });
    });
    if (!this._shouldAnimate()) {
      ghosts.forEach((g2) => g2.node.remove());
      return;
    }
    const speed = Math.max(1, w2.config.chart.animations.speed || 800);
    if (this.w.globals.unitExitRAF != null) {
      BrowserAPIs.cancelAnimationFrame(this.w.globals.unitExitRAF);
      this.w.globals.unitExitRAF = null;
    }
    const start = performance.now();
    const stepFn = (now) => {
      if (this.w.globals.isDestroyed) {
        this.w.globals.unitExitRAF = null;
        return;
      }
      const t = Math.max(0, Math.min(1, (now - start) / speed));
      const e = easeOutCubic(t);
      for (let k = 0; k < ghosts.length; k++) {
        const g2 = ghosts[k];
        if (drift) {
          const x = g2.x0 + (cx - g2.x0) * e * drift;
          const y = g2.y0 + (cy - g2.y0) * e * drift;
          this._place(g2.node, g2.spec, x, y);
        }
        g2.node.style.opacity = String(1 - e);
      }
      if (t < 1) {
        this.w.globals.unitExitRAF = BrowserAPIs.requestAnimationFrame(stepFn);
      } else {
        this.w.globals.unitExitRAF = null;
        group.node && group.node.remove();
      }
    };
    this.w.globals.unitExitRAF = BrowserAPIs.requestAnimationFrame(stepFn);
  }
  /**
   * Are outer (name) labels on? Only for `layout: 'custom'`: they name a colour
   * BAND, so they need categories that occupy their own part of the shape. The
   * generated layouts (`packed`, `grid`) interleave categories, and the blob /
   * bar / arc layouts already carry a label of their own.
   * @param {any} opts
   */
  _outerLabelsOn(opts) {
    const cfg = opts.clusterLabels;
    return !!(opts.layout === "custom" && cfg && cfg.show !== false && cfg.external && cfg.external.show);
  }
  /**
   * One outer label's text, as lines. Two lines by default (name, then share),
   * which is what makes the label readable at a distance from the band it names.
   * A `clusterLabels.formatter` may return "\n"-separated text to control the
   * split, or a single line.
   * @param {number} i @param {number} value @param {number} total @param {any} opts
   * @returns {string[]}
   */
  _outerLabelLines(i, value, total, opts) {
    const w2 = this.w;
    const name = w2.seriesData.seriesNames[i] || `series-${i + 1}`;
    const percent = total > 0 ? value / total * 100 : 0;
    const cfg = opts.clusterLabels;
    const text = typeof cfg.formatter === "function" ? cfg.formatter(name, { seriesIndex: i, value, percent, w: w2 }) : `${name}
${percent.toFixed(1)}%`;
    return String(text).split("\n");
  }
  /**
   * Room one side has to give up: the widest label, plus the leader line, plus a
   * little air. Capped at a quarter of the plot so one long category name shrinks
   * its own label into the gutter instead of starving the shape.
   * @param {number[]} counts @param {any} opts
   * @returns {number}
   */
  _outerLabelGutter(counts, opts) {
    const w2 = this.w;
    const cfg = opts.clusterLabels;
    const conn = cfg.external.connector || {};
    const total = counts.reduce((a, b2) => a + b2, 0);
    const lines = [];
    counts.forEach((c, i) => {
      if (c > 0) lines.push(...this._outerLabelLines(i, c, total, opts));
    });
    if (!lines.length) return 0;
    const width = measureLabelWidth(w2, lines, {
      fontSize: cfg.fontSize,
      fontFamily: cfg.fontFamily || w2.config.chart.fontFamily
    });
    const gap = conn.gap != null ? conn.gap : 8;
    const length = conn.length != null ? conn.length : 22;
    const room = width + gap + length + 8 + Math.abs(parseFloat(cfg.external.offsetX) || 0);
    return Math.min(room, w2.layout.gridWidth * 0.25);
  }
  /**
   * Plan and draw the outer labels. A band's anchor is one of its own dots - the
   * outermost on the label's side, preferring dots near the band's middle - so
   * the leader line lands on the crowd rather than on a bounding box the viewer
   * cannot see.
   *
   * Sides: a silhouette ordered by rows stacks its categories vertically, so
   * their centroids share an x and the labels have to alternate left/right down
   * the shape. One ordered by columns spreads them horizontally, so each label
   * goes to the side its band is already on.
   *
   * @param {any} ret @param {{ i:number, cx:number, cy:number, dots:{x:number,y:number,r?:number}[] }[]} clusters
   * @param {number[]} counts @param {number} total @param {any} opts
   * @param {boolean} gathering true only when the dots are flying in from the
   *   centre (first render / cross-type morph). On an update the crowd is already
   *   on screen, so the labels must not wait for anything.
   */
  _drawOuterLabels(ret, clusters, counts, total, opts, gathering) {
    const w2 = this.w;
    if (!Environment.isBrowser()) return;
    const cfg = opts.clusterLabels;
    const ext = cfg.external;
    const conn = ext.connector || {};
    const gap = conn.gap != null ? conn.gap : 8;
    const length = conn.length != null ? conn.length : 22;
    const offsetX = parseFloat(ext.offsetX) || 0;
    const offsetY = parseFloat(ext.offsetY) || 0;
    const gw = w2.layout.gridWidth;
    const gh = w2.layout.gridHeight;
    const dotR = this._lastDotR;
    const live = clusters.filter((c) => c.dots.length > 0);
    if (!live.length) return;
    let spreadX = 0;
    let spreadY = 0;
    if (live.length > 1) {
      const xs = live.map((c) => c.cx);
      const ys = live.map((c) => c.cy);
      spreadX = Math.max(...xs) - Math.min(...xs);
      spreadY = Math.max(...ys) - Math.min(...ys);
    }
    const bandedByX = spreadX > spreadY;
    const fontSize = parseFloat(cfg.fontSize) || 13;
    const lineHeight = Math.round(fontSize * 1.35);
    const items = [];
    live.slice().sort((a, b2) => a.cy - b2.cy).forEach((c, k) => {
      const lines = this._outerLabelLines(c.i, counts[c.i], total, opts);
      if (!lines.some((l) => l !== "")) return;
      const side = bandedByX ? c.cx >= gw / 2 ? "right" : "left" : k % 2 === 0 ? "right" : "left";
      const dir = side === "right" ? 1 : -1;
      let best = c.dots[0];
      let bestScore = -Infinity;
      c.dots.forEach((d) => {
        const score = dir * d.x - 0.75 * Math.abs(d.y - c.cy);
        if (score > bestScore) {
          bestScore = score;
          best = d;
        }
      });
      const anchor = { x: best.x + dir * (best.r || dotR), y: best.y };
      const elbow = { x: anchor.x + dir * gap, y: anchor.y };
      items.push({
        i: c.i,
        lines,
        anchor,
        elbow,
        labelX: elbow.x + dir * length + offsetX,
        idealY: anchor.y + offsetY,
        labelY: anchor.y + offsetY,
        side
      });
    });
    if (!items.length) return;
    const maxLines = items.reduce((m2, it) => Math.max(m2, it.lines.length), 1);
    const block = maxLines * lineHeight;
    const half = block / 2;
    ["left", "right"].forEach((side) => {
      spaceOutLabels(
        items.filter((it) => it.side === side),
        block + 2,
        gh - half,
        half
      );
    });
    const group = new Graphics(w2, this.ctx).group({
      class: "apexcharts-unit-outer-labels"
    });
    if (gathering) {
      const speed = Math.max(1, w2.config.chart.animations.speed || 800);
      group.node.classList.add("apexcharts-unit-label-delay");
      group.node.style.animationDelay = `${Math.min(speed * 0.45, 600) / 1e3}s`;
    }
    items.forEach((it) => {
      const color = w2.globals.colors[it.i] || w2.globals.colors[0] || "#008FFB";
      group.add(
        drawOuterLabel(w2, {
          lines: it.lines,
          lineHeight,
          anchor: it.anchor,
          elbow: it.elbow,
          labelX: it.labelX,
          labelY: it.labelY,
          side: it.side,
          connector: {
            show: conn.show !== false,
            width: conn.width != null ? conn.width : 1.5,
            color: conn.color || color
          },
          style: {
            fontSize: cfg.fontSize,
            fontFamily: cfg.fontFamily || w2.config.chart.fontFamily,
            fontWeight: cfg.fontWeight
          },
          foreColor: cfg.color || w2.config.chart.foreColor,
          groupClass: "apexcharts-unit-outer-label-group",
          textClass: "apexcharts-unit-outer-label",
          connectorClass: "apexcharts-unit-label-connector"
        })
      );
    });
    ret.add(group);
  }
  /**
   * A cluster label placed above (default) or below the cluster/bar. A TOP label
   * over a wide grouped/packed blob rides a curved arc (invisible arc path +
   * <textPath>, centred at 50% offset); a bottom label, a 'columns' bar, or a
   * cluster too small for the arc gets a straight centred label instead.
   * `clusterLabels.position` = 'top' | 'bottom'; `offsetY` pushes it further from
   * the blob in either direction.
   * @param {any} elSeries @param {{ i:number, cx:number, cy:number, outerR:number, flat?:boolean }} cluster
   * @param {number} value @param {number} total @param {any} opts @param {string} color
   */
  _drawClusterLabel(elSeries, cluster, value, total, opts, color) {
    const w2 = this.w;
    if (!Environment.isBrowser()) return;
    const NS = "http://www.w3.org/2000/svg";
    const name = w2.seriesData.seriesNames[cluster.i] || `series-${cluster.i + 1}`;
    const percent = total > 0 ? value / total * 100 : 0;
    const cfg = opts.clusterLabels;
    const fontSize = parseFloat(cfg.fontSize) || 13;
    let text;
    if (typeof cfg.formatter === "function") {
      text = cfg.formatter(name, {
        seriesIndex: cluster.i,
        value,
        percent,
        w: w2
      });
    } else {
      text = `${name} (${percent.toFixed(1)}%)`;
    }
    const str = typeof text === "string" ? text : String(text);
    const textEl = BrowserAPIs.createElementNS(NS, "text");
    textEl.setAttribute("class", "apexcharts-unit-label");
    textEl.setAttribute("text-anchor", "middle");
    textEl.setAttribute("font-size", `${fontSize}px`);
    textEl.setAttribute("font-family", cfg.fontFamily || w2.config.chart.fontFamily || "inherit");
    textEl.setAttribute("font-weight", String(cfg.fontWeight || 600));
    textEl.setAttribute("fill", cfg.color || color);
    const bottom = cfg.position === "bottom";
    const R = cluster.outerR + fontSize * 0.6 + 3 + (cfg.offsetY || 0);
    const estWidth = str.length * fontSize * 0.55;
    const curved = !bottom && !cluster.flat && cfg.curved !== false && estWidth <= Math.PI * R * 0.95;
    if (curved) {
      const yMid = cluster.cy;
      const x1 = cluster.cx - R;
      const x2 = cluster.cx + R;
      const d = `M ${x1} ${yMid} A ${R} ${R} 0 0 1 ${x2} ${yMid}`;
      const arcId = `apexcharts-unit-label-${w2.globals.chartID}-${cluster.i}`;
      const pathEl = BrowserAPIs.createElementNS(NS, "path");
      pathEl.setAttribute("id", arcId);
      pathEl.setAttribute("d", d);
      pathEl.setAttribute("fill", "none");
      pathEl.setAttribute("stroke", "none");
      const tp = BrowserAPIs.createElementNS(NS, "textPath");
      tp.setAttribute("href", `#${arcId}`);
      tp.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", `#${arcId}`);
      tp.setAttribute("startOffset", "50%");
      tp.textContent = str;
      textEl.appendChild(tp);
      elSeries.node.appendChild(pathEl);
    } else {
      textEl.setAttribute("x", String(cluster.cx));
      const y = bottom ? cluster.cy + cluster.outerR + fontSize + 6 + (cfg.offsetY || 0) : cluster.cy - cluster.outerR - 6 - (cfg.offsetY || 0);
      textEl.setAttribute("y", String(y));
      textEl.textContent = str;
    }
    elSeries.node.appendChild(textEl);
  }
}
_core__default.use({
  unit: Unit
});
export {
  default2 as default
};