@skdx/angular-charts 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -457,13 +457,29 @@ const size = (input, w = 600, h = 300) => ({
457
457
  width: (input.width ?? w) - legendSide(input),
458
458
  height: (input.height ?? h) - (legendOptions(input)?.position === 'bottom' ? LEGEND_HEIGHT : 0),
459
459
  });
460
- /** Vertical room the legend row takes above the plot, to shift the plot down by. */
461
- const legendTop = (input) => {
460
+ /**
461
+ * Vertical room the legend rows take above the plot, to shift the plot down by. Pass the legend
462
+ * items and the layout width to reserve every wrapped row; without them one row is assumed.
463
+ */
464
+ const legendTop = (input, items, width) => {
462
465
  const l = legendOptions(input);
463
- return l && (l.position === undefined || l.position === 'top') ? LEGEND_HEIGHT : 0;
466
+ if (!l || !(l.position === undefined || l.position === 'top'))
467
+ return 0;
468
+ const rows = items ? Math.max(1, legendRows(items, width ?? size(input).width, l)) : 1;
469
+ return rows * LEGEND_HEIGHT;
464
470
  };
465
471
  /** Average glyph width at the chart font size, for text-width estimates. */
466
472
  const CHAR = 6.5;
473
+ /** Fit visible chart text while callers keep the complete string as a title or item label. */
474
+ function fitText(text, width) {
475
+ const chars = Array.from(text);
476
+ const capacity = Math.max(0, Math.floor(width / CHAR));
477
+ return chars.length <= capacity
478
+ ? text
479
+ : capacity === 0
480
+ ? ''
481
+ : `${chars.slice(0, capacity - 1).join('')}…`;
482
+ }
467
483
  /** The series color for index `i`: an explicit override, else the chart palette, else the tokens. */
468
484
  const colorOf = (input, i, override) => override ??
469
485
  (input.palette && input.palette.length > 0
@@ -619,9 +635,29 @@ function placeLegend(layout) {
619
635
  }
620
636
  /** A tooltip box next to the pointer, kept inside the canvas; one line per entry. */
621
637
  function tooltipShapes(x, y, text, width, height, placement = 'auto') {
622
- const lines = Array.isArray(text) ? text : [text];
623
- const w = Math.max(...lines.map((l) => l.length)) * CHAR + 12;
624
- const h = 6 + lines.length * 16;
638
+ const original = Array.isArray(text) ? text : [text];
639
+ const capacity = Math.max(1, Math.floor((width - 12) / CHAR));
640
+ const wrapped = original.flatMap((line) => {
641
+ const result = [];
642
+ let rest = Array.from(line);
643
+ while (rest.length > capacity) {
644
+ let end = rest.slice(0, capacity + 1).lastIndexOf(' ');
645
+ if (end < 1)
646
+ end = capacity;
647
+ result.push(rest.slice(0, end).join(''));
648
+ rest = rest.slice(end);
649
+ if (rest[0] === ' ')
650
+ rest.shift();
651
+ }
652
+ result.push(rest.join(''));
653
+ return result;
654
+ });
655
+ const maxLines = Math.max(1, Math.floor((height - 6) / 16));
656
+ const lines = wrapped.slice(0, maxLines);
657
+ if (wrapped.length > maxLines)
658
+ lines[maxLines - 1] = fitText(`${lines[maxLines - 1]}…`, width - 12);
659
+ const w = Math.min(width, Math.max(0, ...lines.map((l) => Array.from(l).length)) * CHAR + 12);
660
+ const h = Math.min(height, 6 + lines.length * 16);
625
661
  // `auto` sits above-right of the anchor and slides inside the canvas; a fixed placement
626
662
  // centres the box on the named side and is only clamped.
627
663
  const wanted = placement === 'top'
@@ -639,13 +675,14 @@ function tooltipShapes(x, y, text, width, height, placement = 'auto') {
639
675
  {
640
676
  kind: 'rect',
641
677
  part: 'tooltip',
678
+ title: original.join('\n'),
642
679
  x: px(bx),
643
680
  y: px(by),
644
681
  width: px(w),
645
682
  height: h,
646
683
  fill: 'var(--skdx-color-surface-raised)',
647
684
  stroke: CHART_COLORS.axis,
648
- rx: 3,
685
+ rx: 8,
649
686
  },
650
687
  ...lines.map((line, i) => ({
651
688
  kind: 'text',
@@ -735,6 +772,12 @@ function axis([min, max], range, options = false, chartFormat) {
735
772
  zero: px(scale(Math.min(Math.max(0, domain[0]), domain[1]))),
736
773
  };
737
774
  }
775
+ /**
776
+ * Left (or right) margin wide enough for the widest tick label a value axis over `[lo, hi]` will
777
+ * draw, never below the default. Charts measure it before laying out so a long label such as
778
+ * "$180.00" is not clipped by the svg edge.
779
+ */
780
+ const valueAxisRoom = ([lo, hi], format) => Math.max(MARGIN$1.left, niceTicks(lo, hi).reduce((w, v) => Math.max(w, format(v).length), 0) * CHAR + 14);
738
781
  /** Whether an axis draws its grid lines. */
739
782
  const gridOn = (opts) => opts?.grid !== false;
740
783
  /** Keeps every n-th tick so labels of the given widths do not overlap along `width`. */
@@ -771,7 +814,7 @@ function stateCanvas(input, empty, w, h) {
771
814
  width,
772
815
  height,
773
816
  fill: 'var(--skdx-color-surface-sunken)',
774
- rx: 4,
817
+ rx: 8,
775
818
  });
776
819
  shapes.push({
777
820
  kind: 'text',
@@ -791,6 +834,19 @@ function stateCanvas(input, empty, w, h) {
791
834
  * This file is part of the SkandaDX organization.
792
835
  * Licensed under the MIT License. See LICENSE in the repository root.
793
836
  */
837
+ /**
838
+ * Anchor for a tick label centred at `pos`: `end` when half of it would run past `width`, `start`
839
+ * when it would run past 0, else `middle`. The outermost tick sits on the plot edge, where a long
840
+ * date or currency label would otherwise be clipped by the svg.
841
+ */
842
+ const tickAnchor = (pos, text, width) => {
843
+ const half = (text.length * CHAR) / 2;
844
+ if (pos + half > width)
845
+ return 'end';
846
+ if (pos - half < 0)
847
+ return 'start';
848
+ return 'middle';
849
+ };
794
850
  /** Grid, axis line and tick labels of an axis frame, as shapes. */
795
851
  function frameShapes(frame, options = {}) {
796
852
  const { plot, xTicks, yTicks, baseline } = frame;
@@ -825,7 +881,7 @@ function frameShapes(frame, options = {}) {
825
881
  baseline: 'middle',
826
882
  rotate: rot,
827
883
  })
828
- : label(x, y, text, { part, anchor: 'middle' });
884
+ : boundedLabel(x, y, text, frame.width, { part, anchor: tickAnchor(x, text, frame.width) });
829
885
  // Short marks across an axis line at the ticks.
830
886
  const marks = (ticks, along, at, dir, size) => {
831
887
  for (const t of ticks)
@@ -890,7 +946,10 @@ function frameShapes(frame, options = {}) {
890
946
  const ly = px(top ? y - 8 : y + 20);
891
947
  for (const t of a.ticks)
892
948
  shapes.push(top
893
- ? label(t.pos, ly, t.label, { part: 'x-label-top', anchor: 'middle' })
949
+ ? label(t.pos, ly, t.label, {
950
+ part: 'x-label-top',
951
+ anchor: tickAnchor(t.pos, t.label, frame.width),
952
+ })
894
953
  : rotated(t.pos, ly, t.label, 'x-label'));
895
954
  // The first axis keeps its title at the top left; stacked axes title their own label row.
896
955
  if (a.label)
@@ -1006,6 +1065,54 @@ function frameShapes(frame, options = {}) {
1006
1065
  return shapes;
1007
1066
  }
1008
1067
  const label = (x, y, text, extra = {}) => ({ kind: 'text', part: 'label', x, y, text, fill: CHART_COLORS.label, ...extra });
1068
+ /** A label that stays inside the SVG, with the full wording available as its native title. */
1069
+ function boundedLabel(x, y, text, width, extra = {}) {
1070
+ const at = Math.max(4, Math.min(width - 4, x));
1071
+ const anchor = extra.anchor ?? 'start';
1072
+ const available = anchor === 'middle'
1073
+ ? 2 * Math.min(at - 4, width - at - 4)
1074
+ : anchor === 'end'
1075
+ ? at - 4
1076
+ : width - at - 4;
1077
+ const visible = fitText(text, available);
1078
+ return {
1079
+ kind: 'text',
1080
+ part: 'label',
1081
+ x: at,
1082
+ y,
1083
+ fill: CHART_COLORS.label,
1084
+ ...extra,
1085
+ text: visible,
1086
+ title: visible === text ? extra.title : text,
1087
+ };
1088
+ }
1089
+ /** Keep inside and reference labels readable over arbitrary series colours. */
1090
+ function withLabelSurfaces(shapes) {
1091
+ return shapes.flatMap((shape) => {
1092
+ if (shape.kind !== 'text' ||
1093
+ (shape.fill !== 'var(--skdx-color-text-on-accent)' && shape.part !== 'reference-label'))
1094
+ return [shape];
1095
+ const width = shape.text.length * CHAR + 8;
1096
+ const x = shape.x - (shape.anchor === 'middle' ? width / 2 : shape.anchor === 'end' ? width - 4 : 4);
1097
+ const y = shape.y - (shape.baseline === 'hanging' ? 2 : shape.baseline === 'middle' ? 9 : 14);
1098
+ return [
1099
+ {
1100
+ kind: 'rect',
1101
+ part: 'label-background',
1102
+ x,
1103
+ y,
1104
+ width,
1105
+ height: 18,
1106
+ rx: 8,
1107
+ fill: 'var(--skdx-color-surface-raised)',
1108
+ },
1109
+ {
1110
+ ...shape,
1111
+ fill: shape.part === 'reference-label' ? shape.fill : 'var(--skdx-color-text-default)',
1112
+ },
1113
+ ];
1114
+ });
1115
+ }
1009
1116
  /** A dot with its native tooltip. */
1010
1117
  const point$1 = (cx, cy, r, fill, title, item) => ({ kind: 'circle', part: 'point', cx, cy, r, fill, title, item });
1011
1118
 
@@ -1725,25 +1832,39 @@ function layoutPoints(input, options) {
1725
1832
  const legendRoom = input.colorScale
1726
1833
  ? colorLegendRoom(input.colorLegend)
1727
1834
  : { bottom: 0, right: 0, options: {} };
1835
+ const legend = input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) }));
1728
1836
  const plot = plotBox(width, height, {
1729
1837
  ...MARGIN$1,
1730
- top: MARGIN$1.top + legendTop(input) + (input.yAxis?.label ? 12 : 0),
1838
+ top: MARGIN$1.top + legendTop(input, legend, width) + (input.yAxis?.label ? 12 : 0),
1731
1839
  right: MARGIN$1.right + legendRoom.right,
1732
1840
  bottom: MARGIN$1.bottom + (input.xAxis?.label ? 14 : 0) + legendRoom.bottom,
1733
1841
  });
1734
1842
  const markerSize = input.markerSize ?? 3;
1735
1843
  const hitRadius = Math.max(0, input.hitRadius ?? 0);
1736
- const jitter = Math.max(0, input.jitter ?? 0);
1844
+ const requestedJitter = Math.max(0, input.jitter ?? 0);
1845
+ const sizes = minMax(dots.map((p) => p.size));
1846
+ const extentFactor = (symbol) => symbol === 'diamond' ? 1.3 : symbol === 'triangle' ? 1.2 : 1;
1847
+ const requestedRadius = (dot, sized) => Math.max(0, sized ? options.radius(dot.size, sizes) : markerSize);
1848
+ let largestExtent = 0;
1849
+ for (const series of shown) {
1850
+ const sized = series.data.some((dot) => dot.size !== undefined);
1851
+ const factor = extentFactor(series.symbol);
1852
+ for (const dot of series.data)
1853
+ largestExtent = Math.max(largestExtent, requestedRadius(dot, sized) * factor);
1854
+ }
1855
+ // Scale visible extents together only when a tiny plot cannot contain the requested size.
1856
+ // Keep data domains intact and use the same inset scales for marks/references/trends.
1857
+ const extentScale = Math.min(1, (Math.min(plot.width, plot.height) * 0.45) / Math.max(1, largestExtent + requestedJitter));
1858
+ const jitter = requestedJitter * extentScale;
1859
+ const inset = largestExtent * extentScale + jitter;
1737
1860
  // Duplicates at one pixel are spread deterministically on a ring around it.
1738
1861
  const seen = new Map();
1739
1862
  // A color scale maps each dot's `value`; dots without one keep their series color.
1740
1863
  const colors = input.colorScale
1741
1864
  ? makeColorScale(input.colorScale, dots.map((d) => d.value), colorOf(input, 0), undefined, makeFormatter(input.format))
1742
1865
  : undefined;
1743
- const x = axis(extent(dots.map((p) => p.x), false), [plot.x, plot.x + plot.width], input.xAxis ?? {}, input.format);
1744
- const y = axis(extent(dots.map((p) => p.y), false), [plot.y + plot.height, plot.y], input.yAxis ?? {}, input.format);
1745
- const sizes = minMax(dots.map((p) => p.size));
1746
- const legend = input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) }));
1866
+ const x = axis(extent(dots.map((p) => p.x), false), [plot.x + inset, plot.x + plot.width - inset], input.xAxis ?? {}, input.format);
1867
+ const y = axis(extent(dots.map((p) => p.y), false), [plot.y + plot.height - inset, plot.y + inset], input.yAxis ?? {}, input.format);
1747
1868
  const c = canvas(input, width, height, legend);
1748
1869
  const marks = [];
1749
1870
  const texts = [];
@@ -1755,7 +1876,7 @@ function layoutPoints(input, options) {
1755
1876
  for (const dot of s.data) {
1756
1877
  const di = dot.index;
1757
1878
  const title = options.describe(s.name, dot, x, y);
1758
- const r = sized ? options.radius(dot.size, sizes) : markerSize;
1879
+ const r = requestedRadius(dot, sized) * extentScale;
1759
1880
  let cx = px(x.scale(dot.x));
1760
1881
  let cy = px(y.scale(dot.y));
1761
1882
  if (jitter > 0) {
@@ -1847,7 +1968,29 @@ function layoutPoints(input, options) {
1847
1968
  }
1848
1969
  }
1849
1970
  }
1850
- const frame = { ...c, plot, xTicks: x.ticks, yTicks: y.ticks, baseline: y.zero };
1971
+ // Check the final edge anchors as well as average tick spacing.
1972
+ let tickRight = -Infinity;
1973
+ const candidates = thinTicks(x.ticks, Math.max(1, plot.width - 2 * inset));
1974
+ const kept = new Set();
1975
+ for (const tick of [...candidates].sort((a, b) => a.pos - b.pos)) {
1976
+ const textWidth = tick.label.length * CHAR;
1977
+ const rotation = input.xAxis?.tickRotation ?? 0;
1978
+ const anchor = rotation
1979
+ ? rotation < 0
1980
+ ? 'end'
1981
+ : 'start'
1982
+ : tickAnchor(tick.pos, tick.label, width);
1983
+ const left = -(anchor === 'end' ? textWidth : anchor === 'middle' ? textWidth / 2 : 0);
1984
+ const angle = (rotation * Math.PI) / 180;
1985
+ const corners = [left, left + textWidth].flatMap((x) => [-7, 7].map((y) => tick.pos + x * Math.cos(angle) - y * Math.sin(angle)));
1986
+ const start = Math.min(...corners), end = Math.max(...corners);
1987
+ if (start < tickRight + 4)
1988
+ continue;
1989
+ tickRight = end;
1990
+ kept.add(tick);
1991
+ }
1992
+ const xTicks = candidates.filter((tick) => kept.has(tick));
1993
+ const frame = { ...c, plot, xTicks, yTicks: y.ticks, baseline: y.zero };
1851
1994
  const scales = {
1852
1995
  plot,
1853
1996
  x: (v) => x.scale(categoryValue(v)),
@@ -1873,7 +2016,20 @@ function layoutPoints(input, options) {
1873
2016
  return {
1874
2017
  ...c,
1875
2018
  shapes: [
1876
- ...frameShapes(frame, { xLabel: input.xAxis?.label, yLabel: input.yAxis?.label }),
2019
+ ...frameShapes({ ...frame, height: frame.height - legendRoom.bottom }, {
2020
+ xLabel: input.xAxis?.label,
2021
+ x: input.xAxis,
2022
+ // Frame axes spread decoration fields; never forward a second title through y.
2023
+ y: {
2024
+ showLine: input.yAxis?.showLine,
2025
+ showTicks: input.yAxis?.showTicks,
2026
+ tickSize: input.yAxis?.tickSize,
2027
+ tickRotation: input.yAxis?.tickRotation,
2028
+ },
2029
+ }),
2030
+ ...(input.yAxis?.label
2031
+ ? [label(4, plot.y - 14, input.yAxis.label, { part: 'axis-title', anchor: 'start' })]
2032
+ : []),
1877
2033
  ...references,
1878
2034
  ...(opacity !== undefined && marks.some((m) => m.part === options.part)
1879
2035
  ? marks.map((m) => (m.kind === 'circle' || m.kind === 'path') && m.part !== 'hit' ? { ...m, opacity } : m)
@@ -2343,8 +2499,8 @@ function layoutCartesian(input, defaultType) {
2343
2499
  return MARGIN$1.left;
2344
2500
  const [lo, hi] = extentOf(opts.id);
2345
2501
  const f = makeFormatter(opts.format ?? percentFormat ?? input.format);
2346
- const widest = niceTicks(opts.min ?? lo, opts.max ?? hi).reduce((w, v) => Math.max(w, f(v).length), 0);
2347
- return Math.max(MARGIN$1.left, widest * CHAR + 14) + (opts.showTicks ? (opts.tickSize ?? 4) : 0);
2502
+ return (valueAxisRoom([opts.min ?? lo, opts.max ?? hi], f) +
2503
+ (opts.showTicks ? (opts.tickSize ?? 4) : 0));
2348
2504
  };
2349
2505
  // Horizontal charts run the value axes along the bottom (near) and top (far) edges. Under RTL
2350
2506
  // the vertical sides swap so the first axis sits at the right, next to the reader.
@@ -2364,8 +2520,12 @@ function layoutCartesian(input, defaultType) {
2364
2520
  }
2365
2521
  // The default bottom margin already holds the first bottom axis.
2366
2522
  const firstBottom = shownAxes.find((a) => sideOf(a) === 'bottom');
2523
+ const legend = all.map((s) => ({ name: s.name, color: s.color }));
2367
2524
  const margin = {
2368
- top: MARGIN$1.top + legendTop(input) + (defs.some((a) => a.label) ? 12 : 0) + room.top,
2525
+ top: MARGIN$1.top +
2526
+ legendTop(input, legend, width) +
2527
+ (defs.some((a) => a.label) ? 12 : 0) +
2528
+ room.top,
2369
2529
  right: horizontal
2370
2530
  ? rtl
2371
2531
  ? Math.max(MARGIN$1.left, widestCategory + 16)
@@ -2433,7 +2593,7 @@ function layoutCartesian(input, defaultType) {
2433
2593
  : plot.width /
2434
2594
  Math.max(0.2, Math.abs(Math.cos(((input.xAxis?.tickRotation ?? 0) * Math.PI) / 180))));
2435
2595
  const frame = {
2436
- ...canvas(input, width, height, all.map((s) => ({ name: s.name, color: s.color }))),
2596
+ ...canvas(input, width, height, legend),
2437
2597
  plot,
2438
2598
  xTicks: categoryTicks,
2439
2599
  yTicks: horizontal ? thinTicks(left.ticks, plot.width) : left.ticks,
@@ -2488,7 +2648,10 @@ function layoutCartesian(input, defaultType) {
2488
2648
  },
2489
2649
  y: (v, a) => value(v, a),
2490
2650
  };
2491
- shapes.push(...referenceShapes(input.references, scales));
2651
+ const references = referenceShapes(input.references, scales);
2652
+ shapes.push(...references.filter((shape) => shape.part !== 'reference-label'));
2653
+ // The reference geometry stays behind data, but its label must survive overlapping marks.
2654
+ const referenceLabels = withLabelSurfaces(references.filter((shape) => shape.part === 'reference-label'));
2492
2655
  // One slot per plain bar series and one per bar stack group, side by side inside the band.
2493
2656
  const slots = [];
2494
2657
  const slotOf = new Map();
@@ -2881,7 +3044,7 @@ function layoutCartesian(input, defaultType) {
2881
3044
  width: 6,
2882
3045
  height: box.height - 16,
2883
3046
  fill: CHART_COLORS.axis,
2884
- rx: 2,
3047
+ rx: 8,
2885
3048
  }, {
2886
3049
  kind: 'rect',
2887
3050
  part: 'brush-handle',
@@ -2890,7 +3053,7 @@ function layoutCartesian(input, defaultType) {
2890
3053
  width: 6,
2891
3054
  height: box.height - 16,
2892
3055
  fill: CHART_COLORS.axis,
2893
- rx: 2,
3056
+ rx: 8,
2894
3057
  });
2895
3058
  hit.brush = { box, window };
2896
3059
  }
@@ -2904,6 +3067,7 @@ function layoutCartesian(input, defaultType) {
2904
3067
  ...errorMarks,
2905
3068
  ...points,
2906
3069
  ...texts,
3070
+ ...referenceLabels,
2907
3071
  ...annotationShapes(input.annotations, scales),
2908
3072
  ...brush,
2909
3073
  ...frame.shapes,
@@ -3384,7 +3548,8 @@ function layoutPieChart(input) {
3384
3548
  : (p) => `${Math.round(p * 100)}%`;
3385
3549
  const cornerRadius = Math.max(0, input.cornerRadius ?? 0);
3386
3550
  const { width, height } = size(input, 300, 300);
3387
- const top = legendTop(input);
3551
+ const legend = input.data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
3552
+ const top = legendTop(input, legend, width);
3388
3553
  const start = ((input.startAngle ?? 0) * Math.PI) / 180;
3389
3554
  const end = ((input.endAngle ?? (input.startAngle ?? 0) + 360) * Math.PI) / 180;
3390
3555
  const sweep = Math.max(0, Math.min(TAU$1, end - start));
@@ -3474,7 +3639,7 @@ function layoutPieChart(input) {
3474
3639
  y2: px(cy - (r + 12) * Math.cos(mid)),
3475
3640
  stroke: CHART_COLORS.axis,
3476
3641
  });
3477
- labels.push(label(px(cx + (r + 16) * sin), ly, text, {
3642
+ labels.push(boundedLabel(px(cx + (r + 16) * sin), ly, text, width, {
3478
3643
  part: 'slice-label',
3479
3644
  anchor: sin > 0.1 ? 'start' : sin < -0.1 ? 'end' : 'middle',
3480
3645
  baseline: 'middle',
@@ -3484,7 +3649,7 @@ function layoutPieChart(input) {
3484
3649
  }
3485
3650
  else if (percent > 0.03) {
3486
3651
  const lr = (r + r0) / 2;
3487
- labels.push(label(px(cx + lr * Math.sin(mid)), px(cy - lr * Math.cos(mid)), text, {
3652
+ labels.push(boundedLabel(px(cx + lr * Math.sin(mid)), px(cy - lr * Math.cos(mid)), text, width, {
3488
3653
  part: 'slice-label',
3489
3654
  anchor: 'middle',
3490
3655
  baseline: 'middle',
@@ -3513,8 +3678,12 @@ function layoutPieChart(input) {
3513
3678
  fill: 'var(--skdx-color-text-default)',
3514
3679
  weight: 'bold',
3515
3680
  }));
3516
- const c = canvas(input, width, height, input.data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) })));
3517
- return { ...c, shapes: [...slices, ...connectors, ...c.shapes, ...labels], issues };
3681
+ const c = canvas(input, width, height, legend);
3682
+ return {
3683
+ ...c,
3684
+ shapes: withLabelSurfaces([...slices, ...connectors, ...c.shapes, ...labels]),
3685
+ issues,
3686
+ };
3518
3687
  }
3519
3688
 
3520
3689
  /*
@@ -3674,7 +3843,8 @@ function layoutRadarChart(input) {
3674
3843
  const issues = [];
3675
3844
  const format = makeFormatter(input.format);
3676
3845
  const { width, height } = size(input, 300, 300);
3677
- const top = legendTop(input);
3846
+ const legend = input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) }));
3847
+ const top = legendTop(input, legend, width);
3678
3848
  const cx = width / 2;
3679
3849
  const cy = top + (height - top) / 2;
3680
3850
  const radius = Math.max(0, Math.min(width, height - top) / 2 - 36);
@@ -3836,7 +4006,7 @@ function layoutRadarChart(input) {
3836
4006
  item: { label: s.name, series: s.name, seriesId: s.id },
3837
4007
  });
3838
4008
  });
3839
- const c = canvas(input, width, height, input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) })));
4009
+ const c = canvas(input, width, height, legend);
3840
4010
  return { ...c, shapes: [...grid, ...polygons, ...points, ...texts, ...c.shapes], issues };
3841
4011
  }
3842
4012
 
@@ -3988,7 +4158,12 @@ function layoutHeatmap(input) {
3988
4158
  part: 'cell-label',
3989
4159
  anchor: 'middle',
3990
4160
  baseline: 'middle',
3991
- fill: 'var(--skdx-color-text-default)',
4161
+ // The cell composites its fill over the surface at the ramp's opacity: a faint cell
4162
+ // is mostly surface and reads with the default text token, a strong one is the fill
4163
+ // and needs the token meant to sit on it. A fixed token fails one end or the other.
4164
+ fill: fill.opacity >= 0.5
4165
+ ? 'var(--skdx-color-text-on-accent)'
4166
+ : 'var(--skdx-color-text-default)',
3992
4167
  }));
3993
4168
  });
3994
4169
  });
@@ -4068,7 +4243,8 @@ function layoutFunnelChart(input) {
4068
4243
  const max = Math.max(0, ...data.map((d) => d.value));
4069
4244
  const total = data.reduce((s, d) => s + Math.max(0, d.value), 0);
4070
4245
  const first = data[0]?.value ?? 0;
4071
- const offset = legendTop(input);
4246
+ const legend = data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
4247
+ const offset = legendTop(input, legend, width);
4072
4248
  // Along: the axis stages are stacked on; across: the axis a stage's width is measured on.
4073
4249
  const along = horizontal ? width : height - offset;
4074
4250
  const across = horizontal ? height - offset : width;
@@ -4082,7 +4258,6 @@ function layoutFunnelChart(input) {
4082
4258
  const percentFormat = input.percentFormat
4083
4259
  ? makeFormatter(input.percentFormat)
4084
4260
  : (p) => `${Math.round(p * 100)}%`;
4085
- const legend = data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
4086
4261
  const mid = (horizontal ? offset + (height - offset) / 2 : width / 2) - (outside ? across / 6 : 0);
4087
4262
  const stages = data.flatMap((d, i) => {
4088
4263
  const near = widthOf(d.value);
@@ -4091,8 +4266,9 @@ function layoutFunnelChart(input) {
4091
4266
  const p1 = p0 + stageLen - gap;
4092
4267
  const percent = pyramid ? (total > 0 ? d.value / total : 0) : first > 0 ? d.value / first : 0;
4093
4268
  const previous = data[i - 1]?.value;
4269
+ const change = previous !== undefined && previous > 0 ? d.value / previous - 1 : 0;
4094
4270
  const drop = !pyramid && previous !== undefined && previous > 0
4095
- ? ` · −${percentFormat(1 - d.value / previous)} from ${data[i - 1]?.name}`
4271
+ ? ` · ${change < 0 ? '−' : change > 0 ? '+' : ''}${percentFormat(Math.abs(change))} from ${data[i - 1]?.name}`
4096
4272
  : '';
4097
4273
  const title = `${d.name}: ${format(d.value)} (${percentFormat(percent)})${drop}`;
4098
4274
  const d0 = horizontal
@@ -4115,7 +4291,7 @@ function layoutFunnelChart(input) {
4115
4291
  const at = mid + full / 2 + PAD$2;
4116
4292
  return [
4117
4293
  stage,
4118
- label(px(horizontal ? center : at), px(horizontal ? at : center), text, {
4294
+ boundedLabel(px(horizontal ? center : at), px(horizontal ? at : center), text, width, {
4119
4295
  part: 'stage-label',
4120
4296
  anchor: horizontal ? 'middle' : 'start',
4121
4297
  baseline: horizontal ? 'hanging' : 'middle',
@@ -4123,14 +4299,27 @@ function layoutFunnelChart(input) {
4123
4299
  }),
4124
4300
  ];
4125
4301
  }
4302
+ const caption = boundedLabel(px(horizontal ? center : mid), px(horizontal ? mid : center), text, width, {
4303
+ part: 'stage-label',
4304
+ anchor: 'middle',
4305
+ baseline: 'middle',
4306
+ fill: 'var(--skdx-color-text-default)',
4307
+ });
4126
4308
  return [
4127
4309
  stage,
4128
- label(px(horizontal ? center : mid), px(horizontal ? mid : center), text, {
4129
- part: 'stage-label',
4130
- anchor: 'middle',
4131
- baseline: 'middle',
4132
- fill: 'var(--skdx-color-text-on-accent)',
4133
- }),
4310
+ // A stage can be narrower than its label, and arbitrary series colors
4311
+ // cannot guarantee contrast. Keep the bounded label on a token surface.
4312
+ {
4313
+ kind: 'rect',
4314
+ part: 'stage-label-background',
4315
+ x: px(caption.x - (caption.text.length * CHAR + 8) / 2),
4316
+ y: px((horizontal ? mid : center) - 9),
4317
+ width: caption.text.length * CHAR + 8,
4318
+ height: 18,
4319
+ rx: 8,
4320
+ fill: 'var(--skdx-color-surface-default)',
4321
+ },
4322
+ caption,
4134
4323
  ];
4135
4324
  });
4136
4325
  const c = canvas(input, width, height, legend);
@@ -4154,7 +4343,8 @@ function layoutRadialBar(input) {
4154
4343
  return state;
4155
4344
  const format = makeFormatter(input.format);
4156
4345
  const { width, height } = size(input, 300, 300);
4157
- const top = legendTop(input);
4346
+ const legend = data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
4347
+ const top = legendTop(input, legend, width);
4158
4348
  const cx = width / 2;
4159
4349
  const cy = top + (height - top) / 2;
4160
4350
  const outer = Math.max(0, Math.min(width, height - top) / 2 - (input.bands ? 20 : 8));
@@ -4169,7 +4359,6 @@ function layoutRadialBar(input) {
4169
4359
  const rounded = input.rounded ?? true;
4170
4360
  const ratio = (v) => (max > 0 ? Math.max(0, Math.min(1, v / max)) : 0);
4171
4361
  const shapes = [];
4172
- const legend = data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
4173
4362
  // Threshold sectors outside the arcs, e.g. poor / fair / good.
4174
4363
  for (const b of input.bands ?? []) {
4175
4364
  const a0 = start + ratio(b.from) * sweep;
@@ -4765,6 +4954,12 @@ function layoutRangeChart(input) {
4765
4954
  * This file is part of the SkandaDX organization.
4766
4955
  * Licensed under the MIT License. See LICENSE in the repository root.
4767
4956
  */
4957
+ /**
4958
+ * Trigonometry rounded to 1e-6: `Math.cos`/`Math.sin` are implementation-defined, so their
4959
+ * last-bit differences between a Node render and a browser render would otherwise reach the
4960
+ * markup and break hydration.
4961
+ */
4962
+ const q = (v) => Math.round(v * 1e6) / 1e6;
4768
4963
  /** Node labels are drawn by default up to this many nodes. */
4769
4964
  const NETWORK_LABEL_LIMIT = 60;
4770
4965
  /** Per-edge titles and keyboard stops are emitted up to this many edges. */
@@ -4808,13 +5003,14 @@ function layoutNetworkChart(input) {
4808
5003
  const { width, height } = size(input);
4809
5004
  const [rMin, rMax] = input.nodeRange ?? [5, 16];
4810
5005
  const labels = input.labels ?? nodes.length <= NETWORK_LABEL_LIMIT;
5006
+ const groups = [...new Set(input.nodes.map(groupOf).filter((g) => g !== undefined))];
5007
+ const legend = groups.map((g, i) => ({ name: g, color: colorOf(input, i) }));
4811
5008
  const plot = plotBox(width, height, {
4812
- top: legendTop(input) + rMax + 8,
5009
+ top: legendTop(input, legend, width) + rMax + 8,
4813
5010
  right: rMax + (labels ? 48 : 8),
4814
5011
  bottom: rMax + 8,
4815
5012
  left: rMax + 8,
4816
5013
  });
4817
- const groups = [...new Set(input.nodes.map(groupOf).filter((g) => g !== undefined))];
4818
5014
  const colorFor = (n, i) => n.color ?? colorOf(input, n.group !== undefined ? groups.indexOf(n.group) : i);
4819
5015
  const [, vMax] = minMax(nodes.map((n) => n.value));
4820
5016
  const radius = (v) => isFinite(v) && vMax > 0 ? rMin + Math.sqrt(Math.max(0, v) / vMax) * (rMax - rMin) : rMin;
@@ -4834,8 +5030,8 @@ function layoutNetworkChart(input) {
4834
5030
  ys = nodes.map((node) => node.y);
4835
5031
  }
4836
5032
  else if (mode === 'circular' || mode === 'manual' || n < 3) {
4837
- xs = nodes.map((_, i) => Math.cos((2 * Math.PI * i) / n - Math.PI / 2));
4838
- ys = nodes.map((_, i) => Math.sin((2 * Math.PI * i) / n - Math.PI / 2));
5033
+ xs = nodes.map((_, i) => q(Math.cos((2 * Math.PI * i) / n - Math.PI / 2)));
5034
+ ys = nodes.map((_, i) => q(Math.sin((2 * Math.PI * i) / n - Math.PI / 2)));
4839
5035
  }
4840
5036
  else {
4841
5037
  ({ xs, ys } = force(n, edges.map((e) => [at.get(e.source), at.get(e.target)]), input.iterations ?? Math.max(50, Math.min(300, Math.floor(30_000 / n)))));
@@ -4908,7 +5104,7 @@ function layoutNetworkChart(input) {
4908
5104
  item,
4909
5105
  };
4910
5106
  }
4911
- const len = Math.hypot(b.x - a.x, b.y - a.y) || 1;
5107
+ const len = Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y)) || 1;
4912
5108
  const ux = (b.x - a.x) / len;
4913
5109
  const uy = (b.y - a.y) / len;
4914
5110
  if (input.directed) {
@@ -4989,7 +5185,6 @@ function layoutNetworkChart(input) {
4989
5185
  fill: dim(p.node.id) === undefined ? 'var(--skdx-color-text-default)' : CHART_COLORS.grid,
4990
5186
  }));
4991
5187
  }
4992
- const legend = groups.map((g, i) => ({ name: g, color: colorOf(input, i) }));
4993
5188
  const c = canvas(input, width, height, legend);
4994
5189
  return {
4995
5190
  ...c,
@@ -4999,11 +5194,15 @@ function layoutNetworkChart(input) {
4999
5194
  }
5000
5195
  /**
5001
5196
  * Fruchterman–Reingold in the unit square with linear cooling. Deterministic: nodes start on a
5002
- * circle in input order. ponytail: O(n²) per iteration; a Barnes–Hut tree if graphs exceed ~2000 nodes.
5197
+ * circle in input order, the seed is quantised and every step afterwards uses only IEEE-exact
5198
+ * arithmetic (`+ - * /` and `sqrt`), so a server render and a browser render agree bit for bit —
5199
+ * `Math.cos`/`Math.sin`/`Math.hypot` are implementation-defined and their last-bit differences
5200
+ * would otherwise amplify over the iterations into a hydration mismatch.
5201
+ * ponytail: O(n²) per iteration; a Barnes–Hut tree if graphs exceed ~2000 nodes.
5003
5202
  */
5004
5203
  function force(n, edges, iterations) {
5005
- const xs = Array.from({ length: n }, (_, i) => Math.cos((2 * Math.PI * i) / n));
5006
- const ys = Array.from({ length: n }, (_, i) => Math.sin((2 * Math.PI * i) / n));
5204
+ const xs = Array.from({ length: n }, (_, i) => q(Math.cos((2 * Math.PI * i) / n)));
5205
+ const ys = Array.from({ length: n }, (_, i) => q(Math.sin((2 * Math.PI * i) / n)));
5007
5206
  const k = Math.sqrt(4 / n);
5008
5207
  const dx = new Array(n).fill(0);
5009
5208
  const dy = new Array(n).fill(0);
@@ -5032,7 +5231,7 @@ function force(n, edges, iterations) {
5032
5231
  continue;
5033
5232
  const ex = xs[a] - xs[b];
5034
5233
  const ey = ys[a] - ys[b];
5035
- const d = Math.hypot(ex, ey) || 1e-6;
5234
+ const d = Math.sqrt(ex * ex + ey * ey) || 1e-6;
5036
5235
  const f = (d * d) / k / d;
5037
5236
  dx[a] = dx[a] - ex * f;
5038
5237
  dy[a] = dy[a] - ey * f;
@@ -5040,7 +5239,8 @@ function force(n, edges, iterations) {
5040
5239
  dy[b] = dy[b] + ey * f;
5041
5240
  }
5042
5241
  for (let i = 0; i < n; i++) {
5043
- const d = Math.hypot(dx[i], dy[i]) || 1e-6;
5242
+ const d = Math.sqrt(dx[i] * dx[i] + dy[i] * dy[i]) ||
5243
+ 1e-6;
5044
5244
  const step = Math.min(d, t);
5045
5245
  xs[i] = xs[i] + (dx[i] / d) * step;
5046
5246
  ys[i] = ys[i] + (dy[i] / d) * step;
@@ -5119,7 +5319,7 @@ function layoutTimelineChart(input) {
5119
5319
  // A list: the date column, a spine, one marker and label per row.
5120
5320
  const dateW = Math.max(...events.map((e) => dateOf(e.time).length)) * CHAR + 16;
5121
5321
  const plot = plotBox(width, height, {
5122
- top: 8 + legendTop(input),
5322
+ top: 8 + legendTop(input, legend, width),
5123
5323
  right: 8,
5124
5324
  bottom: 8,
5125
5325
  left: dateW + 12,
@@ -5173,7 +5373,7 @@ function layoutTimelineChart(input) {
5173
5373
  // Half a date label at each end, so the first and last ticks stay inside the canvas.
5174
5374
  const dateW = (Math.max(dateOf(t0).length, dateOf(t1).length) * CHAR) / 2 + 4;
5175
5375
  const plot = plotBox(width, height, {
5176
- top: 8 + legendTop(input),
5376
+ top: 8 + legendTop(input, legend, width),
5177
5377
  right: Math.max(16, dateW),
5178
5378
  bottom: 32 + (input.xAxis?.label ? 14 : 0),
5179
5379
  left: Math.max(16, laneW, dateW),
@@ -5267,6 +5467,26 @@ function layoutTimelineChart(input) {
5267
5467
  return { ...c, shapes: [...shapes, ...texts, ...marks, ...c.shapes], issues };
5268
5468
  }
5269
5469
 
5470
+ /*
5471
+ * Copyright (c) 2026 SkandaDX
5472
+ * This file is part of the SkandaDX organization.
5473
+ * Licensed under the MIT License. See LICENSE in the repository root.
5474
+ */
5475
+ /** Shared input/surface rounding; consumer tokens can customize it without changing data geometry. */
5476
+ const CHART_SURFACE_RADIUS = 'var(--skdx-radius-default, var(--skdx-radius-md, 0.5rem))';
5477
+ /** Only rectangular UI surfaces follow control rounding. Data marks retain their geometry/options. */
5478
+ function chartSurfaceRadius(part) {
5479
+ return [
5480
+ 'tooltip',
5481
+ 'skeleton',
5482
+ 'label-background',
5483
+ 'stage-label-background',
5484
+ 'brush-handle',
5485
+ ].includes(part)
5486
+ ? CHART_SURFACE_RADIUS
5487
+ : undefined;
5488
+ }
5489
+
5270
5490
  /*
5271
5491
  * Copyright (c) 2026 SkandaDX
5272
5492
  * This file is part of the SkandaDX organization.
@@ -5829,7 +6049,7 @@ function shapeMarkup(s) {
5829
6049
  case 'circle':
5830
6050
  return `<circle${attrs([part, ['cx', s.cx], ['cy', s.cy], ['r', s.r], ['fill', s.fill], ['stroke', s.stroke]])}>${title}</circle>`;
5831
6051
  case 'text':
5832
- return `<text${attrs([part, ['x', s.x], ['y', s.y], ['text-anchor', s.anchor], ['dominant-baseline', s.baseline], ['fill', s.fill], ['font-weight', s.weight], ['transform', s.rotate ? `rotate(${s.rotate} ${s.x} ${s.y})` : undefined]])}>${esc(s.text)}</text>`;
6052
+ return `<text${attrs([part, ['aria-label', s.title], ['x', s.x], ['y', s.y], ['text-anchor', s.anchor], ['dominant-baseline', s.baseline], ['fill', s.fill], ['font-weight', s.weight], ['transform', s.rotate ? `rotate(${s.rotate} ${s.x} ${s.y})` : undefined]])}>${esc(s.text)}</text>`;
5833
6053
  }
5834
6054
  }
5835
6055
  /** The `<defs>` block for a layout's gradients, or an empty string. */
@@ -5917,6 +6137,11 @@ function seriesCsv(series, categories = []) {
5917
6137
  */
5918
6138
  /** A node's size: its own value, else the sum of its children's. */
5919
6139
  const nodeValue = (n) => isFinite(n.value) ? n.value : (n.children ?? []).reduce((s, c) => s + nodeValue(c), 0);
6140
+ /**
6141
+ * Whether this node or anything under it carries a value. A branch in a tree where nobody sets
6142
+ * `value` sums to 0, which is not data and must not be printed as one.
6143
+ */
6144
+ const hasValue = (n) => isFinite(n.value) || (n.children ?? []).some(hasValue);
5920
6145
  /** Levels below the given nodes (0 for none). */
5921
6146
  const depthOf = (nodes) => nodes.length === 0 ? 0 : 1 + Math.max(...nodes.map((n) => depthOf(n.children ?? [])));
5922
6147
  /** A node's identity: its id, else its name. */
@@ -6376,13 +6601,13 @@ function layoutTreemap(input) {
6376
6601
  const formatValue = makeFormatter(input.format);
6377
6602
  const { width, height } = size(input);
6378
6603
  const crumbs = input.breadcrumbs && path.length > 0;
6379
- const top = legendTop(input) + (crumbs ? 20 : 0);
6604
+ const legend = nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) }));
6605
+ const top = legendTop(input, legend, width) + (crumbs ? 20 : 0);
6380
6606
  const pad = Math.max(0, input.padding ?? 2);
6381
6607
  const sort = input.sort ?? 'desc';
6382
6608
  const tiling = input.tiling ?? 'squarify';
6383
6609
  const showLabels = input.labels ?? true;
6384
6610
  const shapes = [];
6385
- const legend = nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) }));
6386
6611
  // Leaves are colored by value when a scale is given; branches keep the branch color.
6387
6612
  const leaves = [];
6388
6613
  const collect = (list) => {
@@ -6430,11 +6655,14 @@ function layoutTreemap(input) {
6430
6655
  });
6431
6656
  if (branch)
6432
6657
  tile(t.node.children, tx + pad, ty + pad, Math.max(0, tw - 2 * pad), Math.max(0, th - 2 * pad), t.color, depth + 1);
6433
- else if (showLabels && tw > 40 && th > 20)
6434
- shapes.push(label(px(tx + 6), px(ty + 6), t.node.name, {
6658
+ else if (showLabels && tw > 40 && th > 20) {
6659
+ const capacity = Math.max(1, Math.floor((tw - 12) / CHAR));
6660
+ const text = t.node.name.length > capacity ? `${t.node.name.slice(0, capacity - 1)}…` : t.node.name;
6661
+ shapes.push(label(px(tx + 6), px(ty + 6), text, {
6435
6662
  baseline: 'hanging',
6436
6663
  fill: 'var(--skdx-color-text-on-accent)',
6437
6664
  }));
6665
+ }
6438
6666
  };
6439
6667
  if (tiling === 'squarify')
6440
6668
  squarify(tiles, x, y, w, h, place);
@@ -6443,9 +6671,9 @@ function layoutTreemap(input) {
6443
6671
  };
6444
6672
  tile(nodes, 0, top, width, height - top, undefined, 0);
6445
6673
  if (crumbs)
6446
- shapes.push(...breadcrumbShapes(path, 8, legendTop(input) + 10));
6674
+ shapes.push(...breadcrumbShapes(path, 8, legendTop(input, legend, width) + 10));
6447
6675
  const c = canvas(input, width, height, legend);
6448
- return { ...c, shapes: [...shapes, ...c.shapes] };
6676
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...c.shapes]) };
6449
6677
  }
6450
6678
  /** The drill path as clickable crumbs (each carries the node's id) separated by `›`, from `x`. */
6451
6679
  function breadcrumbShapes(path, x, y) {
@@ -6498,6 +6726,7 @@ function layoutCandlestick(input) {
6498
6726
  const UP = input.upColor ?? CHART_COLORS.positive;
6499
6727
  const DOWN = input.downColor ?? CHART_COLORS.negative;
6500
6728
  const priceFormat = makeFormatter(input.format);
6729
+ const volumeFormat = makeFormatter(input.volumeFormat ?? { style: 'integer' });
6501
6730
  const count = all.length;
6502
6731
  const start = Math.max(0, Math.min(count - 2, Math.floor(input.zoom?.start ?? 0)));
6503
6732
  const end = input.zoom ? Math.max(start + 2, Math.min(count, Math.ceil(input.zoom.end))) : count;
@@ -6507,19 +6736,12 @@ function layoutCandlestick(input) {
6507
6736
  const { width, height } = size(input);
6508
6737
  const brushRoom = input.brush ? BRUSH_HEIGHT + BRUSH_GAP : 0;
6509
6738
  const selector = input.rangeSelector ?? [];
6510
- const outer = plotBox(width, height, {
6511
- ...MARGIN$1,
6512
- top: MARGIN$1.top + legendTop(input) + (selector.length ? SELECTOR_HEIGHT : 0),
6513
- bottom: MARGIN$1.bottom + brushRoom,
6514
- });
6515
6739
  // `compare` plots the change from the first visible close: y values (and the axis format)
6516
6740
  // become fractions while tooltips keep the prices and add the percentage.
6517
6741
  const base = input.compare ? data[0].close : undefined;
6518
6742
  const cmp = (v) => (base !== undefined && base !== 0 ? v / base - 1 : v);
6519
6743
  const pctFormat = base !== undefined ? { style: 'percent', digits: 1 } : undefined;
6520
6744
  const pct = makeFormatter({ style: 'percent', digits: 1 });
6521
- const volumeH = hasVolume ? outer.height * VOLUME_SHARE : 0;
6522
- const plot = { ...outer, height: Math.max(0, outer.height - volumeH - (hasVolume ? 8 : 0)) };
6523
6745
  // Moving averages run over every close, so zooming never shortens the window; the visible
6524
6746
  // slice is drawn and read in the crosshair.
6525
6747
  const closes = all.map((c) => c.close);
@@ -6535,12 +6757,30 @@ function layoutCandlestick(input) {
6535
6757
  hidden: isHidden(input, name),
6536
6758
  };
6537
6759
  });
6538
- const y = axis(extent([
6760
+ // The price extent is known before the plot box, so the left margin can hold the widest tick
6761
+ // label: a currency price such as "$180.00" overflows the default 48.
6762
+ const prices = extent([
6539
6763
  ...data.flatMap((c) => [cmp(c.low), cmp(c.high)]),
6540
6764
  ...overlays
6541
6765
  .filter((o) => !o.hidden)
6542
6766
  .flatMap((o) => o.values.map((v) => (isFinite(v) ? cmp(v) : v))),
6543
- ], false), [plot.y + plot.height, plot.y], { scale: input.scale === 'log' ? 'log' : 'linear', format: pctFormat }, input.format);
6767
+ ], false);
6768
+ const legend = [
6769
+ { name: 'Up', color: UP },
6770
+ { name: 'Down', color: DOWN },
6771
+ ...overlays.map((o) => ({ name: o.name, color: o.color })),
6772
+ ];
6773
+ const outer = plotBox(width, height, {
6774
+ ...MARGIN$1,
6775
+ top: MARGIN$1.top + legendTop(input, legend, width) + (selector.length ? SELECTOR_HEIGHT : 0),
6776
+ bottom: MARGIN$1.bottom + brushRoom,
6777
+ left: input.scale === 'log'
6778
+ ? MARGIN$1.left
6779
+ : valueAxisRoom(prices, makeFormatter(pctFormat ?? input.yAxis?.format ?? input.format)),
6780
+ });
6781
+ const volumeH = hasVolume ? outer.height * VOLUME_SHARE : 0;
6782
+ const plot = { ...outer, height: Math.max(0, outer.height - volumeH - (hasVolume ? 8 : 0)) };
6783
+ const y = axis(prices, [plot.y + plot.height, plot.y], { scale: input.scale === 'log' ? 'log' : 'linear', format: pctFormat ?? input.yAxis?.format }, input.format);
6544
6784
  // A time axis places candles by their label's time, so missing sessions leave gaps; the bar
6545
6785
  // width is 80% of the smallest gap. Otherwise every candle takes an equal slot.
6546
6786
  const times = data.map((c) => categoryValue(c.label));
@@ -6568,11 +6808,6 @@ function layoutCandlestick(input) {
6568
6808
  .map((t) => ({ pos: px(tx(t)), label: categoryLabel(t, tickFormat) }));
6569
6809
  }
6570
6810
  const labels = data.map((c) => categoryLabel(c.label, input.xAxis?.format));
6571
- const legend = [
6572
- { name: 'Up', color: UP },
6573
- { name: 'Down', color: DOWN },
6574
- ...overlays.map((o) => ({ name: o.name, color: o.color })),
6575
- ];
6576
6811
  const c0 = canvas(input, width, height, legend);
6577
6812
  const frame = {
6578
6813
  ...c0,
@@ -6616,7 +6851,7 @@ function layoutCandlestick(input) {
6616
6851
  const color = up ? UP : DOWN;
6617
6852
  const cx = px(x.center(i));
6618
6853
  const change = base !== undefined ? ` (${pct(cmp(c.close))})` : '';
6619
- const title = `${labels[i]}: O ${priceFormat(c.open)} H ${priceFormat(c.high)} L ${priceFormat(c.low)} C ${priceFormat(c.close)}${change}${isFinite(c.volume) ? ` V ${priceFormat(c.volume)}` : ''}`;
6854
+ const title = `${labels[i]}: O ${priceFormat(c.open)} H ${priceFormat(c.high)} L ${priceFormat(c.low)} C ${priceFormat(c.close)}${change}${isFinite(c.volume) ? ` V ${volumeFormat(c.volume)}` : ''}`;
6620
6855
  const item = {
6621
6856
  label: title,
6622
6857
  id: c.id,
@@ -6630,7 +6865,7 @@ function layoutCandlestick(input) {
6630
6865
  `High: ${priceFormat(c.high)}`,
6631
6866
  `Low: ${priceFormat(c.low)}`,
6632
6867
  `Close: ${priceFormat(c.close)}${change}`,
6633
- ...(isFinite(c.volume) ? [`Volume: ${priceFormat(c.volume)}`] : []),
6868
+ ...(isFinite(c.volume) ? [`Volume: ${volumeFormat(c.volume)}`] : []),
6634
6869
  ...overlays
6635
6870
  .filter((o) => !o.hidden)
6636
6871
  .map((o) => `${o.name}: ${isFinite(o.values[i]) ? priceFormat(o.values[i]) : '—'}`),
@@ -6709,7 +6944,7 @@ function layoutCandlestick(input) {
6709
6944
  height: px(Math.max(h, 0)),
6710
6945
  fill: c.close >= c.open ? UP : DOWN,
6711
6946
  opacity: 0.4,
6712
- title: `${labels[i]}: volume ${priceFormat(c.volume)}`,
6947
+ title: `${labels[i]}: volume ${volumeFormat(c.volume)}`,
6713
6948
  });
6714
6949
  });
6715
6950
  }
@@ -6808,7 +7043,7 @@ function layoutCandlestick(input) {
6808
7043
  width: 6,
6809
7044
  height: box.height - 16,
6810
7045
  fill: CHART_COLORS.axis,
6811
- rx: 2,
7046
+ rx: 8,
6812
7047
  }, {
6813
7048
  kind: 'rect',
6814
7049
  part: 'brush-handle',
@@ -6817,7 +7052,7 @@ function layoutCandlestick(input) {
6817
7052
  width: 6,
6818
7053
  height: box.height - 16,
6819
7054
  fill: CHART_COLORS.axis,
6820
- rx: 2,
7055
+ rx: 8,
6821
7056
  });
6822
7057
  hit.brush = { box, window };
6823
7058
  }
@@ -6909,19 +7144,20 @@ function layoutWaterfall(input) {
6909
7144
  flat: true,
6910
7145
  });
6911
7146
  const widest = horizontal ? Math.max(0, ...steps.map((s) => s.name.length)) * CHAR + 16 : 0;
7147
+ const legend = [
7148
+ { name: 'Increase', color: up },
7149
+ { name: 'Decrease', color: down },
7150
+ ];
6912
7151
  const plot = plotBox(width, height, {
6913
7152
  ...MARGIN$1,
6914
- top: MARGIN$1.top + legendTop(input),
7153
+ top: MARGIN$1.top + legendTop(input, legend, width),
6915
7154
  left: Math.max(MARGIN$1.left, widest),
6916
7155
  right: input.labels && horizontal ? 56 : MARGIN$1.right,
6917
7156
  });
6918
7157
  const totals = steps.flatMap((s) => [s.from, s.to]);
6919
7158
  const value = axis([Math.min(0, ...totals), Math.max(0, ...totals)], horizontal ? [plot.x, plot.x + plot.width] : [plot.y + plot.height, plot.y], {}, input.format);
6920
7159
  const band = bandScale(steps.length, horizontal ? [plot.y, plot.y + plot.height] : [plot.x, plot.x + plot.width]);
6921
- const c = canvas(input, width, height, [
6922
- { name: 'Increase', color: up },
6923
- { name: 'Decrease', color: down },
6924
- ]);
7160
+ const c = canvas(input, width, height, legend);
6925
7161
  const categoryTicks = steps.map((s, i) => ({ pos: px(band.center(i)), label: s.name }));
6926
7162
  const shapes = frameShapes({
6927
7163
  ...c,
@@ -7133,8 +7369,9 @@ function layoutGantt(input) {
7133
7369
  : '';
7134
7370
  const widest = Math.max(0, ...rowNames.map((n) => n.length + rowSuffix(n).length)) * CHAR +
7135
7371
  Math.max(0, ...rowNames.map((n) => (rowDepth.get(n) ?? 0) * 12));
7372
+ const legend = input.tasks.map((t, i) => ({ name: t.name, color: colorOf(input, i, t.color) }));
7136
7373
  const plot = plotBox(width, height, {
7137
- top: 8 + legendTop(input),
7374
+ top: 8 + legendTop(input, legend, width),
7138
7375
  right: 16,
7139
7376
  bottom: 32 + (input.xAxis?.label ? 14 : 0),
7140
7377
  left: Math.min(width / 2, Math.max(64, widest + 16)),
@@ -7167,7 +7404,10 @@ function layoutGantt(input) {
7167
7404
  y2: plot.y + plot.height,
7168
7405
  stroke: CHART_COLORS.grid,
7169
7406
  });
7170
- shapes.push(label(t.pos, plot.y + plot.height + 20, t.label, { part: 'x-label', anchor: 'middle' }));
7407
+ shapes.push(label(t.pos, plot.y + plot.height + 20, t.label, {
7408
+ part: 'x-label',
7409
+ anchor: tickAnchor(t.pos, t.label, width),
7410
+ }));
7171
7411
  }
7172
7412
  rowNames.forEach((name, i) => {
7173
7413
  const owner = visible.find((t) => (t.group ?? t.name) === name);
@@ -7363,7 +7603,7 @@ function layoutGantt(input) {
7363
7603
  });
7364
7604
  }
7365
7605
  shapes.push(...overlays, ...annotationShapes(input.annotations, scales));
7366
- const c = canvas(input, width, height, input.tasks.map((t, i) => ({ name: t.name, color: colorOf(input, i, t.color) })));
7606
+ const c = canvas(input, width, height, legend);
7367
7607
  return {
7368
7608
  ...c,
7369
7609
  shapes: [...shapes, ...c.shapes],
@@ -7528,9 +7768,10 @@ function layoutBoxplot(input) {
7528
7768
  const horizontal = input.orientation === 'horizontal';
7529
7769
  const groups = grouped ? categories : input.series.map((s) => s.name);
7530
7770
  const widest = Math.max(0, ...groups.map((n) => n.length)) * CHAR;
7771
+ const legend = input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) }));
7531
7772
  const plot = plotBox(width, height, {
7532
7773
  ...MARGIN$1,
7533
- top: MARGIN$1.top + legendTop(input) + (input.yAxis?.label ? 12 : 0),
7774
+ top: MARGIN$1.top + legendTop(input, legend, width) + (input.yAxis?.label ? 12 : 0),
7534
7775
  left: horizontal ? Math.max(MARGIN$1.left, widest + 16) : MARGIN$1.left,
7535
7776
  bottom: MARGIN$1.bottom + (horizontal && input.yAxis?.label ? 14 : 0),
7536
7777
  });
@@ -7548,7 +7789,7 @@ function layoutBoxplot(input) {
7548
7789
  const slots = grouped ? [...new Set(shown.map((c) => c.si))] : [];
7549
7790
  const slotW = grouped ? band.bandwidth / Math.max(1, slots.length) : band.bandwidth;
7550
7791
  const center = (c) => grouped ? band.start(c.ci) + (slots.indexOf(c.si) + 0.5) * slotW : band.center(c.si);
7551
- const c = canvas(input, width, height, input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) })));
7792
+ const c = canvas(input, width, height, legend);
7552
7793
  const categoryTicks = groups.map((g, i) => ({ pos: px(band.center(i)), label: g }));
7553
7794
  const shapes = frameShapes({
7554
7795
  ...c,
@@ -7699,7 +7940,8 @@ function layoutSunburst(input) {
7699
7940
  // Crumbs sit in the hole when there is one, else in a row above the rings.
7700
7941
  const crumbs = input.breadcrumbs && path.length > 0;
7701
7942
  const crumbsAbove = crumbs && inner === 0;
7702
- const top = legendTop(input) + (crumbsAbove ? 20 : 0);
7943
+ const legend = nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) }));
7944
+ const top = legendTop(input, legend, width) + (crumbsAbove ? 20 : 0);
7703
7945
  const cx = width / 2;
7704
7946
  const cy = top + (height - top) / 2;
7705
7947
  const radius = Math.max(0, Math.min(width, height - top) / 2 - 8);
@@ -7779,11 +8021,11 @@ function layoutSunburst(input) {
7779
8021
  if (crumbs) {
7780
8022
  const w = path.reduce((s, n) => s + n.name.length * CHAR + 6, 0) + (path.length - 1) * (CHAR + 6);
7781
8023
  texts.push(...(crumbsAbove
7782
- ? breadcrumbShapes(path, 8, legendTop(input) + 10)
8024
+ ? breadcrumbShapes(path, 8, legendTop(input, legend, width) + 10)
7783
8025
  : breadcrumbShapes(path, cx - w / 2, input.centerLabel ? cy + 8 : cy)));
7784
8026
  }
7785
- const c = canvas(input, width, height, nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) })));
7786
- return { ...c, shapes: [...shapes, ...texts, ...c.shapes] };
8027
+ const c = canvas(input, width, height, legend);
8028
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...texts, ...c.shapes]) };
7787
8029
  }
7788
8030
 
7789
8031
  /*
@@ -7847,12 +8089,6 @@ function layoutChord(input) {
7847
8089
  return { ...state, issues };
7848
8090
  const formatValue = makeFormatter(input.format);
7849
8091
  const { width, height } = size(input, 300, 300);
7850
- const top = legendTop(input);
7851
- const cx = width / 2;
7852
- const cy = top + (height - top) / 2;
7853
- const r = Math.max(0, Math.min(width, height - top) / 2 - 36);
7854
- const r0 = Math.max(0, r - RING);
7855
- const PAD = ((input.padding ?? 2.3) * Math.PI) / 180;
7856
8092
  // Groups keep their input order (and color) but can be arranged by total flow.
7857
8093
  const cellRaw = (i, j) => {
7858
8094
  const v = given2[i]?.[j];
@@ -7864,6 +8100,17 @@ function layoutChord(input) {
7864
8100
  order.sort((a, b) => input.sort === 'asc' ? totalOf(a) - totalOf(b) : totalOf(b) - totalOf(a));
7865
8101
  const names = order.map((i) => given[i]);
7866
8102
  const colorAt = (k) => colorOf(input, order[k]);
8103
+ const legend = names.map((name, i) => ({ name, color: colorAt(i) }));
8104
+ const top = legendTop(input, legend, width);
8105
+ const cx = width / 2;
8106
+ const cy = top + (height - top) / 2;
8107
+ // Group names sit outside the rim, so the circle keeps room for the widest one on either side;
8108
+ // without it the left-hand label runs off the svg.
8109
+ const nameGap = input.ticks ? 8 + TICK + 14 : 8;
8110
+ const nameRoom = nameGap + Math.max(0, ...given.map((s) => s.length)) * CHAR + 4;
8111
+ const r = Math.max(0, Math.min(width / 2 - nameRoom, (height - top) / 2 - 36));
8112
+ const r0 = Math.max(0, r - RING);
8113
+ const PAD = ((input.padding ?? 2.3) * Math.PI) / 180;
7867
8114
  const n = names.length;
7868
8115
  const selectedId = selectedIds(input.selected)[0];
7869
8116
  const selected = selectedId === undefined ? -1 : names.indexOf(String(selectedId));
@@ -7943,7 +8190,7 @@ function layoutChord(input) {
7943
8190
  const mid = (g.a0 + g.a1) / 2;
7944
8191
  const sin = Math.sin(mid);
7945
8192
  // Ticks sit on the rim; the group name moves out past their labels.
7946
- const nameR = r + (input.ticks ? 8 + TICK + 14 : 8);
8193
+ const nameR = r + nameGap;
7947
8194
  shapes.push(label(px(cx + nameR * sin), px(cy - nameR * Math.cos(mid)), names[i], {
7948
8195
  anchor: sin > 0.1 ? 'start' : sin < -0.1 ? 'end' : 'middle',
7949
8196
  baseline: 'middle',
@@ -7981,7 +8228,7 @@ function layoutChord(input) {
7981
8228
  }));
7982
8229
  });
7983
8230
  });
7984
- const c = canvas(input, width, height, names.map((name, i) => ({ name, color: colorAt(i) })));
8231
+ const c = canvas(input, width, height, legend);
7985
8232
  return { ...c, shapes: [...shapes, ...c.shapes], issues };
7986
8233
  }
7987
8234
 
@@ -8232,16 +8479,17 @@ function layoutMarimekkoChart(input) {
8232
8479
  const format = makeFormatter(input.format);
8233
8480
  const totalFormat = makeFormatter(input.xAxis?.format ?? input.format);
8234
8481
  const { width, height } = size(input);
8482
+ const legend = segments.map((s) => ({ name: s.name, color: s.color }));
8235
8483
  const plot = plotBox(width, height, {
8236
8484
  ...MARGIN$1,
8237
- top: MARGIN$1.top + legendTop(input),
8485
+ top: MARGIN$1.top + legendTop(input, legend, width),
8238
8486
  bottom: MARGIN$1.bottom + (input.xAxis?.label ? 14 : 0) + (input.xAxis?.format ? 14 : 0),
8239
8487
  });
8240
8488
  const gap = Math.max(0, Math.min(0.2, input.gap ?? 0.01)) * plot.width;
8241
8489
  const gaps = Math.max(0, columns.length - 1);
8242
8490
  const usable = Math.max(0, plot.width - gap * gaps);
8243
8491
  const y = axis([0, 1], [plot.y + plot.height, plot.y], { format: { style: 'percent' } });
8244
- const c = canvas(input, width, height, segments.map((s) => ({ name: s.name, color: s.color })));
8492
+ const c = canvas(input, width, height, legend);
8245
8493
  // Column positions along x, each as wide as its share of the grand total.
8246
8494
  let x = plot.x;
8247
8495
  const placed = columns.map((col) => {
@@ -8303,7 +8551,7 @@ function layoutMarimekkoChart(input) {
8303
8551
  }));
8304
8552
  });
8305
8553
  }
8306
- return { ...c, shapes: [...shapes, ...cells, ...texts, ...c.shapes], issues };
8554
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...cells, ...texts, ...c.shapes]), issues };
8307
8555
  }
8308
8556
 
8309
8557
  /*
@@ -8331,8 +8579,9 @@ function layoutParallelCoordinates(input) {
8331
8579
  const first = dims[0];
8332
8580
  const last = dims[dims.length - 1];
8333
8581
  const pad = (d) => ((d.label ?? d.key).length * CHAR) / 2 + 8;
8582
+ const legend = groups.map((g, i) => ({ name: g, color: colorOf(input, i) }));
8334
8583
  const plot = plotBox(width, height, {
8335
- top: 28 + legendTop(input),
8584
+ top: 28 + legendTop(input, legend, width),
8336
8585
  right: Math.max(16, pad(last)),
8337
8586
  bottom: 16,
8338
8587
  left: Math.max(48, pad(first)),
@@ -8435,7 +8684,7 @@ function layoutParallelCoordinates(input) {
8435
8684
  : undefined,
8436
8685
  };
8437
8686
  });
8438
- const c = canvas(input, width, height, groups.map((g, i) => ({ name: g, color: colorOf(input, i) })));
8687
+ const c = canvas(input, width, height, legend);
8439
8688
  return { ...c, shapes: [...shapes, ...lines, ...c.shapes], issues };
8440
8689
  }
8441
8690
 
@@ -8512,6 +8761,10 @@ function layoutDumbbellChart(input) {
8512
8761
  const vertical = input.orientation === 'vertical';
8513
8762
  const startLabel = input.startLabel ?? 'Start';
8514
8763
  const endLabel = input.endLabel ?? 'End';
8764
+ // The legend toggles the two ends like any other series: a hidden end drops its markers, its
8765
+ // value labels and the connector that would dangle, and leaves the value axis to the rest.
8766
+ const hideStart = isHidden(input, startLabel);
8767
+ const hideEnd = isHidden(input, endLabel);
8515
8768
  const sorted = [...data];
8516
8769
  if (input.sort) {
8517
8770
  const key = (d) => input.sort === 'change'
@@ -8522,21 +8775,22 @@ function layoutDumbbellChart(input) {
8522
8775
  sorted.sort((a, b) => (input.sort === 'asc' ? key(a) - key(b) : key(b) - key(a)));
8523
8776
  }
8524
8777
  const widest = Math.max(0, ...sorted.map((d) => d.name.length)) * CHAR;
8778
+ const startColor = colorOf(input, 0);
8779
+ const endColor = colorOf(input, 1);
8780
+ const legend = [
8781
+ { name: startLabel, color: startColor },
8782
+ { name: endLabel, color: endColor },
8783
+ ];
8525
8784
  const plot = plotBox(width, height, {
8526
8785
  ...MARGIN$1,
8527
- top: MARGIN$1.top + legendTop(input) + (vertical && input.xAxis?.label ? 12 : 0),
8786
+ top: MARGIN$1.top + legendTop(input, legend, width) + (vertical && input.xAxis?.label ? 12 : 0),
8528
8787
  right: input.labels && !vertical ? 56 : MARGIN$1.right,
8529
8788
  bottom: MARGIN$1.bottom + (!vertical && input.xAxis?.label ? 14 : 0),
8530
8789
  left: vertical ? MARGIN$1.left : Math.max(MARGIN$1.left, widest + 16),
8531
8790
  });
8532
- const value = axis(extent(sorted.flatMap((d) => [d.start, d.end]), false), vertical ? [plot.y + plot.height, plot.y] : [plot.x, plot.x + plot.width], input.xAxis ?? {}, input.format);
8791
+ const value = axis(extent(sorted.flatMap((d) => [hideStart ? undefined : d.start, hideEnd ? undefined : d.end]), false), vertical ? [plot.y + plot.height, plot.y] : [plot.x, plot.x + plot.width], input.xAxis ?? {}, input.format);
8533
8792
  const band = bandScale(sorted.length, vertical ? [plot.x, plot.x + plot.width] : [plot.y, plot.y + plot.height]);
8534
- const startColor = colorOf(input, 0);
8535
- const endColor = colorOf(input, 1);
8536
- const c = canvas(input, width, height, [
8537
- { name: startLabel, color: startColor },
8538
- { name: endLabel, color: endColor },
8539
- ]);
8793
+ const c = canvas(input, width, height, legend);
8540
8794
  const categoryTicks = sorted.map((d, i) => ({ pos: px(band.center(i)), label: d.name }));
8541
8795
  const shapes = frameShapes({
8542
8796
  ...c,
@@ -8575,7 +8829,7 @@ function layoutDumbbellChart(input) {
8575
8829
  index: i,
8576
8830
  value: isFinite(d.end) ? d.end : undefined,
8577
8831
  };
8578
- if (isFinite(d.start) && isFinite(d.end)) {
8832
+ if (!hideStart && !hideEnd && isFinite(d.start) && isFinite(d.end)) {
8579
8833
  const a = at(d.start, mid);
8580
8834
  const b = at(d.end, mid);
8581
8835
  connectors.push({
@@ -8591,11 +8845,11 @@ function layoutDumbbellChart(input) {
8591
8845
  item,
8592
8846
  });
8593
8847
  }
8594
- for (const [v, part, name, fill] of [
8595
- [d.start, 'start', startLabel, startColor],
8596
- [d.end, 'end', endLabel, endColor],
8848
+ for (const [v, part, name, fill, hidden] of [
8849
+ [d.start, 'start', startLabel, startColor, hideStart],
8850
+ [d.end, 'end', endLabel, endColor, hideEnd],
8597
8851
  ]) {
8598
- if (!isFinite(v))
8852
+ if (hidden || !isFinite(v))
8599
8853
  continue;
8600
8854
  const p = at(v, mid);
8601
8855
  const text = `${d.name} · ${name}: ${format(v)}`;
@@ -8801,7 +9055,7 @@ function layoutTreeChart(input) {
8801
9055
  });
8802
9056
  }
8803
9057
  const value = nodeValue(p.node);
8804
- const title = `${p.node.name}${p.node.subtitle ? ` · ${p.node.subtitle}` : ''}${isFinite(p.node.value) || (p.node.children?.length ?? 0) > 0 ? `: ${format(value)}` : ''}${p.collapsed ? ' (collapsed)' : ''}`;
9058
+ const title = `${p.node.name}${p.node.subtitle ? ` · ${p.node.subtitle}` : ''}${hasValue(p.node) ? `: ${format(value)}` : ''}${p.collapsed ? ' (collapsed)' : ''}`;
8805
9059
  const item = { label: title, id: nodeId(p.node), name: p.node.name, value };
8806
9060
  if (box) {
8807
9061
  marks.push({
@@ -8877,7 +9131,10 @@ function layoutTreeChart(input) {
8877
9131
  ]
8878
9132
  : [];
8879
9133
  const c = canvas(input, width, height);
8880
- return { ...c, shapes: [...links, ...marks, ...texts, ...crumbs, ...c.shapes] };
9134
+ return {
9135
+ ...c,
9136
+ shapes: withLabelSurfaces([...links, ...marks, ...texts, ...crumbs, ...c.shapes]),
9137
+ };
8881
9138
  }
8882
9139
  /** An organization chart: the tree with boxes and elbow links. */
8883
9140
  const layoutOrganizationChart = (input) => layoutTreeChart({ ...input, nodeShape: 'box', linkStyle: 'elbow' });
@@ -8942,13 +9199,21 @@ function packCircles(radii, padding) {
8942
9199
  function layoutCirclePacking(input) {
8943
9200
  const issues = [];
8944
9201
  const { nodes: roots } = drill(input.data, input.rootId);
8945
- const nodes = cutDepth(roots, input.maxDepth).filter((n) => !isHidden(input, n.name));
9202
+ // The legend lists every top-level branch, hidden ones included, so a hide can be undone; and
9203
+ // a branch keeps the colour of its place in that full list whatever is hidden.
9204
+ const branches = cutDepth(roots, input.maxDepth);
9205
+ const branchColor = new Map(branches.map((n, i) => [nodeId(n), colorOf(input, i, n.color)]));
9206
+ const nodes = branches.filter((n) => !isHidden(input, n.name));
8946
9207
  const state = stateCanvas(input, !nodes.some((n) => nodeValue(n) > 0), 300, 300);
8947
9208
  if (state)
8948
9209
  return { ...state, issues };
8949
9210
  const format = makeFormatter(input.format);
8950
9211
  const { width, height } = size(input, 300, 300);
8951
- const top = legendTop(input);
9212
+ const legend = branches.map((n) => ({
9213
+ name: n.name,
9214
+ color: branchColor.get(nodeId(n)),
9215
+ }));
9216
+ const top = legendTop(input, legend, width);
8952
9217
  const pad = input.padding ?? 2;
8953
9218
  const labels = input.labels ?? true;
8954
9219
  const leafValues = [];
@@ -8987,7 +9252,7 @@ function layoutCirclePacking(input) {
8987
9252
  const r = Math.max(0, c.r * k - pad / 2);
8988
9253
  const value = nodeValue(n);
8989
9254
  const branch = (n.children?.length ?? 0) > 0;
8990
- const color = n.color ?? inherited ?? colorOf(input, i);
9255
+ const color = n.color ?? inherited ?? branchColor.get(nodeId(n)) ?? colorOf(input, i);
8991
9256
  const fill = branch ? color : input.colorScale ? (colors.color(value)?.fill ?? color) : color;
8992
9257
  const title = `${n.name}: ${format(value)}`;
8993
9258
  shapes.push({
@@ -9025,8 +9290,8 @@ function layoutCirclePacking(input) {
9025
9290
  };
9026
9291
  const radius = Math.max(0, Math.min(width, height - top) / 2 - 8);
9027
9292
  pack(nodes, width / 2, top + (height - top) / 2, radius, undefined);
9028
- const c = canvas(input, width, height, nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) })));
9029
- return { ...c, shapes: [...shapes, ...texts, ...c.shapes], issues };
9293
+ const c = canvas(input, width, height, legend);
9294
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...texts, ...c.shapes]), issues };
9030
9295
  }
9031
9296
 
9032
9297
  /*
@@ -9049,7 +9314,8 @@ function layoutIcicle(input) {
9049
9314
  const { width, height } = size(input);
9050
9315
  const horizontal = input.orientation === 'horizontal';
9051
9316
  const crumbs = input.breadcrumbs && path.length > 0;
9052
- const top = legendTop(input) + (crumbs ? 20 : 0);
9317
+ const legend = nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) }));
9318
+ const top = legendTop(input, legend, width) + (crumbs ? 20 : 0);
9053
9319
  const plot = plotBox(width, height, { top, right: 0, bottom: 0, left: 0 });
9054
9320
  const gap = input.padding ?? 1;
9055
9321
  const labels = input.labels ?? true;
@@ -9117,9 +9383,9 @@ function layoutIcicle(input) {
9117
9383
  };
9118
9384
  walk(nodes, 0, horizontal ? plot.y : plot.x, horizontal ? plot.height : plot.width, undefined);
9119
9385
  if (crumbs)
9120
- shapes.push(...breadcrumbShapes(path, 8, legendTop(input) + 10));
9121
- const c = canvas(input, width, height, nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) })));
9122
- return { ...c, shapes: [...shapes, ...texts, ...c.shapes], issues };
9386
+ shapes.push(...breadcrumbShapes(path, 8, legendTop(input, legend, width) + 10));
9387
+ const c = canvas(input, width, height, legend);
9388
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...texts, ...c.shapes]), issues };
9123
9389
  }
9124
9390
 
9125
9391
  /*
@@ -9177,16 +9443,17 @@ function layoutBumpChart(input) {
9177
9443
  });
9178
9444
  const showLabels = input.labels ?? true;
9179
9445
  const widest = showLabels ? Math.max(0, ...drawn.map((s) => s.name.length)) * CHAR + 12 : 0;
9446
+ const legend = input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) }));
9180
9447
  const plot = plotBox(width, height, {
9181
9448
  ...MARGIN$1,
9182
- top: MARGIN$1.top + legendTop(input),
9449
+ top: MARGIN$1.top + legendTop(input, legend, width),
9183
9450
  right: Math.max(MARGIN$1.right, widest),
9184
9451
  bottom: MARGIN$1.bottom + (input.xAxis?.label ? 14 : 0),
9185
9452
  });
9186
9453
  // Rank 1 sits at the top: the axis runs down from half a rank above 1 to half a rank below `top`.
9187
9454
  const y = linearScale([0.5, top + 0.5], [plot.y, plot.y + plot.height]);
9188
9455
  const band = bandScale(count, [plot.x, plot.x + plot.width], 0);
9189
- const c = canvas(input, width, height, input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) })));
9456
+ const c = canvas(input, width, height, legend);
9190
9457
  const rankTicks = Array.from({ length: top }, (_, k) => ({
9191
9458
  pos: px(y(k + 1)),
9192
9459
  label: `#${k + 1}`,
@@ -9294,7 +9561,8 @@ function layoutPolarAreaChart(input) {
9294
9561
  return { ...state, issues };
9295
9562
  const format = makeFormatter(input.format);
9296
9563
  const { width, height } = size(input, 300, 300);
9297
- const top = legendTop(input);
9564
+ const legend = input.data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
9565
+ const top = legendTop(input, legend, width);
9298
9566
  const cx = width / 2;
9299
9567
  const cy = top + (height - top) / 2;
9300
9568
  const outer = Math.max(0, Math.min(width, height - top) / 2 - (input.labels ? 40 : 12));
@@ -9356,7 +9624,7 @@ function layoutPolarAreaChart(input) {
9356
9624
  }));
9357
9625
  }
9358
9626
  });
9359
- const c = canvas(input, width, height, input.data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) })));
9627
+ const c = canvas(input, width, height, legend);
9360
9628
  return { ...c, shapes: [...grid, ...sectors, ...texts, ...c.shapes], issues };
9361
9629
  }
9362
9630
 
@@ -9419,15 +9687,16 @@ function layoutSwarmPlot(input) {
9419
9687
  const { width, height } = size(input);
9420
9688
  const horizontal = input.orientation === 'horizontal';
9421
9689
  const widest = Math.max(0, ...input.series.map((s) => s.name.length)) * CHAR;
9690
+ const legend = input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) }));
9422
9691
  const plot = plotBox(width, height, {
9423
9692
  ...MARGIN$1,
9424
- top: MARGIN$1.top + legendTop(input) + (!horizontal && input.yAxis?.label ? 12 : 0),
9693
+ top: MARGIN$1.top + legendTop(input, legend, width) + (!horizontal && input.yAxis?.label ? 12 : 0),
9425
9694
  left: horizontal ? Math.max(MARGIN$1.left, widest + 16) : MARGIN$1.left,
9426
9695
  bottom: MARGIN$1.bottom + (horizontal && input.yAxis?.label ? 14 : 0),
9427
9696
  });
9428
9697
  const value = axis(extent(samples.flat(), false), horizontal ? [plot.x, plot.x + plot.width] : [plot.y + plot.height, plot.y], input.yAxis ?? {}, input.format);
9429
9698
  const band = bandScale(input.series.length, horizontal ? [plot.y, plot.y + plot.height] : [plot.x, plot.x + plot.width]);
9430
- const c = canvas(input, width, height, input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) })));
9699
+ const c = canvas(input, width, height, legend);
9431
9700
  const categoryTicks = input.series.map((s, i) => ({ pos: px(band.center(i)), label: s.name }));
9432
9701
  const shapes = frameShapes({
9433
9702
  ...c,
@@ -9540,7 +9809,23 @@ function layoutWaffleChart(input) {
9540
9809
  const share = (v) => (total > 0 ? v / total : 0);
9541
9810
  const counts = waffleCounts(data.map((d) => (isHidden(input, d.name) ? 0 : Math.min(1, share(d.value)) * cells)));
9542
9811
  const { width, height } = size(input, 300, 300);
9543
- const top = legendTop(input);
9812
+ // `labels` appends the share to each legend entry; the entry still toggles by the datum's name.
9813
+ const shares = new Map(data.map((d) => [d.name, `${Math.round(share(d.value) * 100)}%`]));
9814
+ const legend = legendOptions(input);
9815
+ const legendItems = input.data.map((d, i) => ({
9816
+ name: d.name,
9817
+ color: colorOf(input, i, d.color),
9818
+ }));
9819
+ const legendInput = input.labels && legend
9820
+ ? {
9821
+ ...input,
9822
+ legend: {
9823
+ ...legend,
9824
+ formatter: (n) => `${legend.formatter?.(n) ?? n} ${shares.get(n) ?? ''}`,
9825
+ },
9826
+ }
9827
+ : input;
9828
+ const top = legendTop(legendInput, legendItems, width);
9544
9829
  const gap = Math.max(0, input.cellGap ?? 2);
9545
9830
  const cell = Math.max(1, Math.min((width - 2 * PAD) / columns, (height - top - 2 * PAD) / rows));
9546
9831
  const x0 = (width - cell * columns) / 2;
@@ -9550,7 +9835,7 @@ function layoutWaffleChart(input) {
9550
9835
  const corner = (k) => horizontal
9551
9836
  ? { x: x0 + (k % columns) * cell, y: y0 + Math.floor(k / columns) * cell }
9552
9837
  : { x: x0 + Math.floor(k / rows) * cell, y: y0 + (rows - 1 - (k % rows)) * cell };
9553
- const rect = (k, fill, title, item, opacity) => {
9838
+ const rect = (k, fill, title, item, opacity, stroke) => {
9554
9839
  const { x, y } = corner(k);
9555
9840
  return {
9556
9841
  kind: 'rect',
@@ -9560,6 +9845,7 @@ function layoutWaffleChart(input) {
9560
9845
  width: px(Math.max(0.5, cell - gap)),
9561
9846
  height: px(Math.max(0.5, cell - gap)),
9562
9847
  fill,
9848
+ stroke,
9563
9849
  opacity,
9564
9850
  rx: input.cellRadius ?? 2,
9565
9851
  title,
@@ -9577,20 +9863,10 @@ function layoutWaffleChart(input) {
9577
9863
  });
9578
9864
  const rest = total - sum;
9579
9865
  const restTitle = rest > 0 ? `Other: ${format(rest)} (${Math.round(share(rest) * 100)}%)` : 'Empty';
9866
+ // A subtle fill alone is the same luminance as a dark stage; the outline carries the contrast.
9580
9867
  for (; k < cells; k++)
9581
- shapes.push(rect(k, CHART_COLORS.grid, restTitle, undefined, 0.6));
9582
- // `labels` appends the share to each legend entry; the entry still toggles by the datum's name.
9583
- const shares = new Map(data.map((d) => [d.name, `${Math.round(share(d.value) * 100)}%`]));
9584
- const legend = legendOptions(input);
9585
- const c = canvas(input.labels && legend
9586
- ? {
9587
- ...input,
9588
- legend: {
9589
- ...legend,
9590
- formatter: (n) => `${legend.formatter?.(n) ?? n} ${shares.get(n) ?? ''}`,
9591
- },
9592
- }
9593
- : input, width, height, input.data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) })));
9868
+ shapes.push(rect(k, CHART_COLORS.grid, restTitle, undefined, 0.6, CHART_COLORS.axis));
9869
+ const c = canvas(legendInput, width, height, legendItems);
9594
9870
  return { ...c, shapes: [...shapes, ...c.shapes], issues };
9595
9871
  }
9596
9872
 
@@ -9608,6 +9884,7 @@ function layoutWaffleChart(input) {
9608
9884
  /** Draws a core shape list back to front; marks with an `item` carry their index for hit-testing. */
9609
9885
  class SkdxChartShapesComponent {
9610
9886
  constructor() {
9887
+ this.surfaceRadius = chartSurfaceRadius;
9611
9888
  this.shapes = input.required(/* @ts-ignore */
9612
9889
  ...(ngDevMode ? [{ debugName: "shapes" }] : /* istanbul ignore next */ []));
9613
9890
  }
@@ -9643,9 +9920,13 @@ class SkdxChartShapesComponent {
9643
9920
  [attr.width]="s.width"
9644
9921
  [attr.height]="s.height"
9645
9922
  [attr.rx]="s.rx"
9923
+ [style.rx]="surfaceRadius(s.part)"
9646
9924
  [attr.fill]="s.fill"
9647
9925
  [attr.stroke]="s.stroke"
9648
9926
  [attr.fill-opacity]="s.opacity"
9927
+ [attr.pointer-events]="
9928
+ s.part === 'stage-label-background' || s.part === 'label-background' ? 'none' : null
9929
+ "
9649
9930
  >
9650
9931
  @if (s.title) {
9651
9932
  <svg:title>{{ s.title }}</svg:title>
@@ -9688,6 +9969,7 @@ class SkdxChartShapesComponent {
9688
9969
  }
9689
9970
  @case ('text') {
9690
9971
  <svg:text
9972
+ [attr.aria-label]="s.title"
9691
9973
  [attr.data-skdx-part]="s.part"
9692
9974
  [attr.data-skdx-i]="s.item ? $index : null"
9693
9975
  [attr.x]="s.x"
@@ -9742,9 +10024,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
9742
10024
  [attr.width]="s.width"
9743
10025
  [attr.height]="s.height"
9744
10026
  [attr.rx]="s.rx"
10027
+ [style.rx]="surfaceRadius(s.part)"
9745
10028
  [attr.fill]="s.fill"
9746
10029
  [attr.stroke]="s.stroke"
9747
10030
  [attr.fill-opacity]="s.opacity"
10031
+ [attr.pointer-events]="
10032
+ s.part === 'stage-label-background' || s.part === 'label-background' ? 'none' : null
10033
+ "
9748
10034
  >
9749
10035
  @if (s.title) {
9750
10036
  <svg:title>{{ s.title }}</svg:title>
@@ -9787,6 +10073,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
9787
10073
  }
9788
10074
  @case ('text') {
9789
10075
  <svg:text
10076
+ [attr.aria-label]="s.title"
9790
10077
  [attr.data-skdx-part]="s.part"
9791
10078
  [attr.data-skdx-i]="s.item ? $index : null"
9792
10079
  [attr.x]="s.x"
@@ -9882,12 +10169,16 @@ class SkdxChartRootComponent {
9882
10169
  ...(ngDevMode ? [{ debugName: "svg" }] : /* istanbul ignore next */ []));
9883
10170
  this.hover = null;
9884
10171
  this.token = {};
10172
+ this.dismissedCrosshair = signal(null, /* @ts-ignore */
10173
+ ...(ngDevMode ? [{ debugName: "dismissedCrosshair" }] : /* istanbul ignore next */ []));
9885
10174
  this.tip = signal([], /* @ts-ignore */
9886
10175
  ...(ngDevMode ? [{ debugName: "tip" }] : /* istanbul ignore next */ []));
9887
10176
  this.tipShapes = computed(() => {
9888
10177
  const own = this.tip();
9889
10178
  const cross = this.crosshair();
9890
- return own.length || !cross ? own : crosshairAt(this.layout(), cross);
10179
+ return own.length || !cross || cross === this.dismissedCrosshair()
10180
+ ? own
10181
+ : crosshairAt(this.layout(), cross);
9891
10182
  }, /* @ts-ignore */
9892
10183
  ...(ngDevMode ? [{ debugName: "tipShapes" }] : /* istanbul ignore next */ []));
9893
10184
  this.lastCross = null;
@@ -9912,8 +10203,6 @@ class SkdxChartRootComponent {
9912
10203
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
9913
10204
  this.focusable = computed(() => !this.layout().decorative && focusTargets(this.layout()).length > 0, /* @ts-ignore */
9914
10205
  ...(ngDevMode ? [{ debugName: "focusable" }] : /* istanbul ignore next */ []));
9915
- this.descId = computed(() => this.layout().description ? `${this.kind()}-desc-${this.label().replace(/\W+/g, '-')}` : null, /* @ts-ignore */
9916
- ...(ngDevMode ? [{ debugName: "descId" }] : /* istanbul ignore next */ []));
9917
10206
  this.focusShapes = computed(() => focusShapes(this.layout(), this.focused()), /* @ts-ignore */
9918
10207
  ...(ngDevMode ? [{ debugName: "focusShapes" }] : /* istanbul ignore next */ []));
9919
10208
  this.drag = null;
@@ -10019,6 +10308,7 @@ class SkdxChartRootComponent {
10019
10308
  this.shareCrosshair(null);
10020
10309
  }
10021
10310
  onPointerMove(e) {
10311
+ this.dismissedCrosshair.set(null);
10022
10312
  const layout = this.layout();
10023
10313
  if (this.pointers.has(e.pointerId))
10024
10314
  this.pointers.set(e.pointerId, this.viewBox(e).x);
@@ -10233,6 +10523,14 @@ class SkdxChartRootComponent {
10233
10523
  }
10234
10524
  }
10235
10525
  onKeyDown(e) {
10526
+ if (e.key === 'Escape') {
10527
+ e.preventDefault();
10528
+ this.focused.set(null);
10529
+ this.clearTip();
10530
+ this.dismissedCrosshair.set(this.crosshair());
10531
+ this.highlight(null);
10532
+ return;
10533
+ }
10236
10534
  if (!this.focusable())
10237
10535
  return;
10238
10536
  const focused = this.focused();
@@ -10270,14 +10568,13 @@ class SkdxChartRootComponent {
10270
10568
  [attr.role]="layout().decorative ? 'presentation' : 'img'"
10271
10569
  [attr.aria-hidden]="layout().decorative ? 'true' : null"
10272
10570
  [attr.aria-label]="layout().decorative ? null : label()"
10273
- [attr.aria-describedby]="descId()"
10571
+ [attr.aria-description]="layout().description"
10274
10572
  [attr.tabindex]="focusable() ? 0 : null"
10275
10573
  [attr.data-skdx-chart]="kind()"
10276
10574
  [attr.font-family]="colors.fontFamily"
10277
10575
  [attr.font-size]="colors.fontSize"
10278
10576
  [style.max-width]="'100%'"
10279
10577
  [style.height]="'auto'"
10280
- [style.outline]="'none'"
10281
10578
  [style.cursor]="canZoom() || canPan() ? 'grab' : brushing() ? 'crosshair' : null"
10282
10579
  [style.touch-action]="active() ? 'none' : null"
10283
10580
  [style.--skdx-chart-duration]="duration()"
@@ -10294,7 +10591,7 @@ class SkdxChartRootComponent {
10294
10591
  >
10295
10592
  <title>{{ label() }}</title>
10296
10593
  @if (layout().description) {
10297
- <svg:desc [attr.id]="descId()">{{ layout().description }}</svg:desc>
10594
+ <svg:desc>{{ layout().description }}</svg:desc>
10298
10595
  }
10299
10596
  @if (layout().defs?.length) {
10300
10597
  <svg:defs>
@@ -10340,7 +10637,7 @@ class SkdxChartRootComponent {
10340
10637
  [attr.data-skdx-tooltip]="kind()"
10341
10638
  [style.left.px]="p.x + 12"
10342
10639
  [style.top.px]="p.y + 12"
10343
- style="position:fixed;pointer-events:none;z-index:1000;background:var(--skdx-color-surface-raised);color:var(--skdx-color-text-default);border:1px solid var(--skdx-color-border-strong);border-radius:3px;padding:4px 8px;font-family:var(--skdx-font-family);font-size:var(--skdx-font-size-xs);white-space:nowrap"
10640
+ style="position:fixed;pointer-events:none;z-index:1000;background:var(--skdx-color-surface-raised);color:var(--skdx-color-text-default);border:1px solid var(--skdx-color-border-strong);border-radius:var(--skdx-radius-default, var(--skdx-radius-md, 0.5rem));padding:4px 8px;font-family:var(--skdx-font-family);font-size:var(--skdx-font-size-xs);white-space:nowrap"
10344
10641
  >
10345
10642
  @for (line of p.lines; track $index) {
10346
10643
  <div [style.font-weight]="$index === 0 && p.lines.length > 1 ? 'bold' : null">
@@ -10401,14 +10698,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10401
10698
  [attr.role]="layout().decorative ? 'presentation' : 'img'"
10402
10699
  [attr.aria-hidden]="layout().decorative ? 'true' : null"
10403
10700
  [attr.aria-label]="layout().decorative ? null : label()"
10404
- [attr.aria-describedby]="descId()"
10701
+ [attr.aria-description]="layout().description"
10405
10702
  [attr.tabindex]="focusable() ? 0 : null"
10406
10703
  [attr.data-skdx-chart]="kind()"
10407
10704
  [attr.font-family]="colors.fontFamily"
10408
10705
  [attr.font-size]="colors.fontSize"
10409
10706
  [style.max-width]="'100%'"
10410
10707
  [style.height]="'auto'"
10411
- [style.outline]="'none'"
10412
10708
  [style.cursor]="canZoom() || canPan() ? 'grab' : brushing() ? 'crosshair' : null"
10413
10709
  [style.touch-action]="active() ? 'none' : null"
10414
10710
  [style.--skdx-chart-duration]="duration()"
@@ -10425,7 +10721,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10425
10721
  >
10426
10722
  <title>{{ label() }}</title>
10427
10723
  @if (layout().description) {
10428
- <svg:desc [attr.id]="descId()">{{ layout().description }}</svg:desc>
10724
+ <svg:desc>{{ layout().description }}</svg:desc>
10429
10725
  }
10430
10726
  @if (layout().defs?.length) {
10431
10727
  <svg:defs>
@@ -10471,7 +10767,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10471
10767
  [attr.data-skdx-tooltip]="kind()"
10472
10768
  [style.left.px]="p.x + 12"
10473
10769
  [style.top.px]="p.y + 12"
10474
- style="position:fixed;pointer-events:none;z-index:1000;background:var(--skdx-color-surface-raised);color:var(--skdx-color-text-default);border:1px solid var(--skdx-color-border-strong);border-radius:3px;padding:4px 8px;font-family:var(--skdx-font-family);font-size:var(--skdx-font-size-xs);white-space:nowrap"
10770
+ style="position:fixed;pointer-events:none;z-index:1000;background:var(--skdx-color-surface-raised);color:var(--skdx-color-text-default);border:1px solid var(--skdx-color-border-strong);border-radius:var(--skdx-radius-default, var(--skdx-radius-md, 0.5rem));padding:4px 8px;font-family:var(--skdx-font-family);font-size:var(--skdx-font-size-xs);white-space:nowrap"
10475
10771
  >
10476
10772
  @for (line of p.lines; track $index) {
10477
10773
  <div [style.font-weight]="$index === 0 && p.lines.length > 1 ? 'bold' : null">
@@ -10696,10 +10992,19 @@ class SkdxSizedBase {
10696
10992
  };
10697
10993
  }
10698
10994
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxSizedBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
10699
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.4", type: SkdxSizedBase, isStandalone: true, inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, palette: { classPropertyName: "palette", publicName: "palette", isSignal: true, isRequired: false, transformFunction: null }, hiddenSeries: { classPropertyName: "hiddenSeries", publicName: "hiddenSeries", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, highlighted: { classPropertyName: "highlighted", publicName: "highlighted", isSignal: true, isRequired: false, transformFunction: null }, highlightScope: { classPropertyName: "highlightScope", publicName: "highlightScope", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, decorative: { classPropertyName: "decorative", publicName: "decorative", isSignal: true, isRequired: false, transformFunction: null }, accessibleTable: { classPropertyName: "accessibleTable", publicName: "accessibleTable", isSignal: true, isRequired: false, transformFunction: null }, animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, zoomable: { classPropertyName: "zoomable", publicName: "zoomable", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, syncId: { classPropertyName: "syncId", publicName: "syncId", isSignal: true, isRequired: false, transformFunction: null }, crossFilter: { classPropertyName: "crossFilter", publicName: "crossFilter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { hiddenSeries: "hiddenSeriesChange", selected: "selectedChange", highlighted: "highlightedChange", zoom: "zoomChange", view: "viewChange", itemClick: "itemClick", dataIssue: "dataIssue", taskChange: "taskChange" }, viewQueries: [{ propertyName: "chartRoot", first: true, predicate: SkdxChartRootComponent, descendants: true, isSignal: true }], ngImport: i0 }); }
10995
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.4", type: SkdxSizedBase, isStandalone: true, inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, palette: { classPropertyName: "palette", publicName: "palette", isSignal: true, isRequired: false, transformFunction: null }, hiddenSeries: { classPropertyName: "hiddenSeries", publicName: "hiddenSeries", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, highlighted: { classPropertyName: "highlighted", publicName: "highlighted", isSignal: true, isRequired: false, transformFunction: null }, highlightScope: { classPropertyName: "highlightScope", publicName: "highlightScope", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, decorative: { classPropertyName: "decorative", publicName: "decorative", isSignal: true, isRequired: false, transformFunction: null }, accessibleTable: { classPropertyName: "accessibleTable", publicName: "accessibleTable", isSignal: true, isRequired: false, transformFunction: null }, animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, zoomable: { classPropertyName: "zoomable", publicName: "zoomable", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, syncId: { classPropertyName: "syncId", publicName: "syncId", isSignal: true, isRequired: false, transformFunction: null }, crossFilter: { classPropertyName: "crossFilter", publicName: "crossFilter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { hiddenSeries: "hiddenSeriesChange", selected: "selectedChange", highlighted: "highlightedChange", zoom: "zoomChange", view: "viewChange", itemClick: "itemClick", dataIssue: "dataIssue", taskChange: "taskChange" }, host: { properties: { "style.display": "responsive() ? 'block' : null", "style.width": "responsive() ? '100%' : null", "style.min-width": "responsive() ? '0' : null" } }, viewQueries: [{ propertyName: "chartRoot", first: true, predicate: SkdxChartRootComponent, descendants: true, isSignal: true }], ngImport: i0 }); }
10700
10996
  }
10701
10997
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxSizedBase, decorators: [{
10702
- type: Directive
10998
+ type: Directive,
10999
+ args: [{
11000
+ // A responsive chart must fill its container even when that container centers flex items.
11001
+ // Otherwise its last SVG width becomes the host's intrinsic width and it cannot grow again.
11002
+ host: {
11003
+ '[style.display]': "responsive() ? 'block' : null",
11004
+ '[style.width]': "responsive() ? '100%' : null",
11005
+ '[style.min-width]': "responsive() ? '0' : null",
11006
+ },
11007
+ }]
10703
11008
  }], propDecorators: { width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], legend: [{ type: i0.Input, args: [{ isSignal: true, alias: "legend", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], palette: [{ type: i0.Input, args: [{ isSignal: true, alias: "palette", required: false }] }], hiddenSeries: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenSeries", required: false }] }, { type: i0.Output, args: ["hiddenSeriesChange"] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], highlighted: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlighted", required: false }] }, { type: i0.Output, args: ["highlightedChange"] }], highlightScope: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightScope", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], decorative: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorative", required: false }] }], accessibleTable: [{ type: i0.Input, args: [{ isSignal: true, alias: "accessibleTable", required: false }] }], animate: [{ type: i0.Input, args: [{ isSignal: true, alias: "animate", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }, { type: i0.Output, args: ["zoomChange"] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }, { type: i0.Output, args: ["viewChange"] }], zoomable: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomable", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], syncId: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncId", required: false }] }], crossFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossFilter", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], dataIssue: [{ type: i0.Output, args: ["dataIssue"] }], taskChange: [{ type: i0.Output, args: ["taskChange"] }], chartRoot: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SkdxChartRootComponent), { isSignal: true }] }] } });
