@skdx/angular-charts 0.39.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.
@@ -470,6 +470,16 @@ const legendTop = (input, items, width) => {
470
470
  };
471
471
  /** Average glyph width at the chart font size, for text-width estimates. */
472
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
+ }
473
483
  /** The series color for index `i`: an explicit override, else the chart palette, else the tokens. */
474
484
  const colorOf = (input, i, override) => override ??
475
485
  (input.palette && input.palette.length > 0
@@ -625,9 +635,29 @@ function placeLegend(layout) {
625
635
  }
626
636
  /** A tooltip box next to the pointer, kept inside the canvas; one line per entry. */
627
637
  function tooltipShapes(x, y, text, width, height, placement = 'auto') {
628
- const lines = Array.isArray(text) ? text : [text];
629
- const w = Math.max(...lines.map((l) => l.length)) * CHAR + 12;
630
- 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);
631
661
  // `auto` sits above-right of the anchor and slides inside the canvas; a fixed placement
632
662
  // centres the box on the named side and is only clamped.
633
663
  const wanted = placement === 'top'
@@ -645,13 +675,14 @@ function tooltipShapes(x, y, text, width, height, placement = 'auto') {
645
675
  {
646
676
  kind: 'rect',
647
677
  part: 'tooltip',
678
+ title: original.join('\n'),
648
679
  x: px(bx),
649
680
  y: px(by),
650
681
  width: px(w),
651
682
  height: h,
652
683
  fill: 'var(--skdx-color-surface-raised)',
653
684
  stroke: CHART_COLORS.axis,
654
- rx: 3,
685
+ rx: 8,
655
686
  },
656
687
  ...lines.map((line, i) => ({
657
688
  kind: 'text',
@@ -783,7 +814,7 @@ function stateCanvas(input, empty, w, h) {
783
814
  width,
784
815
  height,
785
816
  fill: 'var(--skdx-color-surface-sunken)',
786
- rx: 4,
817
+ rx: 8,
787
818
  });
788
819
  shapes.push({
789
820
  kind: 'text',
@@ -850,7 +881,7 @@ function frameShapes(frame, options = {}) {
850
881
  baseline: 'middle',
851
882
  rotate: rot,
852
883
  })
853
- : label(x, y, text, { part, anchor: tickAnchor(x, text, frame.width) });
884
+ : boundedLabel(x, y, text, frame.width, { part, anchor: tickAnchor(x, text, frame.width) });
854
885
  // Short marks across an axis line at the ticks.
855
886
  const marks = (ticks, along, at, dir, size) => {
856
887
  for (const t of ticks)
@@ -1034,6 +1065,54 @@ function frameShapes(frame, options = {}) {
1034
1065
  return shapes;
1035
1066
  }
1036
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
+ }
1037
1116
  /** A dot with its native tooltip. */
1038
1117
  const point$1 = (cx, cy, r, fill, title, item) => ({ kind: 'circle', part: 'point', cx, cy, r, fill, title, item });
1039
1118
 
@@ -1753,25 +1832,39 @@ function layoutPoints(input, options) {
1753
1832
  const legendRoom = input.colorScale
1754
1833
  ? colorLegendRoom(input.colorLegend)
1755
1834
  : { bottom: 0, right: 0, options: {} };
1835
+ const legend = input.series.map((s, i) => ({ name: s.name, color: colorOf(input, i, s.color) }));
1756
1836
  const plot = plotBox(width, height, {
1757
1837
  ...MARGIN$1,
1758
- 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),
1759
1839
  right: MARGIN$1.right + legendRoom.right,
1760
1840
  bottom: MARGIN$1.bottom + (input.xAxis?.label ? 14 : 0) + legendRoom.bottom,
1761
1841
  });
1762
1842
  const markerSize = input.markerSize ?? 3;
1763
1843
  const hitRadius = Math.max(0, input.hitRadius ?? 0);
1764
- 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;
1765
1860
  // Duplicates at one pixel are spread deterministically on a ring around it.
1766
1861
  const seen = new Map();
1767
1862
  // A color scale maps each dot's `value`; dots without one keep their series color.
1768
1863
  const colors = input.colorScale
1769
1864
  ? makeColorScale(input.colorScale, dots.map((d) => d.value), colorOf(input, 0), undefined, makeFormatter(input.format))
1770
1865
  : undefined;
1771
- const x = axis(extent(dots.map((p) => p.x), false), [plot.x, plot.x + plot.width], input.xAxis ?? {}, input.format);
1772
- const y = axis(extent(dots.map((p) => p.y), false), [plot.y + plot.height, plot.y], input.yAxis ?? {}, input.format);
1773
- const sizes = minMax(dots.map((p) => p.size));
1774
- 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);
1775
1868
  const c = canvas(input, width, height, legend);
1776
1869
  const marks = [];
1777
1870
  const texts = [];
@@ -1783,7 +1876,7 @@ function layoutPoints(input, options) {
1783
1876
  for (const dot of s.data) {
1784
1877
  const di = dot.index;
1785
1878
  const title = options.describe(s.name, dot, x, y);
1786
- const r = sized ? options.radius(dot.size, sizes) : markerSize;
1879
+ const r = requestedRadius(dot, sized) * extentScale;
1787
1880
  let cx = px(x.scale(dot.x));
1788
1881
  let cy = px(y.scale(dot.y));
1789
1882
  if (jitter > 0) {
@@ -1875,7 +1968,29 @@ function layoutPoints(input, options) {
1875
1968
  }
1876
1969
  }
1877
1970
  }
1878
- 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 };
1879
1994
  const scales = {
1880
1995
  plot,
1881
1996
  x: (v) => x.scale(categoryValue(v)),
@@ -1901,7 +2016,20 @@ function layoutPoints(input, options) {
1901
2016
  return {
1902
2017
  ...c,
1903
2018
  shapes: [
1904
- ...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
+ : []),
1905
2033
  ...references,
1906
2034
  ...(opacity !== undefined && marks.some((m) => m.part === options.part)
1907
2035
  ? marks.map((m) => (m.kind === 'circle' || m.kind === 'path') && m.part !== 'hit' ? { ...m, opacity } : m)
@@ -2392,8 +2520,12 @@ function layoutCartesian(input, defaultType) {
2392
2520
  }
2393
2521
  // The default bottom margin already holds the first bottom axis.
2394
2522
  const firstBottom = shownAxes.find((a) => sideOf(a) === 'bottom');
2523
+ const legend = all.map((s) => ({ name: s.name, color: s.color }));
2395
2524
  const margin = {
2396
- 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,
2397
2529
  right: horizontal
2398
2530
  ? rtl
2399
2531
  ? Math.max(MARGIN$1.left, widestCategory + 16)
@@ -2461,7 +2593,7 @@ function layoutCartesian(input, defaultType) {
2461
2593
  : plot.width /
2462
2594
  Math.max(0.2, Math.abs(Math.cos(((input.xAxis?.tickRotation ?? 0) * Math.PI) / 180))));
2463
2595
  const frame = {
2464
- ...canvas(input, width, height, all.map((s) => ({ name: s.name, color: s.color }))),
2596
+ ...canvas(input, width, height, legend),
2465
2597
  plot,
2466
2598
  xTicks: categoryTicks,
2467
2599
  yTicks: horizontal ? thinTicks(left.ticks, plot.width) : left.ticks,
@@ -2516,7 +2648,10 @@ function layoutCartesian(input, defaultType) {
2516
2648
  },
2517
2649
  y: (v, a) => value(v, a),
2518
2650
  };
2519
- 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'));
2520
2655
  // One slot per plain bar series and one per bar stack group, side by side inside the band.
2521
2656
  const slots = [];
2522
2657
  const slotOf = new Map();
@@ -2909,7 +3044,7 @@ function layoutCartesian(input, defaultType) {
2909
3044
  width: 6,
2910
3045
  height: box.height - 16,
2911
3046
  fill: CHART_COLORS.axis,
2912
- rx: 2,
3047
+ rx: 8,
2913
3048
  }, {
2914
3049
  kind: 'rect',
2915
3050
  part: 'brush-handle',
@@ -2918,7 +3053,7 @@ function layoutCartesian(input, defaultType) {
2918
3053
  width: 6,
2919
3054
  height: box.height - 16,
2920
3055
  fill: CHART_COLORS.axis,
2921
- rx: 2,
3056
+ rx: 8,
2922
3057
  });
2923
3058
  hit.brush = { box, window };
2924
3059
  }
@@ -2932,6 +3067,7 @@ function layoutCartesian(input, defaultType) {
2932
3067
  ...errorMarks,
2933
3068
  ...points,
2934
3069
  ...texts,
3070
+ ...referenceLabels,
2935
3071
  ...annotationShapes(input.annotations, scales),
2936
3072
  ...brush,
2937
3073
  ...frame.shapes,
@@ -3412,7 +3548,8 @@ function layoutPieChart(input) {
3412
3548
  : (p) => `${Math.round(p * 100)}%`;
3413
3549
  const cornerRadius = Math.max(0, input.cornerRadius ?? 0);
3414
3550
  const { width, height } = size(input, 300, 300);
3415
- 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);
3416
3553
  const start = ((input.startAngle ?? 0) * Math.PI) / 180;
3417
3554
  const end = ((input.endAngle ?? (input.startAngle ?? 0) + 360) * Math.PI) / 180;
3418
3555
  const sweep = Math.max(0, Math.min(TAU$1, end - start));
@@ -3502,7 +3639,7 @@ function layoutPieChart(input) {
3502
3639
  y2: px(cy - (r + 12) * Math.cos(mid)),
3503
3640
  stroke: CHART_COLORS.axis,
3504
3641
  });
3505
- labels.push(label(px(cx + (r + 16) * sin), ly, text, {
3642
+ labels.push(boundedLabel(px(cx + (r + 16) * sin), ly, text, width, {
3506
3643
  part: 'slice-label',
3507
3644
  anchor: sin > 0.1 ? 'start' : sin < -0.1 ? 'end' : 'middle',
3508
3645
  baseline: 'middle',
@@ -3512,7 +3649,7 @@ function layoutPieChart(input) {
3512
3649
  }
3513
3650
  else if (percent > 0.03) {
3514
3651
  const lr = (r + r0) / 2;
3515
- 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, {
3516
3653
  part: 'slice-label',
3517
3654
  anchor: 'middle',
3518
3655
  baseline: 'middle',
@@ -3541,8 +3678,12 @@ function layoutPieChart(input) {
3541
3678
  fill: 'var(--skdx-color-text-default)',
3542
3679
  weight: 'bold',
3543
3680
  }));
3544
- const c = canvas(input, width, height, input.data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) })));
3545
- 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
+ };
3546
3687
  }
3547
3688
 
3548
3689
  /*
@@ -3702,7 +3843,8 @@ function layoutRadarChart(input) {
3702
3843
  const issues = [];
3703
3844
  const format = makeFormatter(input.format);
3704
3845
  const { width, height } = size(input, 300, 300);
3705
- 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);
3706
3848
  const cx = width / 2;
3707
3849
  const cy = top + (height - top) / 2;
3708
3850
  const radius = Math.max(0, Math.min(width, height - top) / 2 - 36);
@@ -3864,7 +4006,7 @@ function layoutRadarChart(input) {
3864
4006
  item: { label: s.name, series: s.name, seriesId: s.id },
3865
4007
  });
3866
4008
  });
3867
- 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);
3868
4010
  return { ...c, shapes: [...grid, ...polygons, ...points, ...texts, ...c.shapes], issues };
3869
4011
  }
3870
4012
 
@@ -4101,7 +4243,8 @@ function layoutFunnelChart(input) {
4101
4243
  const max = Math.max(0, ...data.map((d) => d.value));
4102
4244
  const total = data.reduce((s, d) => s + Math.max(0, d.value), 0);
4103
4245
  const first = data[0]?.value ?? 0;
4104
- 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);
4105
4248
  // Along: the axis stages are stacked on; across: the axis a stage's width is measured on.
4106
4249
  const along = horizontal ? width : height - offset;
4107
4250
  const across = horizontal ? height - offset : width;
@@ -4115,7 +4258,6 @@ function layoutFunnelChart(input) {
4115
4258
  const percentFormat = input.percentFormat
4116
4259
  ? makeFormatter(input.percentFormat)
4117
4260
  : (p) => `${Math.round(p * 100)}%`;
4118
- const legend = data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
4119
4261
  const mid = (horizontal ? offset + (height - offset) / 2 : width / 2) - (outside ? across / 6 : 0);
4120
4262
  const stages = data.flatMap((d, i) => {
4121
4263
  const near = widthOf(d.value);
@@ -4124,8 +4266,9 @@ function layoutFunnelChart(input) {
4124
4266
  const p1 = p0 + stageLen - gap;
4125
4267
  const percent = pyramid ? (total > 0 ? d.value / total : 0) : first > 0 ? d.value / first : 0;
4126
4268
  const previous = data[i - 1]?.value;
4269
+ const change = previous !== undefined && previous > 0 ? d.value / previous - 1 : 0;
4127
4270
  const drop = !pyramid && previous !== undefined && previous > 0
4128
- ? ` · −${percentFormat(1 - d.value / previous)} from ${data[i - 1]?.name}`
4271
+ ? ` · ${change < 0 ? '−' : change > 0 ? '+' : ''}${percentFormat(Math.abs(change))} from ${data[i - 1]?.name}`
4129
4272
  : '';
4130
4273
  const title = `${d.name}: ${format(d.value)} (${percentFormat(percent)})${drop}`;
4131
4274
  const d0 = horizontal
@@ -4148,7 +4291,7 @@ function layoutFunnelChart(input) {
4148
4291
  const at = mid + full / 2 + PAD$2;
4149
4292
  return [
4150
4293
  stage,
4151
- label(px(horizontal ? center : at), px(horizontal ? at : center), text, {
4294
+ boundedLabel(px(horizontal ? center : at), px(horizontal ? at : center), text, width, {
4152
4295
  part: 'stage-label',
4153
4296
  anchor: horizontal ? 'middle' : 'start',
4154
4297
  baseline: horizontal ? 'hanging' : 'middle',
@@ -4156,14 +4299,27 @@ function layoutFunnelChart(input) {
4156
4299
  }),
4157
4300
  ];
4158
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
+ });
4159
4308
  return [
4160
4309
  stage,
4161
- label(px(horizontal ? center : mid), px(horizontal ? mid : center), text, {
4162
- part: 'stage-label',
4163
- anchor: 'middle',
4164
- baseline: 'middle',
4165
- fill: 'var(--skdx-color-text-on-accent)',
4166
- }),
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,
4167
4323
  ];
4168
4324
  });
4169
4325
  const c = canvas(input, width, height, legend);
@@ -4187,7 +4343,8 @@ function layoutRadialBar(input) {
4187
4343
  return state;
4188
4344
  const format = makeFormatter(input.format);
4189
4345
  const { width, height } = size(input, 300, 300);
4190
- 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);
4191
4348
  const cx = width / 2;
4192
4349
  const cy = top + (height - top) / 2;
4193
4350
  const outer = Math.max(0, Math.min(width, height - top) / 2 - (input.bands ? 20 : 8));
@@ -4202,7 +4359,6 @@ function layoutRadialBar(input) {
4202
4359
  const rounded = input.rounded ?? true;
4203
4360
  const ratio = (v) => (max > 0 ? Math.max(0, Math.min(1, v / max)) : 0);
4204
4361
  const shapes = [];
4205
- const legend = data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) }));
4206
4362
  // Threshold sectors outside the arcs, e.g. poor / fair / good.
4207
4363
  for (const b of input.bands ?? []) {
4208
4364
  const a0 = start + ratio(b.from) * sweep;
@@ -4847,13 +5003,14 @@ function layoutNetworkChart(input) {
4847
5003
  const { width, height } = size(input);
4848
5004
  const [rMin, rMax] = input.nodeRange ?? [5, 16];
4849
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) }));
4850
5008
  const plot = plotBox(width, height, {
4851
- top: legendTop(input) + rMax + 8,
5009
+ top: legendTop(input, legend, width) + rMax + 8,
4852
5010
  right: rMax + (labels ? 48 : 8),
4853
5011
  bottom: rMax + 8,
4854
5012
  left: rMax + 8,
4855
5013
  });
4856
- const groups = [...new Set(input.nodes.map(groupOf).filter((g) => g !== undefined))];
4857
5014
  const colorFor = (n, i) => n.color ?? colorOf(input, n.group !== undefined ? groups.indexOf(n.group) : i);
4858
5015
  const [, vMax] = minMax(nodes.map((n) => n.value));
4859
5016
  const radius = (v) => isFinite(v) && vMax > 0 ? rMin + Math.sqrt(Math.max(0, v) / vMax) * (rMax - rMin) : rMin;
@@ -5028,7 +5185,6 @@ function layoutNetworkChart(input) {
5028
5185
  fill: dim(p.node.id) === undefined ? 'var(--skdx-color-text-default)' : CHART_COLORS.grid,
5029
5186
  }));
5030
5187
  }
5031
- const legend = groups.map((g, i) => ({ name: g, color: colorOf(input, i) }));
5032
5188
  const c = canvas(input, width, height, legend);
5033
5189
  return {
5034
5190
  ...c,
@@ -5163,7 +5319,7 @@ function layoutTimelineChart(input) {
5163
5319
  // A list: the date column, a spine, one marker and label per row.
5164
5320
  const dateW = Math.max(...events.map((e) => dateOf(e.time).length)) * CHAR + 16;
5165
5321
  const plot = plotBox(width, height, {
5166
- top: 8 + legendTop(input),
5322
+ top: 8 + legendTop(input, legend, width),
5167
5323
  right: 8,
5168
5324
  bottom: 8,
5169
5325
  left: dateW + 12,
@@ -5217,7 +5373,7 @@ function layoutTimelineChart(input) {
5217
5373
  // Half a date label at each end, so the first and last ticks stay inside the canvas.
5218
5374
  const dateW = (Math.max(dateOf(t0).length, dateOf(t1).length) * CHAR) / 2 + 4;
5219
5375
  const plot = plotBox(width, height, {
5220
- top: 8 + legendTop(input),
5376
+ top: 8 + legendTop(input, legend, width),
5221
5377
  right: Math.max(16, dateW),
5222
5378
  bottom: 32 + (input.xAxis?.label ? 14 : 0),
5223
5379
  left: Math.max(16, laneW, dateW),
@@ -5311,6 +5467,26 @@ function layoutTimelineChart(input) {
5311
5467
  return { ...c, shapes: [...shapes, ...texts, ...marks, ...c.shapes], issues };
5312
5468
  }
5313
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
+
5314
5490
  /*
5315
5491
  * Copyright (c) 2026 SkandaDX
5316
5492
  * This file is part of the SkandaDX organization.
@@ -5873,7 +6049,7 @@ function shapeMarkup(s) {
5873
6049
  case 'circle':
5874
6050
  return `<circle${attrs([part, ['cx', s.cx], ['cy', s.cy], ['r', s.r], ['fill', s.fill], ['stroke', s.stroke]])}>${title}</circle>`;
5875
6051
  case 'text':
5876
- 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>`;
5877
6053
  }
5878
6054
  }
5879
6055
  /** The `<defs>` block for a layout's gradients, or an empty string. */
@@ -6425,13 +6601,13 @@ function layoutTreemap(input) {
6425
6601
  const formatValue = makeFormatter(input.format);
6426
6602
  const { width, height } = size(input);
6427
6603
  const crumbs = input.breadcrumbs && path.length > 0;
6428
- 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);
6429
6606
  const pad = Math.max(0, input.padding ?? 2);
6430
6607
  const sort = input.sort ?? 'desc';
6431
6608
  const tiling = input.tiling ?? 'squarify';
6432
6609
  const showLabels = input.labels ?? true;
6433
6610
  const shapes = [];
6434
- const legend = nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) }));
6435
6611
  // Leaves are colored by value when a scale is given; branches keep the branch color.
6436
6612
  const leaves = [];
6437
6613
  const collect = (list) => {
@@ -6479,11 +6655,14 @@ function layoutTreemap(input) {
6479
6655
  });
6480
6656
  if (branch)
6481
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);
6482
- else if (showLabels && tw > 40 && th > 20)
6483
- 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, {
6484
6662
  baseline: 'hanging',
6485
6663
  fill: 'var(--skdx-color-text-on-accent)',
6486
6664
  }));
6665
+ }
6487
6666
  };
