@beyondwork/docx-react-component 1.0.52 → 1.0.54
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.
- package/package.json +31 -40
- package/src/api/public-types.ts +67 -7
- package/src/io/chart-preview-resolver.ts +41 -0
- package/src/io/docx-session.ts +217 -23
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +88 -8
- package/src/runtime/document-runtime.ts +182 -9
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +97 -2
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +70 -1
- package/src/runtime/prerender/cache-envelope.ts +30 -0
- package/src/runtime/prerender/customxml-cache.ts +17 -3
- package/src/runtime/prerender/prerender-document.ts +17 -1
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/render/render-kernel.ts +67 -19
- package/src/runtime/surface-projection.ts +28 -0
- package/src/runtime/table-schema.ts +27 -0
- package/src/runtime/table-style-resolver.ts +51 -0
- package/src/ui/WordReviewEditor.tsx +6 -3
- package/src/ui/editor-runtime-boundary.ts +39 -2
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
- package/src/ui-tailwind/theme/editor-theme.css +275 -46
- package/src/ui-tailwind/theme/tokens.css +345 -0
- package/src/ui-tailwind/theme/tokens.ts +313 -0
- package/src/ui-tailwind/theme/use-density.ts +60 -0
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- 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);
|