@beyondwork/docx-react-component 1.0.53 → 1.0.55

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.
Files changed (99) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +125 -7
  3. package/src/index.ts +5 -0
  4. package/src/io/docx-session.ts +27 -3
  5. package/src/io/normalize/normalize-text.ts +1 -0
  6. package/src/io/ooxml/parse-field-switches.ts +134 -0
  7. package/src/io/ooxml/parse-fields.ts +28 -2
  8. package/src/model/canonical-document.ts +13 -2
  9. package/src/runtime/chart/chart-model-store.ts +88 -0
  10. package/src/runtime/chart/chart-snapshot.ts +239 -0
  11. package/src/runtime/collab/checkpoint-store.ts +1 -1
  12. package/src/runtime/collab/event-types.ts +4 -0
  13. package/src/runtime/collab/runtime-collab-sync.ts +1 -2
  14. package/src/runtime/document-runtime.ts +115 -13
  15. package/src/runtime/layout/inert-layout-facet.ts +1 -0
  16. package/src/runtime/layout/layout-engine-version.ts +58 -1
  17. package/src/runtime/layout/layout-invalidation.ts +150 -30
  18. package/src/runtime/layout/page-graph.ts +19 -0
  19. package/src/runtime/layout/paginated-layout-engine.ts +128 -19
  20. package/src/runtime/layout/project-block-fragments.ts +27 -0
  21. package/src/runtime/layout/public-facet.ts +27 -0
  22. package/src/runtime/page-number-format.ts +207 -0
  23. package/src/runtime/render/render-frame-diff.ts +38 -2
  24. package/src/runtime/surface-projection.ts +32 -3
  25. package/src/ui/WordReviewEditor.tsx +57 -3
  26. package/src/ui/headless/comment-decoration-model.ts +60 -5
  27. package/src/ui/headless/revision-decoration-model.ts +94 -6
  28. package/src/ui/shared/revision-filters.ts +16 -6
  29. package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
  30. package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
  31. package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
  32. package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
  33. package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
  34. package/src/ui-tailwind/chart/render/area.tsx +277 -0
  35. package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
  36. package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
  37. package/src/ui-tailwind/chart/render/combo.tsx +85 -0
  38. package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
  39. package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
  40. package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
  41. package/src/ui-tailwind/chart/render/line.tsx +363 -0
  42. package/src/ui-tailwind/chart/render/number-format.ts +120 -16
  43. package/src/ui-tailwind/chart/render/pie.tsx +275 -0
  44. package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
  45. package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
  46. package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
  47. package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
  48. package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
  49. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
  50. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
  51. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
  52. package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
  53. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
  54. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
  55. package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
  56. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
  57. package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
  58. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
  59. package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
  60. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
  61. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
  62. package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
  63. package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
  64. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
  65. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
  66. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
  67. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
  68. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
  69. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
  70. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
  71. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
  72. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
  73. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
  74. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
  75. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +90 -0
  76. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
  77. package/src/ui-tailwind/editor-surface/pm-schema.ts +4 -0
  78. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +14 -0
  79. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
  80. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
  81. package/src/ui-tailwind/index.ts +11 -0
  82. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
  83. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
  84. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
  85. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
  86. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
  87. package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
  88. package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
  89. package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
  90. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
  91. package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
  92. package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
  93. package/src/ui-tailwind/theme/editor-theme.css +249 -22
  94. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
  95. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
  96. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
  97. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
  98. package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
  99. package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Data-label renderer (Stage 4 Slice 4G).