6488
6667
  if (tiling === 'squarify')
6489
6668
  squarify(tiles, x, y, w, h, place);
@@ -6492,9 +6671,9 @@ function layoutTreemap(input) {
6492
6671
  };
6493
6672
  tile(nodes, 0, top, width, height - top, undefined, 0);
6494
6673
  if (crumbs)
6495
- shapes.push(...breadcrumbShapes(path, 8, legendTop(input) + 10));
6674
+ shapes.push(...breadcrumbShapes(path, 8, legendTop(input, legend, width) + 10));
6496
6675
  const c = canvas(input, width, height, legend);
6497
- return { ...c, shapes: [...shapes, ...c.shapes] };
6676
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...c.shapes]) };
6498
6677
  }
6499
6678
  /** The drill path as clickable crumbs (each carries the node's id) separated by `›`, from `x`. */
6500
6679
  function breadcrumbShapes(path, x, y) {
@@ -6547,6 +6726,7 @@ function layoutCandlestick(input) {
6547
6726
  const UP = input.upColor ?? CHART_COLORS.positive;
6548
6727
  const DOWN = input.downColor ?? CHART_COLORS.negative;
6549
6728
  const priceFormat = makeFormatter(input.format);
6729
+ const volumeFormat = makeFormatter(input.volumeFormat ?? { style: 'integer' });
6550
6730
  const count = all.length;
6551
6731
  const start = Math.max(0, Math.min(count - 2, Math.floor(input.zoom?.start ?? 0)));
6552
6732
  const end = input.zoom ? Math.max(start + 2, Math.min(count, Math.ceil(input.zoom.end))) : count;
@@ -6585,17 +6765,22 @@ function layoutCandlestick(input) {
6585
6765
  .filter((o) => !o.hidden)
6586
6766
  .flatMap((o) => o.values.map((v) => (isFinite(v) ? cmp(v) : v))),
6587
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
+ ];
6588
6773
  const outer = plotBox(width, height, {
6589
6774
  ...MARGIN$1,
6590
- top: MARGIN$1.top + legendTop(input) + (selector.length ? SELECTOR_HEIGHT : 0),
6775
+ top: MARGIN$1.top + legendTop(input, legend, width) + (selector.length ? SELECTOR_HEIGHT : 0),
6591
6776
  bottom: MARGIN$1.bottom + brushRoom,
6592
6777
  left: input.scale === 'log'
6593
6778
  ? MARGIN$1.left
6594
- : valueAxisRoom(prices, makeFormatter(pctFormat ?? input.format)),
6779
+ : valueAxisRoom(prices, makeFormatter(pctFormat ?? input.yAxis?.format ?? input.format)),
6595
6780
  });
6596
6781
  const volumeH = hasVolume ? outer.height * VOLUME_SHARE : 0;
6597
6782
  const plot = { ...outer, height: Math.max(0, outer.height - volumeH - (hasVolume ? 8 : 0)) };
6598
- const y = axis(prices, [plot.y + plot.height, plot.y], { scale: input.scale === 'log' ? 'log' : 'linear', format: pctFormat }, input.format);
6783
+ const y = axis(prices, [plot.y + plot.height, plot.y], { scale: input.scale === 'log' ? 'log' : 'linear', format: pctFormat ?? input.yAxis?.format }, input.format);
6599
6784
  // A time axis places candles by their label's time, so missing sessions leave gaps; the bar
6600
6785
  // width is 80% of the smallest gap. Otherwise every candle takes an equal slot.
6601
6786
  const times = data.map((c) => categoryValue(c.label));
@@ -6623,11 +6808,6 @@ function layoutCandlestick(input) {
6623
6808
  .map((t) => ({ pos: px(tx(t)), label: categoryLabel(t, tickFormat) }));
6624
6809
  }
6625
6810
  const labels = data.map((c) => categoryLabel(c.label, input.xAxis?.format));
6626
- const legend = [
6627
- { name: 'Up', color: UP },
6628
- { name: 'Down', color: DOWN },
6629
- ...overlays.map((o) => ({ name: o.name, color: o.color })),
6630
- ];
6631
6811
  const c0 = canvas(input, width, height, legend);
6632
6812
  const frame = {
6633
6813
  ...c0,
@@ -6671,7 +6851,7 @@ function layoutCandlestick(input) {
6671
6851
  const color = up ? UP : DOWN;
6672
6852
  const cx = px(x.center(i));
6673
6853
  const change = base !== undefined ? ` (${pct(cmp(c.close))})` : '';
6674
- 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)}` : ''}`;
6675
6855
  const item = {
6676
6856
  label: title,
6677
6857
  id: c.id,
@@ -6685,7 +6865,7 @@ function layoutCandlestick(input) {
6685
6865
  `High: ${priceFormat(c.high)}`,
6686
6866
  `Low: ${priceFormat(c.low)}`,
6687
6867
  `Close: ${priceFormat(c.close)}${change}`,
6688
- ...(isFinite(c.volume) ? [`Volume: ${priceFormat(c.volume)}`] : []),
6868
+ ...(isFinite(c.volume) ? [`Volume: ${volumeFormat(c.volume)}`] : []),
6689
6869
  ...overlays
6690
6870
  .filter((o) => !o.hidden)
6691
6871
  .map((o) => `${o.name}: ${isFinite(o.values[i]) ? priceFormat(o.values[i]) : '—'}`),
@@ -6764,7 +6944,7 @@ function layoutCandlestick(input) {
6764
6944
  height: px(Math.max(h, 0)),
6765
6945
  fill: c.close >= c.open ? UP : DOWN,
6766
6946
  opacity: 0.4,
6767
- title: `${labels[i]}: volume ${priceFormat(c.volume)}`,
6947
+ title: `${labels[i]}: volume ${volumeFormat(c.volume)}`,
6768
6948
  });
6769
6949
  });
6770
6950
  }
