@sliiu/present-renderer 0.0.0-stage → 0.1.1

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