3
+ *
4
+ * Produces `<text>` elements for each data point's label according to
5
+ * the series-level `DataLabelsSpec`. Supports:
6
+ * - `showVal`: raw numeric value (passed through `formatNumber(value,
7
+ * spec.numberFormat?)` — the Stage 3A number-format engine).
8
+ * - `showPercent`: percent of category total (pie) or series total.
9
+ * - `showCatName`: category name.
10
+ * - `showSerName`: series name.
11
+ * - `separator`: default `", "`.
12
+ *
13
+ * Position rules:
14
+ * - Bar/column clustered/stacked: `outEnd` (outside the bar end),
15
+ * `inEnd` (inside the bar end), `inBase` (inside the bar base),
16
+ * `ctr` (center), or `bestFit` (heuristic: outEnd if value fits,
17
+ * else inEnd). Default bar: `outEnd`.
18
+ * - Line/area: `t` (above), `b` (below), `l`, `r`, `ctr`. Default: `t`.
19
+ * - Pie: `ctr` (inside slice), `outEnd` (outside, with leader line
20
+ * NOT rendered here — leader-line geometry is deferred to Stage 7).
21
+ * Default pie: `ctr`.
22
+ * - Scatter/bubble: same as line (`t` default).
23
+ *
24
+ * This module is called by `ChartSurface` after the per-family
25
+ * renderer has committed its SVG, filling the `<g data-role="data-
26
+ * labels"/>` placeholder each renderer emitted.
27
+ */
28
+
29
+ import React from "react";
30
+ import { formatNumber } from "./number-format.ts";
31
+ import type {
32
+ AreaChartModel,
33
+ BarChartModel,
34
+ BubbleChartModel,
35
+ ChartModel,
36
+ DataLabelsSpec,
37
+ LineChartModel,
38
+ PieChartModel,
39
+ ScatterChartModel,
40
+ Series,
41
+ } from "../../../io/ooxml/chart/types.ts";
42
+ import type { ResolvedTheme } from "../../../model/canonical-document.ts";
43
+ import type { PlotAreaLayout } from "../layout/plot-area.ts";
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Public API
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export interface DataLabelsProps {
50
+ model: ChartModel;
51
+ layout: PlotAreaLayout;
52
+ theme: ResolvedTheme | undefined;
53
+ }
54
+
55
+ /**
56
+ * Render data labels for every series in the model whose spec opts in.
57
+ * Callers are responsible for placing the returned elements inside the
58
+ * appropriate `<g data-role="data-labels"/>` container in their SVG.
59
+ */
60
+ export function DataLabels({
61
+ model,
62
+ layout,
63
+ theme,
64
+ }: DataLabelsProps): React.ReactElement | null {
65
+ switch (model.kind) {
66
+ case "bar":
67
+ return <>{renderBarLabels(model, layout, theme)}</>;
68
+ case "line":
69
+ return <>{renderLineLabels(model, layout, theme)}</>;
70
+ case "pie":
71
+ return <>{renderPieLabels(model, layout, theme)}</>;
72
+ case "area":
73
+ return <>{renderAreaLabels(model, layout, theme)}</>;
74
+ case "scatter":
75
+ return <>{renderScatterLabels(model, layout, theme)}</>;
76
+ case "bubble":
77
+ return <>{renderBubbleLabels(model, layout, theme)}</>;
78
+ case "combo":
79
+ return (
80
+ <>
81
+ {model.groups.flatMap((g, i) => {
82
+ const groupProps = { model: g, layout, theme };
83
+ const inner = DataLabels(groupProps);
84
+ return inner ? [React.cloneElement(inner, { key: `combo-labels-${i}` })] : [];
85
+ })}
86
+ </>
87
+ );
88
+ case "unsupported":
89
+ return null;
90
+ }
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Label composition
95
+ // ---------------------------------------------------------------------------
96
+
97
+ interface LabelComposition {
98
+ parts: string[];
99
+ }
100
+
101
+ /**
102
+ * Compose a single data-label string from `spec` + value context.
103
+ *
104
+ * The order matches Word's observed behavior: series name → category
105
+ * name → value/percent → bubble size.
106
+ */
107
+ function composeLabel(
108
+ spec: DataLabelsSpec | undefined,
109
+ ctx: {
110
+ value?: number;
111
+ percent?: number;
112
+ categoryName?: string;
113
+ seriesName?: string;
114
+ bubbleSize?: number;
115
+ },
116
+ ): string {
117
+ if (!spec) return "";
118
+ const sep = spec.separator ?? ", ";
119
+ const parts: string[] = [];
120
+ if (spec.showSerName && ctx.seriesName) parts.push(ctx.seriesName);
121
+ if (spec.showCatName && ctx.categoryName) parts.push(ctx.categoryName);
122
+ if (spec.showVal && ctx.value !== undefined) {
123
+ parts.push(formatNumber(ctx.value, spec.numberFormat));
124
+ }
125
+ if (spec.showPercent && ctx.percent !== undefined) {
126
+ // Honor numberFormat when it's a percent code; else append a "%".
127
+ if (spec.numberFormat && /%/.test(spec.numberFormat)) {
128
+ parts.push(formatNumber(ctx.percent / 100, spec.numberFormat));
129
+ } else {
130
+ parts.push(`${Math.round(ctx.percent * 10) / 10}%`);
131
+ }
132
+ }
133
+ if (spec.showBubbleSize && ctx.bubbleSize !== undefined) {
134
+ parts.push(formatNumber(ctx.bubbleSize, spec.numberFormat));
135
+ }
136
+ return parts.join(sep);
137
+ }
138
+
139
+ function hasLabel(spec: DataLabelsSpec | undefined): boolean {
140
+ if (!spec) return false;
141
+ return (
142
+ spec.showVal ||
143
+ spec.showCatName ||
144
+ spec.showSerName ||
145
+ spec.showPercent ||
146
+ spec.showBubbleSize ||
147
+ spec.showLegendKey
148
+ );
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Per-family renderers
153
+ // ---------------------------------------------------------------------------
154
+
155
+ function renderBarLabels(
156
+ model: BarChartModel,
157
+ layout: PlotAreaLayout,
158
+ _theme: ResolvedTheme | undefined,
159
+ ): React.ReactElement[] {
160
+ const plot = layout.plotRect;
161
+ const horizontal = model.direction === "bar";
162
+ const categoryCount = Math.max(
163
+ 1,
164
+ model.categoryAxis.kind === "category"
165
+ ? model.categoryAxis.categoryLabels.length
166
+ : model.series[0]?.values.length ?? 1,
167
+ );
168
+ const slotSize = (horizontal ? plot.h : plot.w) / categoryCount;
169
+ const valueAxisMin = model.valueAxis.min ?? 0;
170
+ const valueAxisMax = model.valueAxis.max ?? 100;
171
+ const span = Math.max(1e-9, valueAxisMax - valueAxisMin);
172
+ const valueToPlot = (v: number): number => {
173
+ const frac = (v - valueAxisMin) / span;
174
+ return horizontal ? plot.x + frac * plot.w : plot.y + plot.h - frac * plot.h;
175
+ };
176
+
177
+ const out: React.ReactElement[] = [];
178
+ for (let s = 0; s < model.series.length; s++) {
179
+ const series = model.series[s]!;
180
+ const spec = series.dataLabels;
181
+ if (!hasLabel(spec)) continue;
182
+ const pos = spec!.position ?? "outEnd";
183
+ for (let c = 0; c < categoryCount; c++) {
184
+ const v = series.values[c];
185
+ if (v === null || v === undefined || !Number.isFinite(v)) continue;
186
+ const category = model.categoryAxis.kind === "category"
187
+ ? model.categoryAxis.categoryLabels[c]
188
+ : undefined;
189
+ const label = composeLabel(spec, {
190
+ value: v,
191
+ categoryName: category,
192
+ seriesName: series.name,
193
+ });
194
+ if (!label) continue;
195
+ const slotStart = (horizontal ? plot.y : plot.x) + c * slotSize;
196
+ const slotCenter = slotStart + slotSize / 2;
197
+ const valuePos = valueToPlot(v);
198
+ const { x, y, anchor } = positionBarLabel(pos, horizontal, slotCenter, valuePos, v);
199
+ out.push(
200
+ <text
201
+ key={`lbl-${s}-${c}`}
202
+ x={x}
203
+ y={y}
204
+ textAnchor={anchor}
205
+ fontSize={9 * (96 / 72)}
206
+ fill="#595959"
207
+ data-role="data-label"
208
+ data-series-index={s}
209
+ data-category-index={c}
210
+ >
211
+ {label}
212
+ </text>,
213
+ );
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+
219
+ function positionBarLabel(
220
+ pos: NonNullable<DataLabelsSpec["position"]>,
221
+ horizontal: boolean,
222
+ slotCenter: number,
223
+ valuePos: number,
224
+ value: number,
225
+ ): { x: number; y: number; anchor: "start" | "middle" | "end" } {
226
+ const offset = 6; // pt
227
+ if (horizontal) {
228
+ // Bars extend horizontally; label position lives on the x axis.
229
+ const outEndX = valuePos + (value >= 0 ? offset : -offset);
230
+ const inEndX = valuePos - (value >= 0 ? offset : -offset);
231
+ switch (pos) {
232
+ case "outEnd":
233
+ case "bestFit":
234
+ return { x: outEndX, y: slotCenter + 3, anchor: value >= 0 ? "start" : "end" };
235
+ case "inEnd":
236
+ return { x: inEndX, y: slotCenter + 3, anchor: value >= 0 ? "end" : "start" };
237
+ case "ctr":
238
+ return { x: (valuePos + outEndX) / 2, y: slotCenter + 3, anchor: "middle" };
239
+ case "inBase":
240
+ default:
241
+ return { x: outEndX, y: slotCenter + 3, anchor: value >= 0 ? "start" : "end" };
242
+ }
243
+ }
244
+ // Column (vertical bars).
245
+ const outEndY = valuePos + (value >= 0 ? -offset : offset);
246
+ const inEndY = valuePos + (value >= 0 ? offset : -offset);
247
+ switch (pos) {
248
+ case "outEnd":
249
+ case "bestFit":
250
+ return { x: slotCenter, y: outEndY, anchor: "middle" };
251
+ case "inEnd":
252
+ return { x: slotCenter, y: inEndY, anchor: "middle" };
253
+ case "ctr":
254
+ return { x: slotCenter, y: (valuePos + outEndY) / 2, anchor: "middle" };
255
+ case "inBase":
256
+ default:
257
+ return { x: slotCenter, y: outEndY, anchor: "middle" };
258
+ }
259
+ }
260
+
261
+ function renderLineLabels(
262
+ model: LineChartModel,
263
+ layout: PlotAreaLayout,
264
+ _theme: ResolvedTheme | undefined,
265
+ ): React.ReactElement[] {
266
+ const plot = layout.plotRect;
267
+ const categoryCount = Math.max(
268
+ 1,
269
+ model.categoryAxis.kind === "category"
270
+ ? model.categoryAxis.categoryLabels.length
271
+ : model.series[0]?.values.length ?? 1,
272
+ );
273
+ const slotWidth = plot.w / categoryCount;
274
+ const valueAxisMin = model.valueAxis.min ?? 0;
275
+ const valueAxisMax = model.valueAxis.max ?? 100;
276
+ const span = Math.max(1e-9, valueAxisMax - valueAxisMin);
277
+
278
+ const out: React.ReactElement[] = [];
279
+ for (let s = 0; s < model.series.length; s++) {
280
+ const series = model.series[s]!;
281
+ const spec = series.dataLabels;
282
+ if (!hasLabel(spec)) continue;
283
+ for (let c = 0; c < categoryCount; c++) {
284
+ const v = series.values[c];
285
+ if (v === null || v === undefined || !Number.isFinite(v)) continue;
286
+ const category = model.categoryAxis.kind === "category"
287
+ ? model.categoryAxis.categoryLabels[c]
288
+ : undefined;
289
+ const label = composeLabel(spec, {
290
+ value: v,
291
+ categoryName: category,
292
+ seriesName: series.name,
293
+ });
294
+ if (!label) continue;
295
+ const x = plot.x + (c + 0.5) * slotWidth;
296
+ const frac = (v - valueAxisMin) / span;
297
+ const yPoint = plot.y + plot.h - frac * plot.h;
298
+ const y = yPoint - 8; // above the point by default
299
+ out.push(
300
+ <text
301
+ key={`lbl-${s}-${c}`}
302
+ x={x}
303
+ y={y}
304
+ textAnchor="middle"
305
+ fontSize={9 * (96 / 72)}
306
+ fill="#595959"
307
+ data-role="data-label"
308
+ data-series-index={s}
309
+ data-category-index={c}
310
+ >
311
+ {label}
312
+ </text>,
313
+ );
314
+ }
315
+ }
316
+ return out;
317
+ }
318
+
319
+ function renderAreaLabels(
320
+ model: AreaChartModel,
321
+ layout: PlotAreaLayout,
322
+ theme: ResolvedTheme | undefined,
323
+ ): React.ReactElement[] {
324
+ // Area labels use the same logic as line labels — anchor on top of
325
+ // each series' upper boundary. Casting the model to LineChartModel's
326
+ // shape is safe because Series → values lookup is identical.
327
+ return renderLineLabels(
328
+ {
329
+ ...model,
330
+ kind: "line" as const,
331
+ smooth: false,
332
+ marker: false,
333
+ series: model.series.map((s): LineChartModel["series"][number] => ({
334
+ ...s,
335
+ })),
336
+ },
337
+ layout,
338
+ theme,
339
+ );
340
+ }
341
+
342
+ function renderPieLabels(
343
+ model: PieChartModel,
344
+ layout: PlotAreaLayout,
345
+ _theme: ResolvedTheme | undefined,
346
+ ): React.ReactElement[] {
347
+ const plot = layout.plotRect;
348
+ const cx = plot.x + plot.w / 2;
349
+ const cy = plot.y + plot.h / 2;
350
+ const rOuter = Math.max(0, Math.min(plot.w, plot.h) / 2);
351
+ const rInner = model.doughnut
352
+ ? (rOuter * Math.max(10, Math.min(90, model.holeSizePercent ?? 50))) / 100
353
+ : 0;
354
+ const labelRadius = (rInner + rOuter) / 2;
355
+
356
+ const series = model.series[0];
357
+ if (!series) return [];
358
+ // PieSeries has no per-series dataLabels field — pie data labels are
359
+ // declared at the chart level in OOXML (c:dLbls child of c:pieChart).
360
+ // Stage 1 parser doesn't yet surface the chart-level dLbls into
361
+ // PieChartModel, so we treat pie labels as opt-out for now. Renderers
362
+ // can still supply labels via a future chart-level spec.
363
+ const spec: DataLabelsSpec | undefined = undefined;
364
+ if (!hasLabel(spec)) return [];
365
+ const values = series.values;
366
+ const total = values.reduce<number>(
367
+ (sum, v) => (v === null || v === undefined || !Number.isFinite(v) || v <= 0 ? sum : sum + v),
368
+ 0,
369
+ );
370
+ if (total === 0) return [];
371
+
372
+ const startDeg = model.firstSliceAngle ?? 0;
373
+ let cursor = startDeg;
374
+ const out: React.ReactElement[] = [];
375
+ for (let i = 0; i < values.length; i++) {
376
+ const v = values[i];
377
+ if (v === null || v === undefined || !Number.isFinite(v) || v <= 0) continue;
378
+ const sweep = (v / total) * 360;
379
+ const midDeg = cursor + sweep / 2;
380
+ cursor += sweep;
381
+ const svgDeg = midDeg - 90;
382
+ const rad = (svgDeg * Math.PI) / 180;
383
+ const x = cx + labelRadius * Math.cos(rad);
384
+ const y = cy + labelRadius * Math.sin(rad);
385
+ const label = composeLabel(spec, {
386
+ value: v,
387
+ percent: (v / total) * 100,
388
+ categoryName: model.categoryLabels[i],
389
+ seriesName: series.name,
390
+ });
391
+ if (!label) continue;
392
+ out.push(
393
+ <text
394
+ key={`lbl-${i}`}
395
+ x={x}
396
+ y={y}
397
+ textAnchor="middle"
398
+ fontSize={9 * (96 / 72)}
399
+ fill="#595959"
400
+ data-role="data-label"
401
+ data-slice-index={i}
402
+ >
403
+ {label}
404
+ </text>,
405
+ );
406
+ }
407
+ return out;
408
+ }
409
+
410
+ function renderScatterLabels(
411
+ model: ScatterChartModel,
412
+ layout: PlotAreaLayout,
413
+ _theme: ResolvedTheme | undefined,
414
+ ): React.ReactElement[] {
415
+ const plot = layout.plotRect;
416
+ const xMin = model.xAxis.min ?? 0;
417
+ const xMax = model.xAxis.max ?? 1;
418
+ const yMin = model.yAxis.min ?? 0;
419
+ const yMax = model.yAxis.max ?? 1;
420
+ const xSpan = Math.max(1e-9, xMax - xMin);
421
+ const ySpan = Math.max(1e-9, yMax - yMin);
422
+
423
+ const out: React.ReactElement[] = [];
424
+ for (let s = 0; s < model.series.length; s++) {
425
+ const series = model.series[s]!;
426
+ const spec: DataLabelsSpec | undefined = undefined;
427
+ if (!hasLabel(spec)) continue;
428
+ const n = Math.min(series.xValues.length, series.yValues.length);
429
+ for (let i = 0; i < n; i++) {
430
+ const x = series.xValues[i];
431
+ const y = series.yValues[i];
432
+ if (x === null || y === null || !Number.isFinite(x) || !Number.isFinite(y)) continue;
433
+ const px = plot.x + ((x - xMin) / xSpan) * plot.w;
434
+ const py = plot.y + plot.h - ((y - yMin) / ySpan) * plot.h;
435
+ const label = composeLabel(spec, {
436
+ value: y,
437
+ seriesName: series.name,
438
+ });
439
+ if (!label) continue;
440
+ out.push(
441
+ <text
442
+ key={`lbl-${s}-${i}`}
443
+ x={px}
444
+ y={py - 8}
445
+ textAnchor="middle"
446
+ fontSize={9 * (96 / 72)}
447
+ fill="#595959"
448
+ data-role="data-label"
449
+ data-series-index={s}
450
+ data-point-index={i}
451
+ >
452
+ {label}
453
+ </text>,
454
+ );
455
+ }
456
+ }
457
+ return out;
458
+ }
459
+
460
+ function renderBubbleLabels(
461
+ model: BubbleChartModel,
462
+ layout: PlotAreaLayout,
463
+ _theme: ResolvedTheme | undefined,
464
+ ): React.ReactElement[] {
465
+ const plot = layout.plotRect;
466
+ const xMin = model.xAxis.min ?? 0;
467
+ const xMax = model.xAxis.max ?? 1;
468
+ const yMin = model.yAxis.min ?? 0;
469
+ const yMax = model.yAxis.max ?? 1;
470
+ const xSpan = Math.max(1e-9, xMax - xMin);
471
+ const ySpan = Math.max(1e-9, yMax - yMin);
472
+
473
+ const out: React.ReactElement[] = [];
474
+ for (let s = 0; s < model.series.length; s++) {
475
+ const series = model.series[s]!;
476
+ const spec: DataLabelsSpec | undefined = undefined;
477
+ if (!hasLabel(spec)) continue;
478
+ const n = Math.min(series.xValues.length, series.yValues.length, series.sizes.length);
479
+ for (let i = 0; i < n; i++) {
480
+ const x = series.xValues[i];
481
+ const y = series.yValues[i];
482
+ const size = series.sizes[i];
483
+ if (x === null || y === null || size === null) continue;
484
+ const px = plot.x + ((x - xMin) / xSpan) * plot.w;
485
+ const py = plot.y + plot.h - ((y - yMin) / ySpan) * plot.h;
486
+ const label = composeLabel(spec, {
487
+ value: y,
488
+ bubbleSize: size,
489
+ seriesName: series.name,
490
+ });
491
+ if (!label) continue;
492
+ out.push(
493
+ <text
494
+ key={`lbl-${s}-${i}`}
495
+ x={px}
496
+ y={py - 8}
497
+ textAnchor="middle"
498
+ fontSize={9 * (96 / 72)}
499
+ fill="#595959"
500
+ data-role="data-label"
501
+ data-series-index={s}
502
+ data-point-index={i}
503
+ >
504
+ {label}
505
+ </text>,
506
+ );
507
+ }
508
+ }
509
+ return out;
510
+ }
511
+
512
+ // Reference the unused parameter to avoid lint warnings.
513
+ void ({} as Series);