@@ -6863,7 +7043,7 @@ function layoutCandlestick(input) {
6863
7043
  width: 6,
6864
7044
  height: box.height - 16,
6865
7045
  fill: CHART_COLORS.axis,
6866
- rx: 2,
7046
+ rx: 8,
6867
7047
  }, {
6868
7048
  kind: 'rect',
6869
7049
  part: 'brush-handle',
@@ -6872,7 +7052,7 @@ function layoutCandlestick(input) {
6872
7052
  width: 6,
6873
7053
  height: box.height - 16,
6874
7054
  fill: CHART_COLORS.axis,
6875
- rx: 2,
7055
+ rx: 8,
6876
7056
  });
6877
7057
  hit.brush = { box, window };
6878
7058
  }
@@ -6964,19 +7144,20 @@ function layoutWaterfall(input) {
6964
7144
  flat: true,
6965
7145
  });
6966
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
+ ];
6967
7151
  const plot = plotBox(width, height, {
6968
7152
  ...MARGIN$1,
6969
- top: MARGIN$1.top + legendTop(input),
7153
+ top: MARGIN$1.top + legendTop(input, legend, width),
6970
7154
  left: Math.max(MARGIN$1.left, widest),
6971
7155
  right: input.labels && horizontal ? 56 : MARGIN$1.right,
6972
7156
  });
