@skdx/angular-charts 0.38.0 → 0.39.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,10 +457,16 @@ 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;
@@ -735,6 +741,12 @@ function axis([min, max], range, options = false, chartFormat) {
735
741
  zero: px(scale(Math.min(Math.max(0, domain[0]), domain[1]))),
736
742
  };
737
743
  }
744
+ /**
745
+ * Left (or right) margin wide enough for the widest tick label a value axis over `[lo, hi]` will
746
+ * draw, never below the default. Charts measure it before laying out so a long label such as
747
+ * "$180.00" is not clipped by the svg edge.
748
+ */
749
+ 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
750
  /** Whether an axis draws its grid lines. */
739
751
  const gridOn = (opts) => opts?.grid !== false;
740
752
  /** Keeps every n-th tick so labels of the given widths do not overlap along `width`. */
@@ -791,6 +803,19 @@ function stateCanvas(input, empty, w, h) {
791
803
  * This file is part of the SkandaDX organization.
792
804
  * Licensed under the MIT License. See LICENSE in the repository root.
793
805
  */
806
+ /**
807
+ * Anchor for a tick label centred at `pos`: `end` when half of it would run past `width`, `start`
808
+ * when it would run past 0, else `middle`. The outermost tick sits on the plot edge, where a long
809
+ * date or currency label would otherwise be clipped by the svg.
810
+ */
811
+ const tickAnchor = (pos, text, width) => {
812
+ const half = (text.length * CHAR) / 2;
813
+ if (pos + half > width)
814
+ return 'end';
815
+ if (pos - half < 0)
816
+ return 'start';
817
+ return 'middle';
818
+ };
794
819
  /** Grid, axis line and tick labels of an axis frame, as shapes. */
795
820
  function frameShapes(frame, options = {}) {
796
821
  const { plot, xTicks, yTicks, baseline } = frame;
@@ -825,7 +850,7 @@ function frameShapes(frame, options = {}) {
825
850
  baseline: 'middle',
826
851
  rotate: rot,
827
852
  })
828
- : label(x, y, text, { part, anchor: 'middle' });
853
+ : label(x, y, text, { part, anchor: tickAnchor(x, text, frame.width) });
829
854
  // Short marks across an axis line at the ticks.
830
855
  const marks = (ticks, along, at, dir, size) => {
831
856
  for (const t of ticks)
@@ -890,7 +915,10 @@ function frameShapes(frame, options = {}) {
890
915
  const ly = px(top ? y - 8 : y + 20);
891
916
  for (const t of a.ticks)
892
917
  shapes.push(top
893
- ? label(t.pos, ly, t.label, { part: 'x-label-top', anchor: 'middle' })
918
+ ? label(t.pos, ly, t.label, {
919
+ part: 'x-label-top',
920
+ anchor: tickAnchor(t.pos, t.label, frame.width),
921
+ })
894
922
  : rotated(t.pos, ly, t.label, 'x-label'));
895
923
  // The first axis keeps its title at the top left; stacked axes title their own label row.
896
924
  if (a.label)
@@ -2343,8 +2371,8 @@ function layoutCartesian(input, defaultType) {
2343
2371
  return MARGIN$1.left;
2344
2372
  const [lo, hi] = extentOf(opts.id);
2345
2373
  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);
2374
+ return (valueAxisRoom([opts.min ?? lo, opts.max ?? hi], f) +
2375
+ (opts.showTicks ? (opts.tickSize ?? 4) : 0));
2348
2376
  };
2349
2377
  // Horizontal charts run the value axes along the bottom (near) and top (far) edges. Under RTL
2350
2378
  // the vertical sides swap so the first axis sits at the right, next to the reader.
@@ -3988,7 +4016,12 @@ function layoutHeatmap(input) {
3988
4016
  part: 'cell-label',
3989
4017
  anchor: 'middle',
3990
4018
  baseline: 'middle',
3991
- fill: 'var(--skdx-color-text-default)',
4019
+ // The cell composites its fill over the surface at the ramp's opacity: a faint cell
4020
+ // is mostly surface and reads with the default text token, a strong one is the fill
4021
+ // and needs the token meant to sit on it. A fixed token fails one end or the other.
4022
+ fill: fill.opacity >= 0.5
4023
+ ? 'var(--skdx-color-text-on-accent)'
4024
+ : 'var(--skdx-color-text-default)',
3992
4025
  }));