10704
11009
  /** Inputs the cartesian charts (bar, line, area) take. */
10705
11010
  class SkdxChartBase extends SkdxSizedBase {
@@ -10824,5 +11129,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10824
11129
  * Generated bundle index. Do not edit.
10825
11130
  */
10826
11131
 
10827
- export { ANIMATION_CSS, ANIMATION_LIMIT, CHART_COLORS, MARKER_LIMIT, NETWORK_LABEL_LIMIT, PARALLEL_TITLE_LIMIT, SAMPLING_THRESHOLD, SERIES_COLORS, SWARM_TITLE_LIMIT, SkdxChartBase, SkdxChartRootComponent, SkdxChartShapesComponent, SkdxSizedBase, TABLE_LIMIT, TITLE_LIMIT, aggregate, anchorOf, animationDuration, annotationShapes, applyTransforms, binCounts, binEdges, binSamples, boxFrom, brushBoxShape, brushDrag, brushGrab, categoryLabel, colorLegendShapes, crosshairAt, crosshairLine, crosshairRows, crosshairShapes, csv, cumulative, cutDepth, defsMarkup, depthOf, difference, drill, emphasize, exponentialMovingAverage, exportChart, filter, findPath, focusShapes, focusTargets, formatValue, ganttDrag, ganttGhost, ganttGrab, ganttKey, hoverTooltip, hoveredRef, insidePolygon, isSelected, isUnzoomed, itemAt, itemId, itemsInBox, itemsInPolygon, lassoShape, layoutAreaChart, layoutBarChart, layoutBoxplot, layoutBubbleChart, layoutBulletChart, layoutBumpChart, layoutCandlestick, layoutCartesian, layoutChord, layoutCirclePacking, layoutComboChart, layoutDumbbellChart, layoutFunnelChart, layoutGantt, layoutGaugeChart, layoutHeatmap, layoutHistogram, layoutIcicle, layoutLineChart, layoutLinearGauge, layoutMap, layoutMarimekkoChart, layoutNetworkChart, layoutOrganizationChart, layoutParallelCoordinates, layoutParetoChart, layoutPieChart, layoutPolarAreaChart, layoutRadarChart, layoutRadialBar, layoutRangeChart, layoutSankey, layoutScatterChart, layoutSparkline, layoutSunburst, layoutSwarmPlot, layoutTimelineChart, layoutTreeChart, layoutTreemap, layoutWaffleChart, layoutWaterfall, legendOptions, lttb, m4, makeColorScale, makeFormatter, mapPan, mapWheel, matchesItem, minMaxSample, mixStops, movingAverage, nextFocus, nodeId, nodeValue, normalize, packCircles, parseColor, percentageChange, placeLegend, pointerShapes, pointerTooltipText, rank, refOf, referenceShapes, sameRef, sampleIndexes, samplingOptions, selectedIds, seriesCsv, shapeMarkup, sortChildren, sortValues, stateCanvas, svgMarkup, svgToPng, swarmOffsets, syncGroup, tableRows, timeTicks, toRefs, toViewBox, toggleSelection, tooltipOptions, tooltipShapes, translatePath, translateShapes, variantColor, waffleCounts, zoomPan, zoomPinch, zoomWheel };
11132
+ export { ANIMATION_CSS, ANIMATION_LIMIT, CHART_COLORS, CHART_SURFACE_RADIUS, MARKER_LIMIT, NETWORK_LABEL_LIMIT, PARALLEL_TITLE_LIMIT, SAMPLING_THRESHOLD, SERIES_COLORS, SWARM_TITLE_LIMIT, SkdxChartBase, SkdxChartRootComponent, SkdxChartShapesComponent, SkdxSizedBase, TABLE_LIMIT, TITLE_LIMIT, aggregate, anchorOf, animationDuration, annotationShapes, applyTransforms, binCounts, binEdges, binSamples, boxFrom, brushBoxShape, brushDrag, brushGrab, categoryLabel, chartSurfaceRadius, colorLegendShapes, crosshairAt, crosshairLine, crosshairRows, crosshairShapes, csv, cumulative, cutDepth, defsMarkup, depthOf, difference, drill, emphasize, exponentialMovingAverage, exportChart, filter, findPath, focusShapes, focusTargets, formatValue, ganttDrag, ganttGhost, ganttGrab, ganttKey, hoverTooltip, hoveredRef, insidePolygon, isSelected, isUnzoomed, itemAt, itemId, itemsInBox, itemsInPolygon, lassoShape, layoutAreaChart, layoutBarChart, layoutBoxplot, layoutBubbleChart, layoutBulletChart, layoutBumpChart, layoutCandlestick, layoutCartesian, layoutChord, layoutCirclePacking, layoutComboChart, layoutDumbbellChart, layoutFunnelChart, layoutGantt, layoutGaugeChart, layoutHeatmap, layoutHistogram, layoutIcicle, layoutLineChart, layoutLinearGauge, layoutMap, layoutMarimekkoChart, layoutNetworkChart, layoutOrganizationChart, layoutParallelCoordinates, layoutParetoChart, layoutPieChart, layoutPolarAreaChart, layoutRadarChart, layoutRadialBar, layoutRangeChart, layoutSankey, layoutScatterChart, layoutSparkline, layoutSunburst, layoutSwarmPlot, layoutTimelineChart, layoutTreeChart, layoutTreemap, layoutWaffleChart, layoutWaterfall, legendOptions, lttb, m4, makeColorScale, makeFormatter, mapPan, mapWheel, matchesItem, minMaxSample, mixStops, movingAverage, nextFocus, nodeId, nodeValue, normalize, packCircles, parseColor, percentageChange, placeLegend, pointerShapes, pointerTooltipText, rank, refOf, referenceShapes, sameRef, sampleIndexes, samplingOptions, selectedIds, seriesCsv, shapeMarkup, sortChildren, sortValues, stateCanvas, svgMarkup, svgToPng, swarmOffsets, syncGroup, tableRows, timeTicks, toRefs, toViewBox, toggleSelection, tooltipOptions, tooltipShapes, translatePath, translateShapes, variantColor, waffleCounts, zoomPan, zoomPinch, zoomWheel };
10828
11133
  //# sourceMappingURL=skdx-angular-charts-internal.mjs.map