6973
7157
  const totals = steps.flatMap((s) => [s.from, s.to]);
6974
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);
6975
7159
  const band = bandScale(steps.length, horizontal ? [plot.y, plot.y + plot.height] : [plot.x, plot.x + plot.width]);
6976
- const c = canvas(input, width, height, [
6977
- { name: 'Increase', color: up },
6978
- { name: 'Decrease', color: down },
6979
- ]);
7160
+ const c = canvas(input, width, height, legend);
6980
7161
  const categoryTicks = steps.map((s, i) => ({ pos: px(band.center(i)), label: s.name }));
6981
7162
  const shapes = frameShapes({
6982
7163
  ...c,
@@ -7188,8 +7369,9 @@ function layoutGantt(input) {
7188
7369
  : '';
7189
7370
  const widest = Math.max(0, ...rowNames.map((n) => n.length + rowSuffix(n).length)) * CHAR +
7190
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) }));
7191
7373
  const plot = plotBox(width, height, {
7192
- top: 8 + legendTop(input),
7374
+ top: 8 + legendTop(input, legend, width),
7193
7375
  right: 16,
7194
7376
  bottom: 32 + (input.xAxis?.label ? 14 : 0),
7195
7377
  left: Math.min(width / 2, Math.max(64, widest + 16)),
@@ -7421,7 +7603,7 @@ function layoutGantt(input) {
7421
7603
  });
7422
7604
  }