3993
4026
  });
3994
4027
  });
@@ -4765,6 +4798,12 @@ function layoutRangeChart(input) {
4765
4798
  * This file is part of the SkandaDX organization.
4766
4799
  * Licensed under the MIT License. See LICENSE in the repository root.
4767
4800
  */
4801
+ /**
4802
+ * Trigonometry rounded to 1e-6: `Math.cos`/`Math.sin` are implementation-defined, so their
4803
+ * last-bit differences between a Node render and a browser render would otherwise reach the
4804
+ * markup and break hydration.
4805
+ */
4806
+ const q = (v) => Math.round(v * 1e6) / 1e6;
4768
4807
  /** Node labels are drawn by default up to this many nodes. */
4769
4808
  const NETWORK_LABEL_LIMIT = 60;
4770
4809
  /** Per-edge titles and keyboard stops are emitted up to this many edges. */
@@ -4834,8 +4873,8 @@ function layoutNetworkChart(input) {
4834
4873
  ys = nodes.map((node) => node.y);
4835
4874
  }
4836
4875
  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));
4876
+ xs = nodes.map((_, i) => q(Math.cos((2 * Math.PI * i) / n - Math.PI / 2)));
4877
+ ys = nodes.map((_, i) => q(Math.sin((2 * Math.PI * i) / n - Math.PI / 2)));
4839
4878
  }
4840
4879
  else {
4841
4880
  ({ 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 +4947,7 @@ function layoutNetworkChart(input) {
4908
4947
  item,
4909
4948
  };
4910
4949
  }
4911
- const len = Math.hypot(b.x - a.x, b.y - a.y) || 1;
4950
+ const len = Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y)) || 1;
4912
4951
  const ux = (b.x - a.x) / len;
4913
4952
  const uy = (b.y - a.y) / len;
4914
4953
  if (input.directed) {
@@ -4999,11 +5038,15 @@ function layoutNetworkChart(input) {
4999
5038
  }
5000
5039
  /**
5001
5040
  * 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.
5041
+ * circle in input order, the seed is quantised and every step afterwards uses only IEEE-exact
5042
+ * arithmetic (`+ - * /` and `sqrt`), so a server render and a browser render agree bit for bit —
5043
+ * `Math.cos`/`Math.sin`/`Math.hypot` are implementation-defined and their last-bit differences
5044
+ * would otherwise amplify over the iterations into a hydration mismatch.
5045
+ * ponytail: O(n²) per iteration; a Barnes–Hut tree if graphs exceed ~2000 nodes.
5003
5046
  */
5004
5047
  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));
5048
+ const xs = Array.from({ length: n }, (_, i) => q(Math.cos((2 * Math.PI * i) / n)));
5049
+ const ys = Array.from({ length: n }, (_, i) => q(Math.sin((2 * Math.PI * i) / n)));
5007
5050
  const k = Math.sqrt(4 / n);
5008
5051
  const dx = new Array(n).fill(0);
5009
5052
  const dy = new Array(n).fill(0);
