@sliiu/present-renderer 0.0.0-stage → 0.1.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.
Files changed (126) hide show
  1. package/COPYRIGHT +5 -0
  2. package/LICENSE +661 -0
  3. package/THIRD-PARTY-NOTICES.md +221 -0
  4. package/dist/cjs/arrow-geom.js +38 -0
  5. package/dist/cjs/canvas-backend.js +1368 -0
  6. package/dist/cjs/cell-borders.js +25 -0
  7. package/dist/cjs/chart-canvas.js +1059 -0
  8. package/dist/cjs/chart-ex-render.js +275 -0
  9. package/dist/cjs/chart-geom.js +313 -0
  10. package/dist/cjs/chart-svg.js +1110 -0
  11. package/dist/cjs/compose.js +74 -0
  12. package/dist/cjs/effect-clip.js +652 -0
  13. package/dist/cjs/entrance-hide.js +63 -0
  14. package/dist/cjs/font-stack.js +103 -0
  15. package/dist/cjs/hit-geom.js +50 -0
  16. package/dist/cjs/hybrid-backend.js +93 -0
  17. package/dist/cjs/hybrid-segments.js +54 -0
  18. package/dist/cjs/index.js +70 -0
  19. package/dist/cjs/linked-src.js +116 -0
  20. package/dist/cjs/metafile-preview.js +142 -0
  21. package/dist/cjs/model3d-frame.js +199 -0
  22. package/dist/cjs/model3d-render.js +78 -0
  23. package/dist/cjs/morph-transition.js +57 -0
  24. package/dist/cjs/node-affine.js +79 -0
  25. package/dist/cjs/ole-render.js +291 -0
  26. package/dist/cjs/package.json +1 -0
  27. package/dist/cjs/paint-canvas.js +701 -0
  28. package/dist/cjs/paint-svg.js +674 -0
  29. package/dist/cjs/scene-node.js +762 -0
  30. package/dist/cjs/selection.js +130 -0
  31. package/dist/cjs/shape-effects.js +66 -0
  32. package/dist/cjs/shape-front.js +38 -0
  33. package/dist/cjs/stage-chrome.js +15 -0
  34. package/dist/cjs/svg-backend.js +2172 -0
  35. package/dist/cjs/text-autofit.js +48 -0
  36. package/dist/cjs/text-hit.js +284 -0
  37. package/dist/cjs/text-layout.js +408 -0
  38. package/dist/cjs/text-shared.js +238 -0
  39. package/dist/cjs/text-stacked.js +82 -0
  40. package/dist/cjs/text-warp.js +377 -0
  41. package/dist/cjs/text-wordart.js +33 -0
  42. package/dist/cjs/video-frame.js +113 -0
  43. package/dist/cjs/webgl-model3d-backend.js +285 -0
  44. package/dist/cjs/webgl-shape-backend.js +360 -0
  45. package/dist/esm/arrow-geom.js +33 -0
  46. package/dist/esm/canvas-backend.js +1363 -0
  47. package/dist/esm/cell-borders.js +21 -0
  48. package/dist/esm/chart-canvas.js +1056 -0
  49. package/dist/esm/chart-ex-render.js +261 -0
  50. package/dist/esm/chart-geom.js +296 -0
  51. package/dist/esm/chart-svg.js +1107 -0
  52. package/dist/esm/compose.js +71 -0
  53. package/dist/esm/effect-clip.js +648 -0
  54. package/dist/esm/entrance-hide.js +59 -0
  55. package/dist/esm/font-stack.js +100 -0
  56. package/dist/esm/hit-geom.js +45 -0
  57. package/dist/esm/hybrid-backend.js +89 -0
  58. package/dist/esm/hybrid-segments.js +51 -0
  59. package/dist/esm/index.js +24 -0
  60. package/dist/esm/linked-src.js +109 -0
  61. package/dist/esm/metafile-preview.js +135 -0
  62. package/dist/esm/model3d-frame.js +158 -0
  63. package/dist/esm/model3d-render.js +73 -0
  64. package/dist/esm/morph-transition.js +54 -0
  65. package/dist/esm/node-affine.js +71 -0
  66. package/dist/esm/ole-render.js +286 -0
  67. package/dist/esm/paint-canvas.js +693 -0
  68. package/dist/esm/paint-svg.js +670 -0
  69. package/dist/esm/scene-node.js +750 -0
  70. package/dist/esm/selection.js +122 -0
  71. package/dist/esm/shape-effects.js +60 -0
  72. package/dist/esm/shape-front.js +35 -0
  73. package/dist/esm/stage-chrome.js +12 -0
  74. package/dist/esm/svg-backend.js +2168 -0
  75. package/dist/esm/text-autofit.js +44 -0
  76. package/dist/esm/text-hit.js +280 -0
  77. package/dist/esm/text-layout.js +400 -0
  78. package/dist/esm/text-shared.js +221 -0
  79. package/dist/esm/text-stacked.js +78 -0
  80. package/dist/esm/text-warp.js +371 -0
  81. package/dist/esm/text-wordart.js +30 -0
  82. package/dist/esm/video-frame.js +109 -0
  83. package/dist/esm/webgl-model3d-backend.js +248 -0
  84. package/dist/esm/webgl-shape-backend.js +323 -0
  85. package/dist/types/arrow-geom.d.ts +10 -0
  86. package/dist/types/canvas-backend.d.ts +55 -0
  87. package/dist/types/cell-borders.d.ts +9 -0
  88. package/dist/types/chart-canvas.d.ts +2 -0
  89. package/dist/types/chart-ex-render.d.ts +79 -0
  90. package/dist/types/chart-geom.d.ts +112 -0
  91. package/dist/types/chart-svg.d.ts +2 -0
  92. package/dist/types/compose.d.ts +2 -0
  93. package/dist/types/effect-clip.d.ts +10 -0
  94. package/dist/types/entrance-hide.d.ts +16 -0
  95. package/dist/types/font-stack.d.ts +9 -0
  96. package/dist/types/hit-geom.d.ts +15 -0
  97. package/dist/types/hybrid-backend.d.ts +21 -0
  98. package/dist/types/hybrid-segments.d.ts +11 -0
  99. package/dist/types/index.d.ts +24 -0
  100. package/dist/types/linked-src.d.ts +13 -0
  101. package/dist/types/metafile-preview.d.ts +11 -0
  102. package/dist/types/model3d-frame.d.ts +8 -0
  103. package/dist/types/model3d-render.d.ts +4 -0
  104. package/dist/types/morph-transition.d.ts +15 -0
  105. package/dist/types/node-affine.d.ts +22 -0
  106. package/dist/types/ole-render.d.ts +7 -0
  107. package/dist/types/paint-canvas.d.ts +22 -0
  108. package/dist/types/paint-svg.d.ts +19 -0
  109. package/dist/types/scene-node.d.ts +128 -0
  110. package/dist/types/selection.d.ts +22 -0
  111. package/dist/types/shape-effects.d.ts +13 -0
  112. package/dist/types/shape-front.d.ts +4 -0
  113. package/dist/types/stage-chrome.d.ts +8 -0
  114. package/dist/types/svg-backend.d.ts +88 -0
  115. package/dist/types/text-autofit.d.ts +7 -0
  116. package/dist/types/text-hit.d.ts +7 -0
  117. package/dist/types/text-layout.d.ts +35 -0
  118. package/dist/types/text-shared.d.ts +86 -0
  119. package/dist/types/text-stacked.d.ts +11 -0
  120. package/dist/types/text-warp.d.ts +37 -0
  121. package/dist/types/text-wordart.d.ts +18 -0
  122. package/dist/types/video-frame.d.ts +2 -0
  123. package/dist/types/webgl-model3d-backend.d.ts +25 -0
  124. package/dist/types/webgl-shape-backend.d.ts +33 -0
  125. package/package.json +33 -4
  126. package/README.md +0 -3