7423
7605
  shapes.push(...overlays, ...annotationShapes(input.annotations, scales));
7424
- 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);
7425
7607
  return {
7426
7608
  ...c,
7427
7609
  shapes: [...shapes, ...c.shapes],
@@ -7586,9 +7768,10 @@ function layoutBoxplot(input) {
7586
7768
  const horizontal = input.orientation === 'horizontal';
7587
7769
  const groups = grouped ? categories : input.series.map((s) => s.name);
7588
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) }));
7589
7772
  const plot = plotBox(width, height, {
7590
7773
  ...MARGIN$1,
7591
- 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),
7592
7775
  left: horizontal ? Math.max(MARGIN$1.left, widest + 16) : MARGIN$1.left,
7593
7776
  bottom: MARGIN$1.bottom + (horizontal && input.yAxis?.label ? 14 : 0),
7594
7777
  });
@@ -7606,7 +7789,7 @@ function layoutBoxplot(input) {
7606
7789
  const slots = grouped ? [...new Set(shown.map((c) => c.si))] : [];
7607
7790
  const slotW = grouped ? band.bandwidth / Math.max(1, slots.length) : band.bandwidth;
7608
7791
  const center = (c) => grouped ? band.start(c.ci) + (slots.indexOf(c.si) + 0.5) * slotW : band.center(c.si);
7609
- 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);
7610
7793
  const categoryTicks = groups.map((g, i) => ({ pos: px(band.center(i)), label: g }));
7611
7794
  const shapes = frameShapes({
7612
7795
  ...c,
@@ -7757,7 +7940,8 @@ function layoutSunburst(input) {
7757
7940
  // Crumbs sit in the hole when there is one, else in a row above the rings.
7758
7941
  const crumbs = input.breadcrumbs && path.length > 0;
7759
7942
  const crumbsAbove = crumbs && inner === 0;
7760
- 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);
7761
7945
  const cx = width / 2;
7762
7946
  const cy = top + (height - top) / 2;
7763
7947
  const radius = Math.max(0, Math.min(width, height - top) / 2 - 8);
@@ -7837,11 +8021,11 @@ function layoutSunburst(input) {
7837
8021
  if (crumbs) {
7838
8022
  const w = path.reduce((s, n) => s + n.name.length * CHAR + 6, 0) + (path.length - 1) * (CHAR + 6);
7839
8023
  texts.push(...(crumbsAbove
7840
- ? breadcrumbShapes(path, 8, legendTop(input) + 10)
8024
+ ? breadcrumbShapes(path, 8, legendTop(input, legend, width) + 10)
7841
8025
  : breadcrumbShapes(path, cx - w / 2, input.centerLabel ? cy + 8 : cy)));
7842
8026
  }
7843
- const c = canvas(input, width, height, nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) })));
7844
- return { ...c, shapes: [...shapes, ...texts, ...c.shapes] };
8027
+ const c = canvas(input, width, height, legend);
8028
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...texts, ...c.shapes]) };
7845
8029
  }
7846
8030
 