@@ -5032,7 +5075,7 @@ function force(n, edges, iterations) {
5032
5075
  continue;
5033
5076
  const ex = xs[a] - xs[b];
5034
5077
  const ey = ys[a] - ys[b];
5035
- const d = Math.hypot(ex, ey) || 1e-6;
5078
+ const d = Math.sqrt(ex * ex + ey * ey) || 1e-6;
5036
5079
  const f = (d * d) / k / d;
5037
5080
  dx[a] = dx[a] - ex * f;
5038
5081
  dy[a] = dy[a] - ey * f;
@@ -5040,7 +5083,8 @@ function force(n, edges, iterations) {
5040
5083
  dy[b] = dy[b] + ey * f;
5041
5084
  }
5042
5085
  for (let i = 0; i < n; i++) {
5043
- const d = Math.hypot(dx[i], dy[i]) || 1e-6;
5086
+ const d = Math.sqrt(dx[i] * dx[i] + dy[i] * dy[i]) ||
5087
+ 1e-6;
5044
5088
  const step = Math.min(d, t);
5045
5089
  xs[i] = xs[i] + (dx[i] / d) * step;
5046
5090
  ys[i] = ys[i] + (dy[i] / d) * step;
@@ -5917,6 +5961,11 @@ function seriesCsv(series, categories = []) {
5917
5961
  */
5918
5962
  /** A node's size: its own value, else the sum of its children's. */
5919
5963
  const nodeValue = (n) => isFinite(n.value) ? n.value : (n.children ?? []).reduce((s, c) => s + nodeValue(c), 0);
5964
+ /**
5965
+ * Whether this node or anything under it carries a value. A branch in a tree where nobody sets
5966
+ * `value` sums to 0, which is not data and must not be printed as one.
5967
+ */
5968
+ const hasValue = (n) => isFinite(n.value) || (n.children ?? []).some(hasValue);
5920
5969
  /** Levels below the given nodes (0 for none). */
5921
5970
  const depthOf = (nodes) => nodes.length === 0 ? 0 : 1 + Math.max(...nodes.map((n) => depthOf(n.children ?? [])));
5922
5971
  /** A node's identity: its id, else its name. */
@@ -6507,19 +6556,12 @@ function layoutCandlestick(input) {
6507
6556
  const { width, height } = size(input);
6508
6557
  const brushRoom = input.brush ? BRUSH_HEIGHT + BRUSH_GAP : 0;
6509
6558
  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
6559
  // `compare` plots the change from the first visible close: y values (and the axis format)
6516
6560
  // become fractions while tooltips keep the prices and add the percentage.
6517
6561
  const base = input.compare ? data[0].close : undefined;
6518
6562
  const cmp = (v) => (base !== undefined && base !== 0 ? v / base - 1 : v);
6519
6563
  const pctFormat = base !== undefined ? { style: 'percent', digits: 1 } : undefined;
6520
6564
  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
6565
  // Moving averages run over every close, so zooming never shortens the window; the visible
6524
6566
  // slice is drawn and read in the crosshair.
6525
6567
  const closes = all.map((c) => c.close);
@@ -6535,12 +6577,25 @@ function layoutCandlestick(input) {
6535
6577
  hidden: isHidden(input, name),
6536
6578
  };
6537
6579
  });
6538
- const y = axis(extent([
6580
+ // The price extent is known before the plot box, so the left margin can hold the widest tick
6581
+ // label: a currency price such as "$180.00" overflows the default 48.
6582
+ const prices = extent([
6539
6583
  ...data.flatMap((c) => [cmp(c.low), cmp(c.high)]),
6540
6584
  ...overlays
6541
6585
  .filter((o) => !o.hidden)
6542
6586
  .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);
6587
+ ], false);
6588
+ const outer = plotBox(width, height, {
6589
+ ...MARGIN$1,
6590
+ top: MARGIN$1.top + legendTop(input) + (selector.length ? SELECTOR_HEIGHT : 0),
6591
+ bottom: MARGIN$1.bottom + brushRoom,
6592
+ left: input.scale === 'log'
6593
+ ? MARGIN$1.left
6594
+ : valueAxisRoom(prices, makeFormatter(pctFormat ?? input.format)),
6595
+ });
6596
+ const volumeH = hasVolume ? outer.height * VOLUME_SHARE : 0;
6597
+ 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);
6544
6599
  // A time axis places candles by their label's time, so missing sessions leave gaps; the bar
6545
6600
  // width is 80% of the smallest gap. Otherwise every candle takes an equal slot.
6546
6601
  const times = data.map((c) => categoryValue(c.label));
@@ -7167,7 +7222,10 @@ function layoutGantt(input) {
7167
7222
  y2: plot.y + plot.height,
7168
7223
  stroke: CHART_COLORS.grid,
7169
7224
  });
7170
- shapes.push(label(t.pos, plot.y + plot.height + 20, t.label, { part: 'x-label', anchor: 'middle' }));
7225
+ shapes.push(label(t.pos, plot.y + plot.height + 20, t.label, {
7226
+ part: 'x-label',
7227
+ anchor: tickAnchor(t.pos, t.label, width),
7228
+ }));
7171
7229
  }
7172
7230
  rowNames.forEach((name, i) => {
7173
7231
  const owner = visible.find((t) => (t.group ?? t.name) === name);
@@ -7850,7 +7908,11 @@ function layoutChord(input) {
7850
7908
  const top = legendTop(input);
7851
7909
  const cx = width / 2;
7852
7910
  const cy = top + (height - top) / 2;
7853
- const r = Math.max(0, Math.min(width, height - top) / 2 - 36);
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));
7854
7916
  const r0 = Math.max(0, r - RING);
7855
7917
  const PAD = ((input.padding ?? 2.3) * Math.PI) / 180;
7856
7918
  // Groups keep their input order (and color) but can be arranged by total flow.
@@ -7943,7 +8005,7 @@ function layoutChord(input) {
7943
8005
  const mid = (g.a0 + g.a1) / 2;
7944
8006
  const sin = Math.sin(mid);
7945
8007
  // Ticks sit on the rim; the group name moves out past their labels.
7946
- const nameR = r + (input.ticks ? 8 + TICK + 14 : 8);
8008
+ const nameR = r + nameGap;
7947
8009
  shapes.push(label(px(cx + nameR * sin), px(cy - nameR * Math.cos(mid)), names[i], {
7948
8010
  anchor: sin > 0.1 ? 'start' : sin < -0.1 ? 'end' : 'middle',
7949
8011
  baseline: 'middle',
@@ -8512,6 +8574,10 @@ function layoutDumbbellChart(input) {
8512
8574
  const vertical = input.orientation === 'vertical';
8513
8575
  const startLabel = input.startLabel ?? 'Start';
8514
8576
  const endLabel = input.endLabel ?? 'End';
8577
+ // The legend toggles the two ends like any other series: a hidden end drops its markers, its
8578
+ // value labels and the connector that would dangle, and leaves the value axis to the rest.
8579
+ const hideStart = isHidden(input, startLabel);
8580
+ const hideEnd = isHidden(input, endLabel);
8515
8581
  const sorted = [...data];
8516
8582
  if (input.sort) {
8517
8583
  const key = (d) => input.sort === 'change'
@@ -8529,7 +8595,7 @@ function layoutDumbbellChart(input) {
8529
8595
  bottom: MARGIN$1.bottom + (!vertical && input.xAxis?.label ? 14 : 0),
8530
8596
  left: vertical ? MARGIN$1.left : Math.max(MARGIN$1.left, widest + 16),
8531
8597
  });
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);
8598
+ 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
8599
  const band = bandScale(sorted.length, vertical ? [plot.x, plot.x + plot.width] : [plot.y, plot.y + plot.height]);
8534
8600
  const startColor = colorOf(input, 0);
8535
8601
  const endColor = colorOf(input, 1);
@@ -8575,7 +8641,7 @@ function layoutDumbbellChart(input) {
8575
8641
  index: i,
8576
8642
  value: isFinite(d.end) ? d.end : undefined,
8577
8643
  };
8578
- if (isFinite(d.start) && isFinite(d.end)) {
8644
+ if (!hideStart && !hideEnd && isFinite(d.start) && isFinite(d.end)) {
8579
8645
  const a = at(d.start, mid);
8580
8646
  const b = at(d.end, mid);
8581
8647
  connectors.push({
@@ -8591,11 +8657,11 @@ function layoutDumbbellChart(input) {
8591
8657
  item,
8592
8658
  });
8593
8659
  }
8594
- for (const [v, part, name, fill] of [
8595
- [d.start, 'start', startLabel, startColor],
8596
- [d.end, 'end', endLabel, endColor],
8660
+ for (const [v, part, name, fill, hidden] of [
8661
+ [d.start, 'start', startLabel, startColor, hideStart],
8662
+ [d.end, 'end', endLabel, endColor, hideEnd],
8597
8663
  ]) {
8598
- if (!isFinite(v))
8664
+ if (hidden || !isFinite(v))
8599
8665
  continue;
8600
8666
  const p = at(v, mid);
8601
8667
  const text = `${d.name} · ${name}: ${format(v)}`;
@@ -8801,7 +8867,7 @@ function layoutTreeChart(input) {
8801
8867
  });
8802
8868
  }
8803
8869
  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)' : ''}`;
8870
+ const title = `${p.node.name}${p.node.subtitle ? ` · ${p.node.subtitle}` : ''}${hasValue(p.node) ? `: ${format(value)}` : ''}${p.collapsed ? ' (collapsed)' : ''}`;
8805
8871
  const item = { label: title, id: nodeId(p.node), name: p.node.name, value };
8806
8872
  if (box) {
8807
8873
  marks.push({
@@ -8942,7 +9008,11 @@ function packCircles(radii, padding) {
8942
9008
  function layoutCirclePacking(input) {
8943
9009
  const issues = [];
8944
9010
  const { nodes: roots } = drill(input.data, input.rootId);
8945
- const nodes = cutDepth(roots, input.maxDepth).filter((n) => !isHidden(input, n.name));
9011
+ // The legend lists every top-level branch, hidden ones included, so a hide can be undone; and
9012
+ // a branch keeps the colour of its place in that full list whatever is hidden.
9013
+ const branches = cutDepth(roots, input.maxDepth);
9014
+ const branchColor = new Map(branches.map((n, i) => [nodeId(n), colorOf(input, i, n.color)]));
9015
+ const nodes = branches.filter((n) => !isHidden(input, n.name));
8946
9016
  const state = stateCanvas(input, !nodes.some((n) => nodeValue(n) > 0), 300, 300);
8947
9017
  if (state)
8948
9018
  return { ...state, issues };
@@ -8987,7 +9057,7 @@ function layoutCirclePacking(input) {
8987
9057
  const r = Math.max(0, c.r * k - pad / 2);
8988
9058
  const value = nodeValue(n);
8989
9059
  const branch = (n.children?.length ?? 0) > 0;
8990
- const color = n.color ?? inherited ?? colorOf(input, i);
9060
+ const color = n.color ?? inherited ?? branchColor.get(nodeId(n)) ?? colorOf(input, i);
8991
9061
  const fill = branch ? color : input.colorScale ? (colors.color(value)?.fill ?? color) : color;
8992
9062
  const title = `${n.name}: ${format(value)}`;
8993
9063
  shapes.push({
@@ -9025,7 +9095,7 @@ function layoutCirclePacking(input) {
9025
9095
  };
9026
9096
  const radius = Math.max(0, Math.min(width, height - top) / 2 - 8);
9027
9097
  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) })));
9098
+ const c = canvas(input, width, height, branches.map((n) => ({ name: n.name, color: branchColor.get(nodeId(n)) })));
9029
9099
  return { ...c, shapes: [...shapes, ...texts, ...c.shapes], issues };
9030
9100
  }
9031
9101
 
@@ -9540,7 +9610,23 @@ function layoutWaffleChart(input) {
9540
9610
  const share = (v) => (total > 0 ? v / total : 0);
9541
9611
  const counts = waffleCounts(data.map((d) => (isHidden(input, d.name) ? 0 : Math.min(1, share(d.value)) * cells)));
9542
9612
  const { width, height } = size(input, 300, 300);
9543
- const top = legendTop(input);
9613
+ // `labels` appends the share to each legend entry; the entry still toggles by the datum's name.
9614
+ const shares = new Map(data.map((d) => [d.name, `${Math.round(share(d.value) * 100)}%`]));
9615
+ const legend = legendOptions(input);
9616
+ const legendItems = input.data.map((d, i) => ({
9617
+ name: d.name,
9618
+ color: colorOf(input, i, d.color),
9619
+ }));
9620
+ const legendInput = input.labels && legend
9621
+ ? {
9622
+ ...input,
9623
+ legend: {
9624
+ ...legend,
9625
+ formatter: (n) => `${legend.formatter?.(n) ?? n} ${shares.get(n) ?? ''}`,
9626
+ },
9627
+ }
9628
+ : input;
9629
+ const top = legendTop(legendInput, legendItems, width);
9544
9630
  const gap = Math.max(0, input.cellGap ?? 2);
9545
9631
  const cell = Math.max(1, Math.min((width - 2 * PAD) / columns, (height - top - 2 * PAD) / rows));
9546
9632
  const x0 = (width - cell * columns) / 2;
@@ -9579,18 +9665,7 @@ function layoutWaffleChart(input) {
9579
9665
  const restTitle = rest > 0 ? `Other: ${format(rest)} (${Math.round(share(rest) * 100)}%)` : 'Empty';
9580
9666
  for (; k < cells; k++)
9581
9667
  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) })));
9668
+ const c = canvas(legendInput, width, height, legendItems);
9594
9669
  return { ...c, shapes: [...shapes, ...c.shapes], issues };
9595
9670
  }
9596
9671