@@ -0,0 +1,1107 @@
1
+ import { layoutBoxWhisker, layoutFunnel, layoutHistogram, layoutPareto, layoutRegion, layoutSunburst, layoutTreemap, layoutWaterfall, normalizeChartExSlices, polygonPath, sunburstArcPath, } from "./chart-ex-render.js";
2
+ import { applyValueAxisScale, barGeometry, barLabelAnchor, chart3DOffset, chartPadding, chartTextStyle, estimateTextWidth, comboXyPlotX, linearFit, niceTicks, pointLabelText, seriesXyPoints, shadeHex, surfaceIsoQuads } from "./chart-geom.js";
3
+ /** Text / line colours of the chart being drawn; set once per `renderChart` (drawing is synchronous). */
4
+ let ts = chartTextStyle({});
5
+ const SVG_NS = "http://www.w3.org/2000/svg";
6
+ function poly(g, pts, fill) {
7
+ const p = document.createElementNS(SVG_NS, "polygon");
8
+ p.setAttribute("points", pts);
9
+ p.setAttribute("fill", fill);
10
+ g.appendChild(p);
11
+ }
12
+ function appendExtrudedRect(g, x, y, w, h, color, off) {
13
+ if (off && (off.dx || off.dy) && w > 0 && h > 0) {
14
+ const { dx, dy } = off;
15
+ poly(g, `${x},${y} ${x + dx},${y + dy} ${x + w + dx},${y + dy} ${x + w},${y}`, shadeHex(color, 1.18));
16
+ poly(g, `${x + w},${y} ${x + w + dx},${y + dy} ${x + w + dx},${y + h + dy} ${x + w},${y + h}`, shadeHex(color, 0.72));
17
+ }
18
+ const rect = document.createElementNS(SVG_NS, "rect");
19
+ rect.setAttribute("x", String(x));
20
+ rect.setAttribute("y", String(y));
21
+ rect.setAttribute("width", String(w));
22
+ rect.setAttribute("height", String(h));
23
+ rect.setAttribute("fill", color);
24
+ g.appendChild(rect);
25
+ }
26
+ export function renderChart(g, shape) {
27
+ while (g.firstChild)
28
+ g.removeChild(g.firstChild);
29
+ ts = chartTextStyle(shape.chart);
30
+ const { x, y, w, h } = shape.bbox;
31
+ const padTop = shape.chart.title ? 28 : 8;
32
+ const pads = chartPadding(shape.chart, ts.size);
33
+ const padBottom = pads.bottom;
34
+ const padLeft = 40;
35
+ const padRight = 12;
36
+ if (shape.chart.title) {
37
+ const t = document.createElementNS(SVG_NS, "text");
38
+ t.setAttribute("x", String(x + w / 2));
39
+ t.setAttribute("y", String(y + 18));
40
+ t.setAttribute("text-anchor", "middle");
41
+ t.setAttribute("font-size", String(ts.size + 1));
42
+ t.setAttribute("font-weight", "600");
43
+ t.setAttribute("fill", ts.text);
44
+ t.textContent = shape.chart.title;
45
+ g.appendChild(t);
46
+ }
47
+ const plot = {
48
+ x: x + padLeft,
49
+ y: y + padTop,
50
+ w: w - padLeft - padRight,
51
+ h: h - padTop - padBottom,
52
+ };
53
+ const def = shape.chart;
54
+ if (hasCombo(def)) {
55
+ drawCombo(g, def, plot);
56
+ }
57
+ else {
58
+ switch (def.type) {
59
+ case "bar":
60
+ case "column":
61
+ drawBar(g, def, plot);
62
+ break;
63
+ case "line":
64
+ drawLine(g, def, plot);
65
+ break;
66
+ case "pie":
67
+ case "doughnut":
68
+ drawPie(g, def, plot);
69
+ break;
70
+ case "area":
71
+ drawArea(g, def, plot);
72
+ break;
73
+ case "scatter":
74
+ drawScatter(g, def, plot);
75
+ break;
76
+ case "bubble":
77
+ drawBubble(g, def, plot);
78
+ break;
79
+ case "radar":
80
+ drawRadar(g, def, plot);
81
+ break;
82
+ case "stock":
83
+ drawStock(g, def, plot);
84
+ break;
85
+ case "surface":
86
+ drawSurface(g, def, plot);
87
+ break;
88
+ case "sunburst":
89
+ drawSunburst(g, def, plot);
90
+ break;
91
+ case "treemap":
92
+ drawTreemap(g, def, plot);
93
+ break;
94
+ case "waterfall":
95
+ drawWaterfall(g, def, plot);
96
+ break;
97
+ case "funnel":
98
+ drawFunnel(g, def, plot);
99
+ break;
100
+ case "pareto":
101
+ drawPareto(g, def, plot);
102
+ break;
103
+ case "histogram":
104
+ drawHistogram(g, def, plot);
105
+ break;
106
+ case "boxWhisker":
107
+ drawBoxWhisker(g, def, plot);
108
+ break;
109
+ case "region":
110
+ drawRegion(g, def, plot);
111
+ break;
112
+ default:
113
+ drawChartExFallback(g, def, plot);
114
+ break;
115
+ }
116
+ }
117
+ if (!hasCombo(def))
118
+ overlayGuides(g, def, plot);
119
+ overlayDataLabels(g, def, plot);
120
+ if (def.legend)
121
+ drawLegend(g, def, { x, y: y + h - pads.legend, w, h: pads.legend });
122
+ }
123
+ function drawAxes(g, plot, ticks, categories, isBar) {
124
+ const min = ticks[0];
125
+ const max = ticks[ticks.length - 1];
126
+ const xAxis = document.createElementNS(SVG_NS, "line");
127
+ xAxis.setAttribute("x1", String(plot.x));
128
+ xAxis.setAttribute("y1", String(plot.y + plot.h));
129
+ xAxis.setAttribute("x2", String(plot.x + plot.w));
130
+ xAxis.setAttribute("y2", String(plot.y + plot.h));
131
+ xAxis.setAttribute("stroke", ts.axis);
132
+ g.appendChild(xAxis);
133
+ const yAxis = document.createElementNS(SVG_NS, "line");
134
+ yAxis.setAttribute("x1", String(plot.x));
135
+ yAxis.setAttribute("y1", String(plot.y));
136
+ yAxis.setAttribute("x2", String(plot.x));
137
+ yAxis.setAttribute("y2", String(plot.y + plot.h));
138
+ yAxis.setAttribute("stroke", ts.axis);
139
+ g.appendChild(yAxis);
140
+ // barDir="bar" (§21.2.2.17) turns the value axis along the bottom: ticks run left→right with
141
+ // vertical gridlines, and the left edge belongs to the category labels.
142
+ for (const t of ticks) {
143
+ const f = (t - min) / (max - min);
144
+ const grid = document.createElementNS(SVG_NS, "line");
145
+ const lab = document.createElementNS(SVG_NS, "text");
146
+ if (isBar) {
147
+ const nx = plot.x + f * plot.w;
148
+ grid.setAttribute("x1", String(nx));
149
+ grid.setAttribute("y1", String(plot.y));
150
+ grid.setAttribute("x2", String(nx));
151
+ grid.setAttribute("y2", String(plot.y + plot.h));
152
+ lab.setAttribute("x", String(nx));
153
+ lab.setAttribute("y", String(plot.y + plot.h + 14));
154
+ lab.setAttribute("text-anchor", "middle");
155
+ }
156
+ else {
157
+ const ny = plot.y + plot.h - f * plot.h;
158
+ grid.setAttribute("x1", String(plot.x));
159
+ grid.setAttribute("y1", String(ny));
160
+ grid.setAttribute("x2", String(plot.x + plot.w));
161
+ grid.setAttribute("y2", String(ny));
162
+ lab.setAttribute("x", String(plot.x - 6));
163
+ lab.setAttribute("y", String(ny + 3));
164
+ lab.setAttribute("text-anchor", "end");
165
+ }
166
+ grid.setAttribute("stroke", ts.grid);
167
+ g.appendChild(grid);
168
+ lab.setAttribute("font-size", String(ts.size));
169
+ lab.setAttribute("fill", ts.muted);
170
+ lab.textContent = String(t);
171
+ g.appendChild(lab);
172
+ }
173
+ if (!isBar) {
174
+ const step = categories.length > 0 ? plot.w / categories.length : 0;
175
+ categories.forEach((c, i) => {
176
+ const lab = document.createElementNS(SVG_NS, "text");
177
+ lab.setAttribute("x", String(plot.x + step * (i + 0.5)));
178
+ lab.setAttribute("y", String(plot.y + plot.h + 14));
179
+ lab.setAttribute("text-anchor", "middle");
180
+ lab.setAttribute("font-size", String(ts.size));
181
+ lab.setAttribute("fill", ts.muted);
182
+ lab.textContent = c;
183
+ g.appendChild(lab);
184
+ });
185
+ }
186
+ else {
187
+ const step = categories.length > 0 ? plot.h / categories.length : 0;
188
+ categories.forEach((c, i) => {
189
+ const lab = document.createElementNS(SVG_NS, "text");
190
+ lab.setAttribute("x", String(plot.x - 6));
191
+ lab.setAttribute("y", String(plot.y + step * (i + 0.5) + 3));
192
+ lab.setAttribute("text-anchor", "end");
193
+ lab.setAttribute("font-size", String(ts.size));
194
+ lab.setAttribute("fill", ts.muted);
195
+ lab.textContent = c;
196
+ g.appendChild(lab);
197
+ });
198
+ }
199
+ }
200
+ function drawChartExFallback(g, def, plot) {
201
+ const categories = def.categories?.length
202
+ ? def.categories
203
+ : Array.from({ length: Math.max(1, def.series[0]?.values.length ?? 1) }, (_, i) => `Cat ${i + 1}`);
204
+ const series = def.series.length > 0
205
+ ? def.series
206
+ : [{ name: "Series 1", values: categories.map(() => 0) }];
207
+ const fallback = {
208
+ type: "column",
209
+ title: def.title,
210
+ categories,
211
+ series,
212
+ legend: def.legend,
213
+ colors: def.colors,
214
+ varyColors: def.varyColors ?? true,
215
+ };
216
+ drawBar(g, fallback, plot);
217
+ }
218
+ function drawTreemap(g, def, plot) {
219
+ const slices = normalizeChartExSlices(def);
220
+ for (const cell of layoutTreemap(slices, plot)) {
221
+ const rect = document.createElementNS(SVG_NS, "rect");
222
+ rect.setAttribute("x", String(cell.x));
223
+ rect.setAttribute("y", String(cell.y));
224
+ rect.setAttribute("width", String(cell.w));
225
+ rect.setAttribute("height", String(cell.h));
226
+ rect.setAttribute("fill", cell.color);
227
+ rect.setAttribute("stroke", "#ffffff");
228
+ rect.setAttribute("stroke-width", "1.5");
229
+ g.appendChild(rect);
230
+ if (cell.w > 36 && cell.h > 18) {
231
+ const t = document.createElementNS(SVG_NS, "text");
232
+ t.setAttribute("x", String(cell.x + 6));
233
+ t.setAttribute("y", String(cell.y + 14));
234
+ t.setAttribute("font-size", String(ts.size));
235
+ t.setAttribute("fill", "#f8fafc");
236
+ t.textContent = cell.label;
237
+ g.appendChild(t);
238
+ }
239
+ }
240
+ }
241
+ function drawSunburst(g, def, plot) {
242
+ const slices = normalizeChartExSlices(def);
243
+ for (const arc of layoutSunburst(slices, plot)) {
244
+ const path = document.createElementNS(SVG_NS, "path");
245
+ path.setAttribute("d", sunburstArcPath(arc));
246
+ path.setAttribute("fill", arc.color);
247
+ path.setAttribute("stroke", "#ffffff");
248
+ path.setAttribute("stroke-width", "1");
249
+ g.appendChild(path);
250
+ }
251
+ }
252
+ function drawWaterfall(g, def, plot) {
253
+ const slices = normalizeChartExSlices(def, { preserveSign: true });
254
+ const bars = layoutWaterfall(slices, plot);
255
+ for (const bar of bars) {
256
+ const rect = document.createElementNS(SVG_NS, "rect");
257
+ rect.setAttribute("x", String(bar.x));
258
+ rect.setAttribute("y", String(bar.y));
259
+ rect.setAttribute("width", String(bar.w));
260
+ rect.setAttribute("height", String(bar.h));
261
+ rect.setAttribute("fill", bar.kind === "decrease" ? "#E15759" : bar.color);
262
+ rect.setAttribute("rx", "2");
263
+ g.appendChild(rect);
264
+ }
265
+ for (let i = 0; i < bars.length - 1; i++) {
266
+ const a = bars[i];
267
+ const b = bars[i + 1];
268
+ const line = document.createElementNS(SVG_NS, "line");
269
+ line.setAttribute("x1", String(a.x + a.w));
270
+ line.setAttribute("y1", String(a.y));
271
+ line.setAttribute("x2", String(b.x));
272
+ line.setAttribute("y2", String(b.y));
273
+ line.setAttribute("stroke", ts.axis);
274
+ line.setAttribute("stroke-dasharray", "3 2");
275
+ g.appendChild(line);
276
+ }
277
+ }
278
+ function drawFunnel(g, def, plot) {
279
+ const slices = normalizeChartExSlices(def);
280
+ for (const seg of layoutFunnel(slices, plot)) {
281
+ const path = document.createElementNS(SVG_NS, "path");
282
+ path.setAttribute("d", polygonPath(seg.points));
283
+ path.setAttribute("fill", seg.color);
284
+ path.setAttribute("stroke", "#ffffff");
285
+ path.setAttribute("stroke-width", "1.5");
286
+ g.appendChild(path);
287
+ const cx = seg.points.reduce((s, p) => s + p[0], 0) / seg.points.length;
288
+ const cy = seg.points.reduce((s, p) => s + p[1], 0) / seg.points.length;
289
+ const t = document.createElementNS(SVG_NS, "text");
290
+ t.setAttribute("x", String(cx));
291
+ t.setAttribute("y", String(cy + 4));
292
+ t.setAttribute("text-anchor", "middle");
293
+ t.setAttribute("font-size", String(ts.size));
294
+ t.setAttribute("fill", "#f8fafc");
295
+ t.textContent = seg.label;
296
+ g.appendChild(t);
297
+ }
298
+ }
299
+ function drawColumnBars(g, bars) {
300
+ for (const bar of bars) {
301
+ const rect = document.createElementNS(SVG_NS, "rect");
302
+ rect.setAttribute("x", String(bar.x));
303
+ rect.setAttribute("y", String(bar.y));
304
+ rect.setAttribute("width", String(bar.w));
305
+ rect.setAttribute("height", String(bar.h));
306
+ rect.setAttribute("fill", bar.color);
307
+ rect.setAttribute("rx", "2");
308
+ g.appendChild(rect);
309
+ }
310
+ }
311
+ function drawPareto(g, def, plot) {
312
+ const { bars, line } = layoutPareto(normalizeChartExSlices(def), plot);
313
+ drawColumnBars(g, bars);
314
+ if (line.length >= 2) {
315
+ const path = document.createElementNS(SVG_NS, "polyline");
316
+ path.setAttribute("points", line.map((p) => `${p[0]},${p[1]}`).join(" "));
317
+ path.setAttribute("fill", "none");
318
+ path.setAttribute("stroke", "#E15759");
319
+ path.setAttribute("stroke-width", "2");
320
+ g.appendChild(path);
321
+ for (const [x, y] of line) {
322
+ const dot = document.createElementNS(SVG_NS, "circle");
323
+ dot.setAttribute("cx", String(x));
324
+ dot.setAttribute("cy", String(y));
325
+ dot.setAttribute("r", "3");
326
+ dot.setAttribute("fill", "#E15759");
327
+ g.appendChild(dot);
328
+ }
329
+ }
330
+ }
331
+ function drawHistogram(g, def, plot) {
332
+ drawColumnBars(g, layoutHistogram(normalizeChartExSlices(def), plot));
333
+ }
334
+ function drawBoxWhisker(g, def, plot) {
335
+ for (const box of layoutBoxWhisker(normalizeChartExSlices(def), plot)) {
336
+ const whisker = document.createElementNS(SVG_NS, "line");
337
+ whisker.setAttribute("x1", String(box.cx));
338
+ whisker.setAttribute("y1", String(box.whiskerLow));
339
+ whisker.setAttribute("x2", String(box.cx));
340
+ whisker.setAttribute("y2", String(box.whiskerHigh));
341
+ whisker.setAttribute("stroke", "#374151");
342
+ whisker.setAttribute("stroke-width", "1");
343
+ g.appendChild(whisker);
344
+ const capW = box.boxW * 0.5;
345
+ for (const y of [box.whiskerLow, box.whiskerHigh]) {
346
+ const cap = document.createElementNS(SVG_NS, "line");
347
+ cap.setAttribute("x1", String(box.cx - capW / 2));
348
+ cap.setAttribute("x2", String(box.cx + capW / 2));
349
+ cap.setAttribute("y1", String(y));
350
+ cap.setAttribute("y2", String(y));
351
+ cap.setAttribute("stroke", "#374151");
352
+ g.appendChild(cap);
353
+ }
354
+ const rect = document.createElementNS(SVG_NS, "rect");
355
+ rect.setAttribute("x", String(box.cx - box.boxW / 2));
356
+ rect.setAttribute("y", String(Math.min(box.q1, box.q3)));
357
+ rect.setAttribute("width", String(box.boxW));
358
+ rect.setAttribute("height", String(Math.abs(box.q3 - box.q1)));
359
+ rect.setAttribute("fill", box.color);
360
+ rect.setAttribute("fill-opacity", "0.75");
361
+ g.appendChild(rect);
362
+ const med = document.createElementNS(SVG_NS, "line");
363
+ med.setAttribute("x1", String(box.cx - box.boxW / 2));
364
+ med.setAttribute("x2", String(box.cx + box.boxW / 2));
365
+ med.setAttribute("y1", String(box.median));
366
+ med.setAttribute("y2", String(box.median));
367
+ med.setAttribute("stroke", "#1f2937");
368
+ med.setAttribute("stroke-width", "2");
369
+ g.appendChild(med);
370
+ }
371
+ }
372
+ function drawRegion(g, def, plot) {
373
+ for (const row of layoutRegion(normalizeChartExSlices(def), plot)) {
374
+ const rect = document.createElementNS(SVG_NS, "rect");
375
+ rect.setAttribute("x", String(row.x));
376
+ rect.setAttribute("y", String(row.y));
377
+ rect.setAttribute("width", String(row.w));
378
+ rect.setAttribute("height", String(row.h));
379
+ rect.setAttribute("fill", row.color);
380
+ rect.setAttribute("rx", "2");
381
+ g.appendChild(rect);
382
+ if (row.h > 14) {
383
+ const t = document.createElementNS(SVG_NS, "text");
384
+ t.setAttribute("x", String(row.x + 6));
385
+ t.setAttribute("y", String(row.y + row.h / 2 + 4));
386
+ t.setAttribute("font-size", String(ts.size));
387
+ t.setAttribute("fill", "#f8fafc");
388
+ t.textContent = row.label;
389
+ g.appendChild(t);
390
+ }
391
+ }
392
+ }
393
+ function drawBar(g, def, plot, shared) {
394
+ const cats = def.categories ?? [];
395
+ const series = def.series;
396
+ const { isHoriz, stacked, ticks, tmin, tmax, groupStep, inner, barW, totals } = barGeometry(def, plot, shared?.ticks);
397
+ if (!shared?.skipAxes)
398
+ drawAxes(g, plot, ticks, cats, isHoriz);
399
+ const extrude = chart3DOffset(def);
400
+ for (let ci = 0; ci < cats.length; ci++) {
401
+ let stackPos = 0;
402
+ let stackNeg = 0;
403
+ for (let si = 0; si < series.length; si++) {
404
+ let v = series[si].values[ci] ?? 0;
405
+ if (def.percentStacked)
406
+ v = (totals[ci] ?? 0) > 0 ? v / totals[ci] : 0;
407
+ const color = series[si].color ?? "#3b82f6";
408
+ if (isHoriz) {
409
+ const x0 = plot.x + ((Math.min(0, tmin) - tmin) / (tmax - tmin)) * plot.w;
410
+ const len = (Math.abs(v) / (tmax - tmin)) * plot.w;
411
+ const yCat = plot.y + groupStep * ci + (groupStep - inner) / 2;
412
+ const ySub = stacked ? yCat : yCat + si * barW;
413
+ const left = stacked
414
+ ? plot.x + ((v >= 0 ? stackPos : stackNeg) - tmin) / (tmax - tmin) * plot.w
415
+ : x0 - (v < 0 ? len : 0);
416
+ appendExtrudedRect(g, v >= 0 ? left : left - len, ySub, len, barW, color, extrude);
417
+ }
418
+ else {
419
+ const xCat = plot.x + groupStep * ci + (groupStep - inner) / 2;
420
+ const xSub = stacked ? xCat : xCat + si * barW;
421
+ const baseY = plot.y + plot.h - ((0 - tmin) / (tmax - tmin)) * plot.h;
422
+ const h = (Math.abs(v) / (tmax - tmin)) * plot.h;
423
+ const yBar = stacked
424
+ ? plot.y + plot.h - (((v >= 0 ? stackPos : stackNeg) + (v >= 0 ? v : 0) - tmin) / (tmax - tmin)) * plot.h
425
+ : v >= 0
426
+ ? baseY - h
427
+ : baseY;
428
+ appendExtrudedRect(g, xSub, yBar, barW, h, color, extrude);
429
+ }
430
+ if (v >= 0)
431
+ stackPos += v;
432
+ else
433
+ stackNeg += v;
434
+ }
435
+ }
436
+ }
437
+ function drawLine(g, def, plot, shared) {
438
+ const cats = def.categories ?? [];
439
+ let yMin = 0;
440
+ let yMax = 1;
441
+ for (const s of def.series) {
442
+ for (const v of s.values) {
443
+ if (v > yMax)
444
+ yMax = v;
445
+ if (v < yMin)
446
+ yMin = v;
447
+ }
448
+ }
449
+ ({ yMin, yMax } = applyValueAxisScale(yMin, yMax, def));
450
+ const ticks = shared?.ticks ?? niceTicks(yMin, yMax, 5);
451
+ if (!shared?.skipAxes)
452
+ drawAxes(g, plot, ticks, cats, false);
453
+ const tmin = ticks[0];
454
+ const tmax = ticks[ticks.length - 1];
455
+ const step = (cats.length > 1 ? plot.w / (cats.length - 1) : plot.w) || plot.w;
456
+ const extrude = chart3DOffset(def);
457
+ const layers = extrude && (extrude.dx || extrude.dy) ? 5 : 1;
458
+ for (const s of def.series) {
459
+ const pts = s.values.map((v, i) => {
460
+ const xx = plot.x + step * (cats.length > 1 ? i : 0.5);
461
+ const yy = plot.y + plot.h - ((v - tmin) / (tmax - tmin)) * plot.h;
462
+ return { x: xx, y: yy };
463
+ });
464
+ if (pts.length === 0)
465
+ continue;
466
+ const color = s.color ?? "#3b82f6";
467
+ for (let layer = layers - 1; layer >= 0; layer--) {
468
+ const t = layers === 1 ? 0 : layer / (layers - 1);
469
+ const ox = extrude ? extrude.dx * t : 0;
470
+ const oy = extrude ? extrude.dy * t : 0;
471
+ const path = document.createElementNS(SVG_NS, "polyline");
472
+ path.setAttribute("points", pts.map((p) => `${p.x + ox},${p.y + oy}`).join(" "));
473
+ path.setAttribute("fill", "none");
474
+ path.setAttribute("stroke", layer === 0 ? color : shadeHex(color, 0.55 + 0.08 * (1 - t)));
475
+ path.setAttribute("stroke-width", layer === 0 ? "2" : "3");
476
+ g.appendChild(path);
477
+ }
478
+ if (def.markers) {
479
+ for (const p of pts) {
480
+ const c = document.createElementNS(SVG_NS, "circle");
481
+ c.setAttribute("cx", String(p.x));
482
+ c.setAttribute("cy", String(p.y));
483
+ c.setAttribute("r", "3");
484
+ c.setAttribute("fill", color);
485
+ g.appendChild(c);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ function drawPie(g, def, plot) {
491
+ const s0 = def.series[0];
492
+ if (!s0)
493
+ return;
494
+ const total = s0.values.reduce((a, b) => a + Math.abs(b), 0);
495
+ if (total === 0)
496
+ return;
497
+ const cx0 = plot.x + plot.w / 2;
498
+ const cy0 = plot.y + plot.h / 2;
499
+ const r = Math.min(plot.w, plot.h) / 2 - 4;
500
+ const innerR = def.type === "doughnut" ? r * (def.holeRatio ?? 0.5) : 0;
501
+ const palette = s0.perSliceColors ?? def.colors ?? [];
502
+ const explode = ((s0.explosion ?? 0) / 100) * r;
503
+ const off = chart3DOffset(def);
504
+ const layers = off && (off.dx || off.dy) ? 6 : 1;
505
+ for (let layer = layers - 1; layer >= 0; layer--) {
506
+ const t = layers === 1 ? 0 : layer / (layers - 1);
507
+ const ox = off ? off.dx * t : 0;
508
+ const oy = off ? off.dy * t : 0;
509
+ const shade = layer === 0 ? 1 : 0.55 + 0.08 * (1 - t);
510
+ let start = -Math.PI / 2 + ((def.startAngle ?? 0) * Math.PI) / 180;
511
+ for (let i = 0; i < s0.values.length; i++) {
512
+ const v = Math.abs(s0.values[i]);
513
+ const frac = v / total;
514
+ if (frac <= 0)
515
+ continue;
516
+ const end = start + frac * Math.PI * 2;
517
+ const mid = (start + end) / 2;
518
+ const cx = cx0 + ox + Math.cos(mid) * explode;
519
+ const cy = cy0 + oy + Math.sin(mid) * explode;
520
+ const color = palette[i] ?? s0.color ?? "#3b82f6";
521
+ const path = document.createElementNS(SVG_NS, "path");
522
+ path.setAttribute("d", pieSlicePath(cx, cy, r, innerR, start, end));
523
+ path.setAttribute("fill", shade === 1 ? color : shadeHex(color, shade));
524
+ g.appendChild(path);
525
+ start = end;
526
+ }
527
+ }
528
+ }
529
+ function pieSlicePath(cx, cy, r, ir, a0, a1) {
530
+ const large = a1 - a0 > Math.PI ? 1 : 0;
531
+ const x0 = cx + Math.cos(a0) * r;
532
+ const y0 = cy + Math.sin(a0) * r;
533
+ const x1 = cx + Math.cos(a1) * r;
534
+ const y1 = cy + Math.sin(a1) * r;
535
+ if (ir <= 0) {
536
+ return `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} Z`;
537
+ }
538
+ const ix0 = cx + Math.cos(a0) * ir;
539
+ const iy0 = cy + Math.sin(a0) * ir;
540
+ const ix1 = cx + Math.cos(a1) * ir;
541
+ const iy1 = cy + Math.sin(a1) * ir;
542
+ return `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} L ${ix1} ${iy1} A ${ir} ${ir} 0 ${large} 0 ${ix0} ${iy0} Z`;
543
+ }
544
+ function drawArea(g, def, plot, shared) {
545
+ const cats = def.categories ?? [];
546
+ let yMin = 0;
547
+ let yMax = 1;
548
+ for (const s of def.series)
549
+ for (const v of s.values) {
550
+ if (v > yMax)
551
+ yMax = v;
552
+ if (v < yMin)
553
+ yMin = v;
554
+ }
555
+ ({ yMin, yMax } = applyValueAxisScale(yMin, yMax, def));
556
+ const ticks = shared?.ticks ?? niceTicks(yMin, yMax, 5);
557
+ if (!shared?.skipAxes)
558
+ drawAxes(g, plot, ticks, cats, false);
559
+ const tmin = ticks[0];
560
+ const tmax = ticks[ticks.length - 1];
561
+ const step = (cats.length > 1 ? plot.w / (cats.length - 1) : plot.w);
562
+ const baseY = plot.y + plot.h - ((0 - tmin) / (tmax - tmin)) * plot.h;
563
+ const extrude = chart3DOffset(def);
564
+ const layers = extrude && (extrude.dx || extrude.dy) ? 5 : 1;
565
+ for (const s of def.series) {
566
+ const pts = s.values.map((v, i) => {
567
+ const xx = plot.x + step * i;
568
+ const yy = plot.y + plot.h - ((v - tmin) / (tmax - tmin)) * plot.h;
569
+ return [xx, yy];
570
+ });
571
+ if (pts.length < 2)
572
+ continue;
573
+ const color = s.color ?? "#3b82f6";
574
+ for (let layer = layers - 1; layer >= 0; layer--) {
575
+ const t = layers === 1 ? 0 : layer / (layers - 1);
576
+ const ox = extrude ? extrude.dx * t : 0;
577
+ const oy = extrude ? extrude.dy * t : 0;
578
+ const d = [
579
+ `M ${pts[0][0] + ox} ${baseY + oy}`,
580
+ ...pts.map((p) => `L ${p[0] + ox} ${p[1] + oy}`),
581
+ `L ${pts[pts.length - 1][0] + ox} ${baseY + oy}`,
582
+ "Z",
583
+ ].join(" ");
584
+ const path = document.createElementNS(SVG_NS, "path");
585
+ path.setAttribute("d", d);
586
+ path.setAttribute("fill", layer === 0 ? color : shadeHex(color, 0.55 + 0.08 * (1 - t)));
587
+ path.setAttribute("fill-opacity", layer === 0 ? "0.5" : "0.35");
588
+ if (layer === 0) {
589
+ path.setAttribute("stroke", color);
590
+ path.setAttribute("stroke-width", "1.5");
591
+ }
592
+ g.appendChild(path);
593
+ }
594
+ }
595
+ }
596
+ function drawScatter(g, def, plot) {
597
+ let xMin = Infinity;
598
+ let xMax = -Infinity;
599
+ let yMin = Infinity;
600
+ let yMax = -Infinity;
601
+ for (const series of def.points) {
602
+ for (const p of series) {
603
+ if (p.x < xMin)
604
+ xMin = p.x;
605
+ if (p.x > xMax)
606
+ xMax = p.x;
607
+ if (p.y < yMin)
608
+ yMin = p.y;
609
+ if (p.y > yMax)
610
+ yMax = p.y;
611
+ }
612
+ }
613
+ if (!Number.isFinite(xMin))
614
+ return;
615
+ const ticks = niceTicks(yMin, yMax, 5);
616
+ drawAxes(g, plot, ticks, [], false);
617
+ const tmin = ticks[0];
618
+ const tmax = ticks[ticks.length - 1];
619
+ for (let si = 0; si < def.points.length; si++) {
620
+ const pts = def.points[si];
621
+ const color = def.series[si]?.color ?? "#3b82f6";
622
+ for (const p of pts) {
623
+ const cx = plot.x + ((p.x - xMin) / (xMax - xMin || 1)) * plot.w;
624
+ const cy = plot.y + plot.h - ((p.y - tmin) / (tmax - tmin)) * plot.h;
625
+ const c = document.createElementNS(SVG_NS, "circle");
626
+ c.setAttribute("cx", String(cx));
627
+ c.setAttribute("cy", String(cy));
628
+ c.setAttribute("r", "3");
629
+ c.setAttribute("fill", color);
630
+ g.appendChild(c);
631
+ }
632
+ }
633
+ }
634
+ function hasCombo(def) {
635
+ return def.series.some((s) => s.chartType && s.chartType !== def.type);
636
+ }
637
+ function drawCombo(g, def, plot) {
638
+ const cats = def.categories ?? [];
639
+ let yMin = 0;
640
+ let yMax = 1;
641
+ for (const s of def.series) {
642
+ for (const v of s.values) {
643
+ if (v > yMax)
644
+ yMax = v;
645
+ if (v < yMin)
646
+ yMin = v;
647
+ }
648
+ if (s.chartType === "scatter" || s.chartType === "bubble") {
649
+ for (const p of seriesXyPoints(def, s)) {
650
+ if (p.y > yMax)
651
+ yMax = p.y;
652
+ if (p.y < yMin)
653
+ yMin = p.y;
654
+ }
655
+ }
656
+ }
657
+ ({ yMin, yMax } = applyValueAxisScale(yMin, yMax, def));
658
+ const ticks = niceTicks(yMin, yMax, 5);
659
+ drawAxes(g, plot, ticks, cats, def.type === "bar");
660
+ const groups = new Map();
661
+ for (const s of def.series) {
662
+ const ty = s.chartType ?? def.type;
663
+ const list = groups.get(ty) ?? [];
664
+ list.push(s);
665
+ groups.set(ty, list);
666
+ }
667
+ const order = ["area", "bar", "column", "stock", "radar", "scatter", "bubble", "line"];
668
+ const keys = [...groups.keys()].sort((a, b) => order.indexOf(a) - order.indexOf(b));
669
+ const shared = { ticks, skipAxes: true };
670
+ for (const ty of keys) {
671
+ const series = groups.get(ty);
672
+ if (ty === "bar" || ty === "column") {
673
+ drawBar(g, { ...def, type: ty, series }, plot, shared);
674
+ }
675
+ else if (ty === "area" || ty === "surface") {
676
+ drawArea(g, { ...def, type: "area", series }, plot, shared);
677
+ }
678
+ else if (ty === "scatter" || ty === "bubble") {
679
+ drawComboXy(g, def, series, plot, ticks, ty === "bubble");
680
+ }
681
+ else {
682
+ drawLine(g, { ...def, type: "line", series }, plot, shared);
683
+ }
684
+ }
685
+ overlayGuides(g, def, plot, ticks);
686
+ }
687
+ function svgLine(g, x1, y1, x2, y2, stroke, width = 1) {
688
+ const el = document.createElementNS(SVG_NS, "line");
689
+ el.setAttribute("x1", String(x1));
690
+ el.setAttribute("y1", String(y1));
691
+ el.setAttribute("x2", String(x2));
692
+ el.setAttribute("y2", String(y2));
693
+ el.setAttribute("stroke", stroke);
694
+ el.setAttribute("stroke-width", String(width));
695
+ g.appendChild(el);
696
+ }
697
+ function drawRadar(g, def, plot) {
698
+ const cats = def.categories ?? [];
699
+ const n = Math.max(cats.length, 1);
700
+ const cx = plot.x + plot.w / 2;
701
+ const cy = plot.y + plot.h / 2;
702
+ const r = Math.min(plot.w, plot.h) / 2 - 8;
703
+ let max = 1;
704
+ for (const s of def.series)
705
+ for (const v of s.values)
706
+ if (v > max)
707
+ max = v;
708
+ for (let ring = 1; ring <= 4; ring++) {
709
+ const pts = [];
710
+ for (let i = 0; i < n; i++) {
711
+ const a = -Math.PI / 2 + (i / n) * Math.PI * 2;
712
+ const rr = (r * ring) / 4;
713
+ pts.push(`${cx + Math.cos(a) * rr},${cy + Math.sin(a) * rr}`);
714
+ }
715
+ const poly = document.createElementNS(SVG_NS, "polygon");
716
+ poly.setAttribute("points", pts.join(" "));
717
+ poly.setAttribute("fill", "none");
718
+ poly.setAttribute("stroke", ts.grid);
719
+ g.appendChild(poly);
720
+ }
721
+ for (let i = 0; i < n; i++) {
722
+ const a = -Math.PI / 2 + (i / n) * Math.PI * 2;
723
+ svgLine(g, cx, cy, cx + Math.cos(a) * r, cy + Math.sin(a) * r, ts.grid);
724
+ if (cats[i]) {
725
+ const t = document.createElementNS(SVG_NS, "text");
726
+ t.setAttribute("x", String(cx + Math.cos(a) * (r + 12)));
727
+ t.setAttribute("y", String(cy + Math.sin(a) * (r + 12) + 3));
728
+ t.setAttribute("text-anchor", "middle");
729
+ t.setAttribute("font-size", String(ts.size));
730
+ t.setAttribute("fill", ts.muted);
731
+ t.textContent = cats[i];
732
+ g.appendChild(t);
733
+ }
734
+ }
735
+ const filled = def.radarStyle === "filled";
736
+ for (const s of def.series) {
737
+ const pts = [];
738
+ for (let i = 0; i < n; i++) {
739
+ const a = -Math.PI / 2 + (i / n) * Math.PI * 2;
740
+ const rr = r * ((s.values[i] ?? 0) / max);
741
+ pts.push(`${cx + Math.cos(a) * rr},${cy + Math.sin(a) * rr}`);
742
+ }
743
+ const poly = document.createElementNS(SVG_NS, "polygon");
744
+ poly.setAttribute("points", pts.join(" "));
745
+ poly.setAttribute("fill", filled ? (s.color ?? "#3b82f6") : "none");
746
+ if (filled)
747
+ poly.setAttribute("fill-opacity", "0.25");
748
+ poly.setAttribute("stroke", s.color ?? "#3b82f6");
749
+ poly.setAttribute("stroke-width", "2");
750
+ g.appendChild(poly);
751
+ if (def.radarStyle !== "standard") {
752
+ for (const pt of pts) {
753
+ const [px, py] = pt.split(",").map(Number);
754
+ const c = document.createElementNS(SVG_NS, "circle");
755
+ c.setAttribute("cx", String(px));
756
+ c.setAttribute("cy", String(py));
757
+ c.setAttribute("r", "3");
758
+ c.setAttribute("fill", s.color ?? "#3b82f6");
759
+ g.appendChild(c);
760
+ }
761
+ }
762
+ }
763
+ }
764
+ function drawBubble(g, def, plot) {
765
+ let xMin = Infinity;
766
+ let xMax = -Infinity;
767
+ let yMin = Infinity;
768
+ let yMax = -Infinity;
769
+ let zMax = 1;
770
+ for (const series of def.points) {
771
+ for (const p of series) {
772
+ if (p.x < xMin)
773
+ xMin = p.x;
774
+ if (p.x > xMax)
775
+ xMax = p.x;
776
+ if (p.y < yMin)
777
+ yMin = p.y;
778
+ if (p.y > yMax)
779
+ yMax = p.y;
780
+ if ((p.z ?? 1) > zMax)
781
+ zMax = p.z ?? 1;
782
+ }
783
+ }
784
+ if (!Number.isFinite(xMin))
785
+ return;
786
+ const ticks = niceTicks(yMin, yMax, 5);
787
+ drawAxes(g, plot, ticks, [], false);
788
+ const tmin = ticks[0];
789
+ const tmax = ticks[ticks.length - 1];
790
+ const rMax = Math.min(plot.w, plot.h) * 0.08;
791
+ for (let si = 0; si < def.points.length; si++) {
792
+ const color = def.series[si]?.color ?? "#3b82f6";
793
+ for (const p of def.points[si]) {
794
+ const cx = plot.x + ((p.x - xMin) / (xMax - xMin || 1)) * plot.w;
795
+ const cy = plot.y + plot.h - ((p.y - tmin) / (tmax - tmin)) * plot.h;
796
+ const rr = Math.max(3, rMax * Math.sqrt((p.z ?? 1) / zMax));
797
+ const c = document.createElementNS(SVG_NS, "circle");
798
+ c.setAttribute("cx", String(cx));
799
+ c.setAttribute("cy", String(cy));
800
+ c.setAttribute("r", String(rr));
801
+ c.setAttribute("fill", color);
802
+ c.setAttribute("fill-opacity", "0.55");
803
+ g.appendChild(c);
804
+ }
805
+ }
806
+ }
807
+ function drawStock(g, def, plot) {
808
+ const cats = def.categories ?? [];
809
+ const n = def.series.length;
810
+ if (n < 3) {
811
+ drawLine(g, { ...def, type: "line" }, plot);
812
+ return;
813
+ }
814
+ const high = def.series[n >= 4 ? n - 3 : 0];
815
+ const low = def.series[n >= 4 ? n - 2 : 1];
816
+ const close = def.series[n >= 4 ? n - 1 : 2];
817
+ const open = n >= 4 ? def.series[n - 4] : undefined;
818
+ let yMin = 0;
819
+ let yMax = 1;
820
+ for (const s of [high, low, close, open]) {
821
+ if (!s)
822
+ continue;
823
+ for (const v of s.values) {
824
+ if (v > yMax)
825
+ yMax = v;
826
+ if (v < yMin)
827
+ yMin = v;
828
+ }
829
+ }
830
+ const ticks = niceTicks(yMin, yMax, 5);
831
+ drawAxes(g, plot, ticks, cats, false);
832
+ const tmin = ticks[0];
833
+ const tmax = ticks[ticks.length - 1];
834
+ const step = plot.w / Math.max(cats.length, 1);
835
+ const yOf = (v) => plot.y + plot.h - ((v - tmin) / (tmax - tmin)) * plot.h;
836
+ for (let i = 0; i < cats.length; i++) {
837
+ const x = plot.x + step * (i + 0.5);
838
+ svgLine(g, x, yOf(high.values[i] ?? 0), x, yOf(low.values[i] ?? 0), ts.text, 1.25);
839
+ svgLine(g, x, yOf(close.values[i] ?? 0), x + step * 0.18, yOf(close.values[i] ?? 0), ts.text, 1.25);
840
+ if (open) {
841
+ const ov = open.values[i] ?? 0;
842
+ svgLine(g, x - step * 0.18, yOf(ov), x, yOf(ov), ts.text, 1.25);
843
+ }
844
+ }
845
+ }
846
+ function drawLegend(g, def, area) {
847
+ const items = [];
848
+ if (def.type === "pie" || def.type === "doughnut") {
849
+ const s0 = def.series[0];
850
+ if (s0) {
851
+ const palette = s0.perSliceColors ?? def.colors ?? [];
852
+ (def.categories ?? []).forEach((c, i) => {
853
+ items.push({ name: c || `Item ${i + 1}`, color: palette[i] ?? s0.color ?? "#3b82f6" });
854
+ });
855
+ }
856
+ }
857
+ else {
858
+ for (const s of def.series)
859
+ items.push({ name: s.name ?? "Series", color: s.color ?? "#3b82f6" });
860
+ }
861
+ let x = area.x + 12;
862
+ const y = area.y + 6;
863
+ for (const it of items) {
864
+ const sw = document.createElementNS(SVG_NS, "rect");
865
+ sw.setAttribute("x", String(x));
866
+ sw.setAttribute("y", String(y));
867
+ sw.setAttribute("width", "10");
868
+ sw.setAttribute("height", "10");
869
+ sw.setAttribute("fill", it.color);
870
+ g.appendChild(sw);
871
+ const t = document.createElementNS(SVG_NS, "text");
872
+ t.setAttribute("x", String(x + 14));
873
+ t.setAttribute("y", String(y + 9));
874
+ t.setAttribute("font-size", String(ts.size));
875
+ t.setAttribute("fill", ts.muted);
876
+ t.textContent = it.name;
877
+ g.appendChild(t);
878
+ x += estimateTextWidth(it.name, ts.size) + 22 + 8;
879
+ }
880
+ }
881
+ function drawSurface(g, def, plot) {
882
+ for (const q of surfaceIsoQuads(def, plot)) {
883
+ if (q.pts.length < 3)
884
+ continue;
885
+ const poly = document.createElementNS(SVG_NS, "polygon");
886
+ poly.setAttribute("points", q.pts.map((p) => `${p.x},${p.y}`).join(" "));
887
+ if (q.stroke) {
888
+ poly.setAttribute("fill", "none");
889
+ poly.setAttribute("stroke", q.color);
890
+ poly.setAttribute("stroke-width", "1");
891
+ }
892
+ else {
893
+ poly.setAttribute("fill", q.color);
894
+ poly.setAttribute("fill-opacity", "0.72");
895
+ poly.setAttribute("stroke", "rgba(255,255,255,0.45)");
896
+ poly.setAttribute("stroke-width", "0.6");
897
+ }
898
+ g.appendChild(poly);
899
+ }
900
+ }
901
+ function drawComboXy(g, def, series, plot, ticks, bubble) {
902
+ const cats = def.categories ?? [];
903
+ const tmin = ticks[0];
904
+ const tmax = ticks[ticks.length - 1];
905
+ const xsAll = series.flatMap((s) => seriesXyPoints(def, s).map((p) => p.x));
906
+ for (const s of series) {
907
+ const pts = seriesXyPoints(def, s);
908
+ for (const p of pts) {
909
+ const cx = comboXyPlotX(p.x, cats, plot, xsAll);
910
+ const cy = plot.y + plot.h - ((p.y - tmin) / (tmax - tmin || 1)) * plot.h;
911
+ const r = bubble ? Math.max(3, Math.min(16, (p.z ?? 1) * 1.8)) : 3.5;
912
+ const c = document.createElementNS(SVG_NS, "circle");
913
+ c.setAttribute("cx", String(cx));
914
+ c.setAttribute("cy", String(cy));
915
+ c.setAttribute("r", String(r));
916
+ c.setAttribute("fill", s.color ?? "#3b82f6");
917
+ if (bubble)
918
+ c.setAttribute("fill-opacity", "0.7");
919
+ g.appendChild(c);
920
+ }
921
+ }
922
+ }
923
+ function overlayGuides(g, def, plot, ticksIn) {
924
+ if (def.type === "pie" ||
925
+ def.type === "doughnut" ||
926
+ def.type === "surface" ||
927
+ def.type === "radar" ||
928
+ def.type === "sunburst" ||
929
+ def.type === "treemap" ||
930
+ def.type === "funnel" ||
931
+ def.type === "region") {
932
+ return;
933
+ }
934
+ const cats = def.categories ?? [];
935
+ let yMin = 0;
936
+ let yMax = 1;
937
+ for (const s of def.series) {
938
+ for (const v of s.values) {
939
+ if (v > yMax)
940
+ yMax = v;
941
+ if (v < yMin)
942
+ yMin = v;
943
+ }
944
+ if (s.errorBars?.val) {
945
+ for (const v of s.values) {
946
+ if (v + s.errorBars.val > yMax)
947
+ yMax = v + s.errorBars.val;
948
+ if (v - s.errorBars.val < yMin)
949
+ yMin = v - s.errorBars.val;
950
+ }
951
+ }
952
+ }
953
+ ({ yMin, yMax } = applyValueAxisScale(yMin, yMax, def));
954
+ const ticks = ticksIn ?? niceTicks(yMin, yMax, 5);
955
+ const tmin = ticks[0];
956
+ const tmax = ticks[ticks.length - 1];
957
+ const yOf = (v) => plot.y + plot.h - ((v - tmin) / (tmax - tmin || 1)) * plot.h;
958
+ const step = cats.length > 1 ? plot.w / (cats.length - 1) : plot.w;
959
+ for (const s of def.series) {
960
+ const ty = s.chartType ?? def.type;
961
+ const color = s.color ?? ts.muted;
962
+ if (ty === "scatter" || ty === "bubble") {
963
+ const pts = seriesXyPoints(def, s);
964
+ if (s.trendline) {
965
+ const fit = linearFit(pts.map((p) => p.x), pts.map((p) => p.y));
966
+ if (fit && pts.length >= 2) {
967
+ const xs = pts.map((p) => p.x);
968
+ const x0 = Math.min(...xs);
969
+ const x1 = Math.max(...xs);
970
+ const xAt = (xv) => comboXyPlotX(xv, cats, plot, xs);
971
+ const line = document.createElementNS(SVG_NS, "line");
972
+ line.setAttribute("x1", String(xAt(x0)));
973
+ line.setAttribute("y1", String(yOf(fit.a + fit.b * x0)));
974
+ line.setAttribute("x2", String(xAt(x1)));
975
+ line.setAttribute("y2", String(yOf(fit.a + fit.b * x1)));
976
+ line.setAttribute("stroke", color);
977
+ line.setAttribute("stroke-width", "1.25");
978
+ line.setAttribute("stroke-dasharray", "5 3");
979
+ g.appendChild(line);
980
+ }
981
+ }
982
+ continue;
983
+ }
984
+ if (s.errorBars) {
985
+ const half = s.errorBars.val ?? 0;
986
+ s.values.forEach((v, i) => {
987
+ const xx = plot.x + step * i;
988
+ svgLine(g, xx, yOf(v - half), xx, yOf(v + half), color, 1);
989
+ svgLine(g, xx - 3, yOf(v - half), xx + 3, yOf(v - half), color, 1);
990
+ svgLine(g, xx - 3, yOf(v + half), xx + 3, yOf(v + half), color, 1);
991
+ });
992
+ }
993
+ if (s.trendline) {
994
+ const xs = s.values.map((_, i) => i);
995
+ const fit = linearFit(xs, s.values);
996
+ if (fit && xs.length >= 2) {
997
+ const line = document.createElementNS(SVG_NS, "line");
998
+ line.setAttribute("x1", String(plot.x));
999
+ line.setAttribute("y1", String(yOf(fit.a)));
1000
+ line.setAttribute("x2", String(plot.x + step * (s.values.length - 1)));
1001
+ line.setAttribute("y2", String(yOf(fit.a + fit.b * (s.values.length - 1))));
1002
+ line.setAttribute("stroke", color);
1003
+ line.setAttribute("stroke-width", "1.25");
1004
+ line.setAttribute("stroke-dasharray", "5 3");
1005
+ g.appendChild(line);
1006
+ }
1007
+ }
1008
+ }
1009
+ }
1010
+ function overlayDataLabels(g, def, plot) {
1011
+ const add = (x, y, text, anchor = "middle", baseline = "bottom") => {
1012
+ const t = document.createElementNS(SVG_NS, "text");
1013
+ t.setAttribute("x", String(x));
1014
+ // SVG text is placed by its alphabetic baseline; shift for the other two.
1015
+ t.setAttribute("y", String(baseline === "middle" ? y + ts.size * 0.35 : baseline === "top" ? y + ts.size : y));
1016
+ t.setAttribute("text-anchor", anchor);
1017
+ t.setAttribute("font-size", String(ts.size));
1018
+ t.setAttribute("fill", ts.text);
1019
+ t.textContent = text;
1020
+ g.appendChild(t);
1021
+ };
1022
+ // Clustered / stacked bars: label each bar at its own free end, in the bar renderer's geometry.
1023
+ if ((def.type === "column" || def.type === "bar") && !hasCombo(def)) {
1024
+ const geo = barGeometry(def, plot);
1025
+ const cats = def.categories ?? [];
1026
+ for (let ci = 0; ci < cats.length; ci++) {
1027
+ let stackPos = 0;
1028
+ let stackNeg = 0;
1029
+ def.series.forEach((s, si) => {
1030
+ let v = s.values[ci] ?? 0;
1031
+ if (def.percentStacked)
1032
+ v = (geo.totals[ci] ?? 0) > 0 ? v / geo.totals[ci] : 0;
1033
+ const label = pointLabelText(def, s, ci, s.values[ci] ?? 0);
1034
+ if (label) {
1035
+ const a = barLabelAnchor(geo, plot, si, ci, v, v >= 0 ? stackPos : stackNeg);
1036
+ add(a.x, a.y, label, a.anchor, a.baseline);
1037
+ }
1038
+ if (v >= 0)
1039
+ stackPos += v;
1040
+ else
1041
+ stackNeg += v;
1042
+ });
1043
+ }
1044
+ return;
1045
+ }
1046
+ if (def.type === "pie" || def.type === "doughnut") {
1047
+ const s0 = def.series[0];
1048
+ if (!s0)
1049
+ return;
1050
+ const total = s0.values.reduce((a, b) => a + Math.abs(b), 0);
1051
+ if (!total)
1052
+ return;
1053
+ const cx0 = plot.x + plot.w / 2;
1054
+ const cy0 = plot.y + plot.h / 2;
1055
+ const r = Math.min(plot.w, plot.h) / 2 - 4;
1056
+ let start = -Math.PI / 2 + ((def.startAngle ?? 0) * Math.PI) / 180;
1057
+ for (let i = 0; i < s0.values.length; i++) {
1058
+ const v = Math.abs(s0.values[i]);
1059
+ const frac = v / total;
1060
+ if (frac <= 0)
1061
+ continue;
1062
+ const end = start + frac * Math.PI * 2;
1063
+ const mid = (start + end) / 2;
1064
+ const label = pointLabelText(def, s0, i, s0.values[i]);
1065
+ if (label)
1066
+ add(cx0 + Math.cos(mid) * r * 0.62, cy0 + Math.sin(mid) * r * 0.62, label);
1067
+ start = end;
1068
+ }
1069
+ return;
1070
+ }
1071
+ const cats = def.categories ?? [];
1072
+ const step = cats.length > 1 ? plot.w / (cats.length - 1) : plot.w;
1073
+ let yMin = 0;
1074
+ let yMax = 1;
1075
+ for (const s of def.series)
1076
+ for (const v of s.values) {
1077
+ if (v > yMax)
1078
+ yMax = v;
1079
+ if (v < yMin)
1080
+ yMin = v;
1081
+ }
1082
+ ({ yMin, yMax } = applyValueAxisScale(yMin, yMax, def));
1083
+ const ticks = niceTicks(yMin, yMax, 5);
1084
+ const tmin = ticks[0];
1085
+ const tmax = ticks[ticks.length - 1];
1086
+ const yOf = (v) => plot.y + plot.h - ((v - tmin) / (tmax - tmin || 1)) * plot.h;
1087
+ for (const s of def.series) {
1088
+ const ty = s.chartType ?? def.type;
1089
+ if (ty === "scatter" || ty === "bubble") {
1090
+ const pts = seriesXyPoints(def, s);
1091
+ const xs = pts.map((p) => p.x);
1092
+ pts.forEach((p, i) => {
1093
+ const label = pointLabelText(def, s, i, p.y);
1094
+ if (label)
1095
+ add(comboXyPlotX(p.x, cats, plot, xs), yOf(p.y) - 4, label);
1096
+ });
1097
+ continue;
1098
+ }
1099
+ s.values.forEach((v, i) => {
1100
+ const label = pointLabelText(def, s, i, v);
1101
+ if (!label)
1102
+ return;
1103
+ const xx = plot.x + step * (cats.length > 1 ? i : 0.5);
1104
+ add(xx, yOf(v) - 3, label);
1105
+ });
1106
+ }
1107
+ }