7847
8031
  /*
@@ -7905,16 +8089,6 @@ function layoutChord(input) {
7905
8089
  return { ...state, issues };
7906
8090
  const formatValue = makeFormatter(input.format);
7907
8091
  const { width, height } = size(input, 300, 300);
7908
- const top = legendTop(input);
7909
- const cx = width / 2;
7910
- const cy = top + (height - top) / 2;
7911
- // Group names sit outside the rim, so the circle keeps room for the widest one on either side;
7912
- // without it the left-hand label runs off the svg.
7913
- const nameGap = input.ticks ? 8 + TICK + 14 : 8;
7914
- const nameRoom = nameGap + Math.max(0, ...given.map((s) => s.length)) * CHAR + 4;
7915
- const r = Math.max(0, Math.min(width / 2 - nameRoom, (height - top) / 2 - 36));
7916
- const r0 = Math.max(0, r - RING);
7917
- const PAD = ((input.padding ?? 2.3) * Math.PI) / 180;
7918
8092
  // Groups keep their input order (and color) but can be arranged by total flow.
7919
8093
  const cellRaw = (i, j) => {
7920
8094
  const v = given2[i]?.[j];
@@ -7926,6 +8100,17 @@ function layoutChord(input) {
7926
8100
  order.sort((a, b) => input.sort === 'asc' ? totalOf(a) - totalOf(b) : totalOf(b) - totalOf(a));
7927
8101
  const names = order.map((i) => given[i]);
7928
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;
7929
8114
  const n = names.length;
7930
8115
  const selectedId = selectedIds(input.selected)[0];
7931
8116
  const selected = selectedId === undefined ? -1 : names.indexOf(String(selectedId));
@@ -8043,7 +8228,7 @@ function layoutChord(input) {
8043
8228
  }));
8044
8229
  });
8045
8230
  });
8046
- const c = canvas(input, width, height, names.map((name, i) => ({ name, color: colorAt(i) })));
8231
+ const c = canvas(input, width, height, legend);
8047
8232
  return { ...c, shapes: [...shapes, ...c.shapes], issues };
8048
8233
  }
8049
8234
 
@@ -8294,16 +8479,17 @@ function layoutMarimekkoChart(input) {
8294
8479
  const format = makeFormatter(input.format);
8295
8480
  const totalFormat = makeFormatter(input.xAxis?.format ?? input.format);
8296
8481
  const { width, height } = size(input);
8482
+ const legend = segments.map((s) => ({ name: s.name, color: s.color }));
8297
8483
  const plot = plotBox(width, height, {
8298
8484
  ...MARGIN$1,
8299
- top: MARGIN$1.top + legendTop(input),
8485
+ top: MARGIN$1.top + legendTop(input, legend, width),
8300
8486
  bottom: MARGIN$1.bottom + (input.xAxis?.label ? 14 : 0) + (input.xAxis?.format ? 14 : 0),
8301
8487
  });
8302
8488
  const gap = Math.max(0, Math.min(0.2, input.gap ?? 0.01)) * plot.width;
8303
8489
  const gaps = Math.max(0, columns.length - 1);
8304
8490
  const usable = Math.max(0, plot.width - gap * gaps);
8305
8491
  const y = axis([0, 1], [plot.y + plot.height, plot.y], { format: { style: 'percent' } });
8306
- const c = canvas(input, width, height, segments.map((s) => ({ name: s.name, color: s.color })));
8492
+ const c = canvas(input, width, height, legend);
8307
8493
  // Column positions along x, each as wide as its share of the grand total.
8308
8494
  let x = plot.x;
8309
8495
  const placed = columns.map((col) => {
@@ -8365,7 +8551,7 @@ function layoutMarimekkoChart(input) {
8365
8551
  }));
8366
8552
  });
8367
8553
  }
8368
- return { ...c, shapes: [...shapes, ...cells, ...texts, ...c.shapes], issues };
8554
+ return { ...c, shapes: withLabelSurfaces([...shapes, ...cells, ...texts, ...c.shapes]), issues };
8369
8555
  }
8370
8556
 
8371
8557
  /*
@@ -8393,8 +8579,9 @@ function layoutParallelCoordinates(input) {
8393
8579
  const first = dims[0];
8394
8580
  const last = dims[dims.length - 1];
8395
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) }));
8396
8583
  const plot = plotBox(width, height, {
8397
- top: 28 + legendTop(input),
8584
+ top: 28 + legendTop(input, legend, width),
8398
8585
  right: Math.max(16, pad(last)),
8399
8586
  bottom: 16,
8400
8587
  left: Math.max(48, pad(first)),
@@ -8497,7 +8684,7 @@ function layoutParallelCoordinates(input) {
8497
8684
  : undefined,
8498
8685
  };
8499
8686
  });
8500
- const c = canvas(input, width, height, groups.map((g, i) => ({ name: g, color: colorOf(input, i) })));
8687
+ const c = canvas(input, width, height, legend);
8501
8688
  return { ...c, shapes: [...shapes, ...lines, ...c.shapes], issues };
8502
8689
  }
8503
8690
 
@@ -8588,21 +8775,22 @@ function layoutDumbbellChart(input) {
8588
8775
  sorted.sort((a, b) => (input.sort === 'asc' ? key(a) - key(b) : key(b) - key(a)));
8589
8776
  }
8590
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
+ ];
8591
8784
  const plot = plotBox(width, height, {
8592
8785
  ...MARGIN$1,
8593
- 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),
8594
8787
  right: input.labels && !vertical ? 56 : MARGIN$1.right,
8595
8788
  bottom: MARGIN$1.bottom + (!vertical && input.xAxis?.label ? 14 : 0),
8596
8789
  left: vertical ? MARGIN$1.left : Math.max(MARGIN$1.left, widest + 16),
8597
8790
  });
8598
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);
8599
8792
  const band = bandScale(sorted.length, vertical ? [plot.x, plot.x + plot.width] : [plot.y, plot.y + plot.height]);
8600
- const startColor = colorOf(input, 0);
8601
- const endColor = colorOf(input, 1);
8602
- const c = canvas(input, width, height, [
8603
- { name: startLabel, color: startColor },
8604
- { name: endLabel, color: endColor },
8605
- ]);
8793
+ const c = canvas(input, width, height, legend);
8606
8794
  const categoryTicks = sorted.map((d, i) => ({ pos: px(band.center(i)), label: d.name }));
8607
8795
  const shapes = frameShapes({
8608
8796
  ...c,
@@ -8943,7 +9131,10 @@ function layoutTreeChart(input) {
8943
9131
  ]
8944
9132
  : [];
8945
9133
  const c = canvas(input, width, height);
8946
- return { ...c, shapes: [...links, ...marks, ...texts, ...crumbs, ...c.shapes] };
9134
+ return {
9135
+ ...c,
9136
+ shapes: withLabelSurfaces([...links, ...marks, ...texts, ...crumbs, ...c.shapes]),
9137
+ };
8947
9138
  }
8948
9139
  /** An organization chart: the tree with boxes and elbow links. */
8949
9140
  const layoutOrganizationChart = (input) => layoutTreeChart({ ...input, nodeShape: 'box', linkStyle: 'elbow' });
@@ -9018,7 +9209,11 @@ function layoutCirclePacking(input) {
9018
9209
  return { ...state, issues };
9019
9210
  const format = makeFormatter(input.format);
9020
9211
  const { width, height } = size(input, 300, 300);
9021
- 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);
9022
9217
  const pad = input.padding ?? 2;
9023
9218
  const labels = input.labels ?? true;
9024
9219
  const leafValues = [];
@@ -9095,8 +9290,8 @@ function layoutCirclePacking(input) {
9095
9290
  };
9096
9291
  const radius = Math.max(0, Math.min(width, height - top) / 2 - 8);
9097
9292
  pack(nodes, width / 2, top + (height - top) / 2, radius, undefined);
9098
- const c = canvas(input, width, height, branches.map((n) => ({ name: n.name, color: branchColor.get(nodeId(n)) })));
9099
- 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 };
9100
9295
  }
9101
9296
 
9102
9297
  /*
@@ -9119,7 +9314,8 @@ function layoutIcicle(input) {
9119
9314
  const { width, height } = size(input);
9120
9315
  const horizontal = input.orientation === 'horizontal';
9121
9316
  const crumbs = input.breadcrumbs && path.length > 0;
9122
- 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);
9123
9319
  const plot = plotBox(width, height, { top, right: 0, bottom: 0, left: 0 });
9124
9320
  const gap = input.padding ?? 1;
9125
9321
  const labels = input.labels ?? true;
@@ -9187,9 +9383,9 @@ function layoutIcicle(input) {
9187
9383
  };
9188
9384
  walk(nodes, 0, horizontal ? plot.y : plot.x, horizontal ? plot.height : plot.width, undefined);
9189
9385
  if (crumbs)
9190
- shapes.push(...breadcrumbShapes(path, 8, legendTop(input) + 10));
9191
- const c = canvas(input, width, height, nodes.map((n, i) => ({ name: n.name, color: colorOf(input, i, n.color) })));
9192
- 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 };
9193
9389
  }
9194
9390
 
9195
9391
  /*
@@ -9247,16 +9443,17 @@ function layoutBumpChart(input) {
9247
9443
  });
9248
9444
  const showLabels = input.labels ?? true;
9249
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) }));
9250
9447
  const plot = plotBox(width, height, {
9251
9448
  ...MARGIN$1,
9252
- top: MARGIN$1.top + legendTop(input),
9449
+ top: MARGIN$1.top + legendTop(input, legend, width),
9253
9450
  right: Math.max(MARGIN$1.right, widest),
9254
9451
  bottom: MARGIN$1.bottom + (input.xAxis?.label ? 14 : 0),
9255
9452
  });
9256
9453
  // Rank 1 sits at the top: the axis runs down from half a rank above 1 to half a rank below `top`.
9257
9454
  const y = linearScale([0.5, top + 0.5], [plot.y, plot.y + plot.height]);
9258
9455
  const band = bandScale(count, [plot.x, plot.x + plot.width], 0);
9259
- 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);
9260
9457
  const rankTicks = Array.from({ length: top }, (_, k) => ({
9261
9458
  pos: px(y(k + 1)),
9262
9459
  label: `#${k + 1}`,
@@ -9364,7 +9561,8 @@ function layoutPolarAreaChart(input) {
9364
9561
  return { ...state, issues };
9365
9562
  const format = makeFormatter(input.format);
9366
9563
  const { width, height } = size(input, 300, 300);
9367
- 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);
9368
9566
  const cx = width / 2;
9369
9567
  const cy = top + (height - top) / 2;
9370
9568
  const outer = Math.max(0, Math.min(width, height - top) / 2 - (input.labels ? 40 : 12));
@@ -9426,7 +9624,7 @@ function layoutPolarAreaChart(input) {
9426
9624
  }));
9427
9625
  }
9428
9626
  });
9429
- 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);
9430
9628
  return { ...c, shapes: [...grid, ...sectors, ...texts, ...c.shapes], issues };
9431
9629
  }
9432
9630
 
@@ -9489,15 +9687,16 @@ function layoutSwarmPlot(input) {
9489
9687
  const { width, height } = size(input);
9490
9688
  const horizontal = input.orientation === 'horizontal';
9491
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) }));
9492
9691
  const plot = plotBox(width, height, {
9493
9692
  ...MARGIN$1,
9494
- 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),
9495
9694
  left: horizontal ? Math.max(MARGIN$1.left, widest + 16) : MARGIN$1.left,
9496
9695
  bottom: MARGIN$1.bottom + (horizontal && input.yAxis?.label ? 14 : 0),
9497
9696
  });
9498
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);
9499
9698
  const band = bandScale(input.series.length, horizontal ? [plot.y, plot.y + plot.height] : [plot.x, plot.x + plot.width]);
9500
- 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);
9501
9700
  const categoryTicks = input.series.map((s, i) => ({ pos: px(band.center(i)), label: s.name }));
9502
9701
  const shapes = frameShapes({
9503
9702
  ...c,
@@ -9636,7 +9835,7 @@ function layoutWaffleChart(input) {
9636
9835
  const corner = (k) => horizontal
9637
9836
  ? { x: x0 + (k % columns) * cell, y: y0 + Math.floor(k / columns) * cell }
9638
9837
  : { x: x0 + Math.floor(k / rows) * cell, y: y0 + (rows - 1 - (k % rows)) * cell };
9639
- const rect = (k, fill, title, item, opacity) => {
9838
+ const rect = (k, fill, title, item, opacity, stroke) => {
9640
9839
  const { x, y } = corner(k);
9641
9840
  return {
9642
9841
  kind: 'rect',
@@ -9646,6 +9845,7 @@ function layoutWaffleChart(input) {
9646
9845
  width: px(Math.max(0.5, cell - gap)),
9647
9846
  height: px(Math.max(0.5, cell - gap)),
9648
9847
  fill,
9848
+ stroke,
9649
9849
  opacity,
9650
9850
  rx: input.cellRadius ?? 2,
9651
9851
  title,
@@ -9663,8 +9863,9 @@ function layoutWaffleChart(input) {
9663
9863
  });
9664
9864
  const rest = total - sum;
9665
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.
9666
9867
  for (; k < cells; k++)
9667
- shapes.push(rect(k, CHART_COLORS.grid, restTitle, undefined, 0.6));
9868
+ shapes.push(rect(k, CHART_COLORS.grid, restTitle, undefined, 0.6, CHART_COLORS.axis));
9668
9869
  const c = canvas(legendInput, width, height, legendItems);
9669
9870
  return { ...c, shapes: [...shapes, ...c.shapes], issues };
9670
9871
  }
@@ -9683,6 +9884,7 @@ function layoutWaffleChart(input) {
9683
9884
  /** Draws a core shape list back to front; marks with an `item` carry their index for hit-testing. */
9684
9885
  class SkdxChartShapesComponent {
9685
9886
  constructor() {
9887
+ this.surfaceRadius = chartSurfaceRadius;
9686
9888
  this.shapes = input.required(/* @ts-ignore */
9687
9889
  ...(ngDevMode ? [{ debugName: "shapes" }] : /* istanbul ignore next */ []));
9688
9890
  }
@@ -9718,9 +9920,13 @@ class SkdxChartShapesComponent {
9718
9920
  [attr.width]="s.width"
9719
9921
  [attr.height]="s.height"
9720
9922
  [attr.rx]="s.rx"
9923
+ [style.rx]="surfaceRadius(s.part)"
9721
9924
  [attr.fill]="s.fill"
9722
9925
  [attr.stroke]="s.stroke"
9723
9926
  [attr.fill-opacity]="s.opacity"
9927
+ [attr.pointer-events]="
9928
+ s.part === 'stage-label-background' || s.part === 'label-background' ? 'none' : null
9929
+ "
9724
9930
  >
9725
9931
  @if (s.title) {
9726
9932
  <svg:title>{{ s.title }}</svg:title>
@@ -9763,6 +9969,7 @@ class SkdxChartShapesComponent {
9763
9969
  }
9764
9970
  @case ('text') {
9765
9971
  <svg:text
9972
+ [attr.aria-label]="s.title"
9766
9973
  [attr.data-skdx-part]="s.part"
9767
9974
  [attr.data-skdx-i]="s.item ? $index : null"
9768
9975
  [attr.x]="s.x"
@@ -9817,9 +10024,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
9817
10024
  [attr.width]="s.width"
9818
10025
  [attr.height]="s.height"
9819
10026
  [attr.rx]="s.rx"
10027
+ [style.rx]="surfaceRadius(s.part)"
9820
10028
  [attr.fill]="s.fill"
9821
10029
  [attr.stroke]="s.stroke"
9822
10030
  [attr.fill-opacity]="s.opacity"
10031
+ [attr.pointer-events]="
10032
+ s.part === 'stage-label-background' || s.part === 'label-background' ? 'none' : null
10033
+ "
9823
10034
  >
9824
10035
  @if (s.title) {
9825
10036
  <svg:title>{{ s.title }}</svg:title>
@@ -9862,6 +10073,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
9862
10073
  }
9863
10074
  @case ('text') {
9864
10075
  <svg:text
10076
+ [attr.aria-label]="s.title"
9865
10077
  [attr.data-skdx-part]="s.part"
9866
10078
  [attr.data-skdx-i]="s.item ? $index : null"
9867
10079
  [attr.x]="s.x"
@@ -9957,12 +10169,16 @@ class SkdxChartRootComponent {
9957
10169
  ...(ngDevMode ? [{ debugName: "svg" }] : /* istanbul ignore next */ []));
9958
10170
  this.hover = null;
9959
10171
  this.token = {};
10172
+ this.dismissedCrosshair = signal(null, /* @ts-ignore */
10173
+ ...(ngDevMode ? [{ debugName: "dismissedCrosshair" }] : /* istanbul ignore next */ []));
9960
10174
  this.tip = signal([], /* @ts-ignore */
9961
10175
  ...(ngDevMode ? [{ debugName: "tip" }] : /* istanbul ignore next */ []));
9962
10176
  this.tipShapes = computed(() => {
9963
10177
  const own = this.tip();
9964
10178
  const cross = this.crosshair();
9965
- return own.length || !cross ? own : crosshairAt(this.layout(), cross);
10179
+ return own.length || !cross || cross === this.dismissedCrosshair()
10180
+ ? own
10181
+ : crosshairAt(this.layout(), cross);
9966
10182
  }, /* @ts-ignore */
9967
10183
  ...(ngDevMode ? [{ debugName: "tipShapes" }] : /* istanbul ignore next */ []));
9968
10184
  this.lastCross = null;
@@ -9987,8 +10203,6 @@ class SkdxChartRootComponent {
9987
10203
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
9988
10204
  this.focusable = computed(() => !this.layout().decorative && focusTargets(this.layout()).length > 0, /* @ts-ignore */
9989
10205
  ...(ngDevMode ? [{ debugName: "focusable" }] : /* istanbul ignore next */ []));
9990
- this.descId = computed(() => this.layout().description ? `${this.kind()}-desc-${this.label().replace(/\W+/g, '-')}` : null, /* @ts-ignore */
9991
- ...(ngDevMode ? [{ debugName: "descId" }] : /* istanbul ignore next */ []));
9992
10206
  this.focusShapes = computed(() => focusShapes(this.layout(), this.focused()), /* @ts-ignore */
9993
10207
  ...(ngDevMode ? [{ debugName: "focusShapes" }] : /* istanbul ignore next */ []));
9994
10208
  this.drag = null;
@@ -10094,6 +10308,7 @@ class SkdxChartRootComponent {
10094
10308
  this.shareCrosshair(null);
10095
10309
  }
10096
10310
  onPointerMove(e) {
10311
+ this.dismissedCrosshair.set(null);
10097
10312
  const layout = this.layout();
10098
10313
  if (this.pointers.has(e.pointerId))
10099
10314
  this.pointers.set(e.pointerId, this.viewBox(e).x);
@@ -10308,6 +10523,14 @@ class SkdxChartRootComponent {
10308
10523
  }
10309
10524
  }
10310
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
+ }
10311
10534
  if (!this.focusable())
10312
10535
  return;
10313
10536
  const focused = this.focused();
@@ -10345,14 +10568,13 @@ class SkdxChartRootComponent {
10345
10568
  [attr.role]="layout().decorative ? 'presentation' : 'img'"
10346
10569
  [attr.aria-hidden]="layout().decorative ? 'true' : null"
10347
10570
  [attr.aria-label]="layout().decorative ? null : label()"
10348
- [attr.aria-describedby]="descId()"
10571
+ [attr.aria-description]="layout().description"
10349
10572
  [attr.tabindex]="focusable() ? 0 : null"
10350
10573
  [attr.data-skdx-chart]="kind()"
10351
10574
  [attr.font-family]="colors.fontFamily"
10352
10575
  [attr.font-size]="colors.fontSize"
10353
10576
  [style.max-width]="'100%'"
10354
10577
  [style.height]="'auto'"
10355
- [style.outline]="'none'"
10356
10578
  [style.cursor]="canZoom() || canPan() ? 'grab' : brushing() ? 'crosshair' : null"
10357
10579
  [style.touch-action]="active() ? 'none' : null"
10358
10580
  [style.--skdx-chart-duration]="duration()"
@@ -10369,7 +10591,7 @@ class SkdxChartRootComponent {
10369
10591
  >
10370
10592
  <title>{{ label() }}</title>
10371
10593
  @if (layout().description) {
10372
- <svg:desc [attr.id]="descId()">{{ layout().description }}</svg:desc>
10594
+ <svg:desc>{{ layout().description }}</svg:desc>
10373
10595
  }
10374
10596
  @if (layout().defs?.length) {
10375
10597
  <svg:defs>
@@ -10415,7 +10637,7 @@ class SkdxChartRootComponent {
10415
10637
  [attr.data-skdx-tooltip]="kind()"
10416
10638
  [style.left.px]="p.x + 12"
10417
10639
  [style.top.px]="p.y + 12"
10418
- 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"
10419
10641
  >
10420
10642
  @for (line of p.lines; track $index) {
10421
10643
  <div [style.font-weight]="$index === 0 && p.lines.length > 1 ? 'bold' : null">
@@ -10476,14 +10698,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10476
10698
  [attr.role]="layout().decorative ? 'presentation' : 'img'"
10477
10699
  [attr.aria-hidden]="layout().decorative ? 'true' : null"
10478
10700
  [attr.aria-label]="layout().decorative ? null : label()"
10479
- [attr.aria-describedby]="descId()"
10701
+ [attr.aria-description]="layout().description"
10480
10702
  [attr.tabindex]="focusable() ? 0 : null"
10481
10703
  [attr.data-skdx-chart]="kind()"
10482
10704
  [attr.font-family]="colors.fontFamily"
10483
10705
  [attr.font-size]="colors.fontSize"
10484
10706
  [style.max-width]="'100%'"
10485
10707
  [style.height]="'auto'"
10486
- [style.outline]="'none'"
10487
10708
  [style.cursor]="canZoom() || canPan() ? 'grab' : brushing() ? 'crosshair' : null"
10488
10709
  [style.touch-action]="active() ? 'none' : null"
10489
10710
  [style.--skdx-chart-duration]="duration()"
@@ -10500,7 +10721,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10500
10721
  >
10501
10722
  <title>{{ label() }}</title>
10502
10723
  @if (layout().description) {
10503
- <svg:desc [attr.id]="descId()">{{ layout().description }}</svg:desc>
10724
+ <svg:desc>{{ layout().description }}</svg:desc>
10504
10725
  }
10505
10726
  @if (layout().defs?.length) {
10506
10727
  <svg:defs>
@@ -10546,7 +10767,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10546
10767
  [attr.data-skdx-tooltip]="kind()"
10547
10768
  [style.left.px]="p.x + 12"
10548
10769
  [style.top.px]="p.y + 12"
10549
- 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"
10550
10771
  >
10551
10772
  @for (line of p.lines; track $index) {
10552
10773
  <div [style.font-weight]="$index === 0 && p.lines.length > 1 ? 'bold' : null">
@@ -10771,10 +10992,19 @@ class SkdxSizedBase {
10771
10992
  };
10772
10993
  }
10773
10994
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxSizedBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
10774
- 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 }); }
10775
10996
  }
10776
10997
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxSizedBase, decorators: [{
10777
- 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
+ }]
10778
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 }] }] } });
10779
11009
  /** Inputs the cartesian charts (bar, line, area) take. */
10780
11010
  class SkdxChartBase extends SkdxSizedBase {
@@ -10899,5 +11129,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
10899
11129
  * Generated bundle index. Do not edit.
10900
11130
  */
10901
11131
 
10902
- 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 };
10903
11133
  //# sourceMappingURL=skdx-angular-charts-internal.mjs.map