@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,261 @@
1
+ export const CHART_EX_PALETTE = [
2
+ "#4E79A7",
3
+ "#F28E2B",
4
+ "#E15759",
5
+ "#76B7B2",
6
+ "#59A14F",
7
+ "#EDC948",
8
+ "#B07AA1",
9
+ "#FF9DA7",
10
+ "#9C755F",
11
+ "#BAB0AC",
12
+ ];
13
+ function columnBarsFromSlices(slices, plot) {
14
+ const items = slices.filter((s) => s.value > 0);
15
+ if (!items.length)
16
+ return [];
17
+ const max = Math.max(...items.map((s) => s.value), 1);
18
+ const gap = plot.w / items.length;
19
+ const barW = gap * 0.65;
20
+ return items.map((s, i) => {
21
+ const h = (s.value / max) * plot.h;
22
+ return {
23
+ ...s,
24
+ x: plot.x + gap * i + (gap - barW) / 2,
25
+ y: plot.y + plot.h - h,
26
+ w: barW,
27
+ h,
28
+ };
29
+ });
30
+ }
31
+ export function layoutHistogram(slices, plot) {
32
+ return columnBarsFromSlices(slices, plot);
33
+ }
34
+ export function layoutPareto(slices, plot) {
35
+ const sorted = [...slices].filter((s) => s.value > 0).sort((a, b) => b.value - a.value);
36
+ const bars = columnBarsFromSlices(sorted, plot);
37
+ const total = sorted.reduce((sum, s) => sum + s.value, 0);
38
+ let cum = 0;
39
+ const line = [];
40
+ const gap = plot.w / Math.max(sorted.length, 1);
41
+ for (let i = 0; i < sorted.length; i++) {
42
+ cum += sorted[i].value;
43
+ const pct = total > 0 ? cum / total : 0;
44
+ line.push([plot.x + gap * (i + 0.5), plot.y + plot.h - pct * plot.h]);
45
+ }
46
+ return { bars, line };
47
+ }
48
+ export function layoutBoxWhisker(slices, plot) {
49
+ const items = slices.filter((s) => s.value >= 0);
50
+ if (!items.length)
51
+ return [];
52
+ const max = Math.max(...items.map((s) => s.value), 1);
53
+ const slot = plot.w / items.length;
54
+ const boxW = slot * 0.45;
55
+ return items.map((s, i) => {
56
+ const v = s.value;
57
+ const cx = plot.x + slot * (i + 0.5);
58
+ const toY = (val) => plot.y + plot.h - (Math.min(val, max) / max) * plot.h;
59
+ const spread = Math.max(v * 0.15, max * 0.04);
60
+ return {
61
+ label: s.label,
62
+ color: s.color,
63
+ cx,
64
+ whiskerLow: toY(Math.max(0, v - spread * 2)),
65
+ q1: toY(Math.max(0, v - spread)),
66
+ median: toY(v),
67
+ q3: toY(Math.min(max, v + spread)),
68
+ whiskerHigh: toY(Math.min(max, v + spread * 2)),
69
+ boxW,
70
+ };
71
+ });
72
+ }
73
+ export function layoutRegion(slices, plot) {
74
+ const items = slices.filter((s) => s.value > 0);
75
+ if (!items.length)
76
+ return [];
77
+ const max = Math.max(...items.map((s) => s.value), 1);
78
+ const rowH = plot.h / items.length;
79
+ return items.map((s, i) => ({
80
+ ...s,
81
+ x: plot.x,
82
+ y: plot.y + rowH * i + 2,
83
+ w: (s.value / max) * plot.w * 0.95,
84
+ h: Math.max(4, rowH - 4),
85
+ }));
86
+ }
87
+ export function normalizeChartExSlices(def, opts) {
88
+ const categories = def.categories?.length
89
+ ? def.categories
90
+ : Array.from({ length: Math.max(1, def.series[0]?.values.length ?? 1) }, (_, i) => `Cat ${i + 1}`);
91
+ const series = def.series.length > 0
92
+ ? def.series
93
+ : [{ name: "Series 1", values: categories.map(() => 0) }];
94
+ const s0 = series[0];
95
+ const palette = def.colors ?? CHART_EX_PALETTE;
96
+ const vary = def.varyColors !== false;
97
+ return categories.map((label, i) => {
98
+ const raw = s0.values[i] ?? 0;
99
+ return {
100
+ label: label || `Item ${i + 1}`,
101
+ value: opts?.preserveSign ? raw : Math.max(0, Math.abs(raw)),
102
+ color: vary ? palette[i % palette.length] : s0.color ?? palette[i % palette.length],
103
+ };
104
+ });
105
+ }
106
+ /** Slice-and-dice treemap — 足够表达占比结构,编辑预览用。 */
107
+ export function layoutTreemap(slices, plot) {
108
+ const items = slices.filter((s) => s.value > 0);
109
+ if (items.length === 0)
110
+ return [];
111
+ const pad = 2;
112
+ const cells = [];
113
+ const walk = (list, x, y, w, h, depth) => {
114
+ if (list.length === 0 || w <= 0 || h <= 0)
115
+ return;
116
+ if (list.length === 1) {
117
+ const s = list[0];
118
+ cells.push({
119
+ ...s,
120
+ x: x + pad / 2,
121
+ y: y + pad / 2,
122
+ w: Math.max(0, w - pad),
123
+ h: Math.max(0, h - pad),
124
+ });
125
+ return;
126
+ }
127
+ const total = list.reduce((sum, s) => sum + s.value, 0);
128
+ if (total <= 0)
129
+ return;
130
+ const mid = Math.ceil(list.length / 2);
131
+ const left = list.slice(0, mid);
132
+ const right = list.slice(mid);
133
+ const leftSum = left.reduce((sum, s) => sum + s.value, 0);
134
+ const ratio = leftSum / total;
135
+ const horizontal = depth % 2 === 0 ? w >= h : h > w;
136
+ if (horizontal) {
137
+ const lw = w * ratio;
138
+ walk(left, x, y, lw, h, depth + 1);
139
+ walk(right, x + lw, y, w - lw, h, depth + 1);
140
+ }
141
+ else {
142
+ const lh = h * ratio;
143
+ walk(left, x, y, w, lh, depth + 1);
144
+ walk(right, x, y + lh, w, h - lh, depth + 1);
145
+ }
146
+ };
147
+ walk(items, plot.x, plot.y, plot.w, plot.h, 0);
148
+ return cells;
149
+ }
150
+ /** 单层 sunburst:内环空心 + 外环扇区(扁平 categories)。 */
151
+ export function layoutSunburst(slices, plot) {
152
+ const items = slices.filter((s) => s.value > 0);
153
+ const total = items.reduce((sum, s) => sum + s.value, 0);
154
+ if (total <= 0)
155
+ return [];
156
+ const cx = plot.x + plot.w / 2;
157
+ const cy = plot.y + plot.h / 2;
158
+ const r = Math.min(plot.w, plot.h) / 2 - 4;
159
+ const r0 = r * 0.38;
160
+ const r1 = r;
161
+ let a = -Math.PI / 2;
162
+ const arcs = [];
163
+ for (const s of items) {
164
+ const span = (s.value / total) * Math.PI * 2;
165
+ if (span <= 0)
166
+ continue;
167
+ arcs.push({ ...s, cx, cy, r0, r1, a0: a, a1: a + span });
168
+ a += span;
169
+ }
170
+ return arcs;
171
+ }
172
+ export function layoutWaterfall(slices, plot) {
173
+ const items = slices.filter((s) => s.value !== 0 || slices.length === 1);
174
+ if (items.length === 0)
175
+ return [];
176
+ let running = 0;
177
+ let minY = 0;
178
+ let maxY = 0;
179
+ const deltas = items.map((s) => s.value);
180
+ for (const v of deltas) {
181
+ const top = running + Math.max(0, v);
182
+ const bottom = running + Math.min(0, v);
183
+ minY = Math.min(minY, bottom, top);
184
+ maxY = Math.max(maxY, bottom, top);
185
+ running += v;
186
+ }
187
+ minY = Math.min(minY, 0);
188
+ maxY = Math.max(maxY, running, 1);
189
+ const range = maxY - minY || 1;
190
+ const barW = (plot.w / Math.max(items.length, 1)) * 0.55;
191
+ const gap = plot.w / Math.max(items.length, 1);
192
+ running = 0;
193
+ const bars = [];
194
+ for (let i = 0; i < items.length; i++) {
195
+ const s = items[i];
196
+ const v = s.value;
197
+ const y0 = running;
198
+ const y1 = running + v;
199
+ const top = Math.max(y0, y1);
200
+ const bottom = Math.min(y0, y1);
201
+ const pxTop = plot.y + plot.h - ((top - minY) / range) * plot.h;
202
+ const pxBottom = plot.y + plot.h - ((bottom - minY) / range) * plot.h;
203
+ bars.push({
204
+ ...s,
205
+ x: plot.x + gap * i + (gap - barW) / 2,
206
+ y: Math.min(pxTop, pxBottom),
207
+ w: barW,
208
+ h: Math.max(2, Math.abs(pxBottom - pxTop)),
209
+ kind: v >= 0 ? "increase" : "decrease",
210
+ });
211
+ running = y1;
212
+ }
213
+ return bars;
214
+ }
215
+ export function layoutFunnel(slices, plot) {
216
+ const items = slices.filter((s) => s.value > 0);
217
+ if (items.length === 0)
218
+ return [];
219
+ const maxVal = Math.max(...items.map((s) => s.value));
220
+ const segH = plot.h / items.length;
221
+ const cx = plot.x + plot.w / 2;
222
+ const segs = [];
223
+ for (let i = 0; i < items.length; i++) {
224
+ const s = items[i];
225
+ const topVal = i === 0 ? s.value : items[i - 1].value;
226
+ const botVal = s.value;
227
+ const topW = (topVal / maxVal) * plot.w * 0.92;
228
+ const botW = (botVal / maxVal) * plot.w * 0.92;
229
+ const y0 = plot.y + segH * i;
230
+ const y1 = plot.y + segH * (i + 1) - 1;
231
+ segs.push({
232
+ ...s,
233
+ points: [
234
+ [cx - topW / 2, y0],
235
+ [cx + topW / 2, y0],
236
+ [cx + botW / 2, y1],
237
+ [cx - botW / 2, y1],
238
+ ],
239
+ });
240
+ }
241
+ return segs;
242
+ }
243
+ export function sunburstArcPath(arc) {
244
+ const { cx, cy, r0, r1, a0, a1 } = arc;
245
+ const large = a1 - a0 > Math.PI ? 1 : 0;
246
+ const x0 = cx + Math.cos(a0) * r1;
247
+ const y0 = cy + Math.sin(a0) * r1;
248
+ const x1 = cx + Math.cos(a1) * r1;
249
+ const y1 = cy + Math.sin(a1) * r1;
250
+ const ix0 = cx + Math.cos(a0) * r0;
251
+ const iy0 = cy + Math.sin(a0) * r0;
252
+ const ix1 = cx + Math.cos(a1) * r0;
253
+ const iy1 = cy + Math.sin(a1) * r0;
254
+ return `M ${x0} ${y0} A ${r1} ${r1} 0 ${large} 1 ${x1} ${y1} L ${ix1} ${iy1} A ${r0} ${r0} 0 ${large} 0 ${ix0} ${iy0} Z`;
255
+ }
256
+ export function polygonPath(points) {
257
+ if (points.length === 0)
258
+ return "";
259
+ const [first, ...rest] = points;
260
+ return `M ${first[0]} ${first[1]} ${rest.map((p) => `L ${p[0]} ${p[1]}`).join(" ")} Z`;
261
+ }
@@ -0,0 +1,296 @@
1
+ /** 散点 / 组合图系列的 (x,y) 点:优先 series.points,否则用分类下标 + values。 */
2
+ export function seriesXyPoints(def, s) {
3
+ if (s.points?.length)
4
+ return s.points;
5
+ const defPts = def.points;
6
+ const idx = def.series.indexOf(s);
7
+ if (idx >= 0 && defPts?.[idx]?.length)
8
+ return defPts[idx];
9
+ return s.values.map((y, i) => ({ x: i, y }));
10
+ }
11
+ export function linearFit(xs, ys) {
12
+ const n = Math.min(xs.length, ys.length);
13
+ if (n < 2)
14
+ return null;
15
+ let sx = 0;
16
+ let sy = 0;
17
+ let sxx = 0;
18
+ let sxy = 0;
19
+ for (let i = 0; i < n; i++) {
20
+ sx += xs[i];
21
+ sy += ys[i];
22
+ sxx += xs[i] * xs[i];
23
+ sxy += xs[i] * ys[i];
24
+ }
25
+ const den = n * sxx - sx * sx;
26
+ if (Math.abs(den) < 1e-12)
27
+ return null;
28
+ const b = (n * sxy - sx * sy) / den;
29
+ const a = (sy - b * sx) / n;
30
+ return { a, b };
31
+ }
32
+ /** Rough advance of `text` at `size` px: CJK glyphs are square, Latin about 0.58em. Legend rows used `length × 6` and overprinted CJK series names. */
33
+ export function estimateTextWidth(text, size) {
34
+ let w = 0;
35
+ for (const ch of text)
36
+ w += /[\u2e80-\u9fff\uff00-\uffef]/.test(ch) ? size : size * 0.58;
37
+ return w;
38
+ }
39
+ /** Chart types whose category labels run along the bottom edge of the plot. */
40
+ /** Types that carry a labelled axis along the bottom edge — categories for most, the value axis for a horizontal bar chart. */
41
+ const BOTTOM_CATEGORY_AXIS = new Set(["column", "bar", "line", "area", "scatter", "bubble", "stock", "waterfall", "pareto", "histogram", "boxWhisker"]);
42
+ /**
43
+ * Bottom padding of the plot: room for the category labels (they used to be drawn 14px under
44
+ * the plot into an 8px pad, and the legend was then drawn over them) plus a legend row.
45
+ */
46
+ export function chartPadding(def, textSize) {
47
+ const hasCombo = def.series.some((s) => s.chartType && s.chartType !== def.type);
48
+ const catAxis = BOTTOM_CATEGORY_AXIS.has(def.type) || hasCombo;
49
+ const legend = def.legend ? textSize + 10 : 0;
50
+ const labels = catAxis ? textSize + 10 : 8;
51
+ return { bottom: labels + legend, legend };
52
+ }
53
+ export function barGeometry(def, plot, sharedTicks) {
54
+ const cats = def.categories ?? [];
55
+ const isHoriz = def.type === "bar";
56
+ const stacked = !!def.stacked || !!def.percentStacked;
57
+ let yMin = 0;
58
+ let yMax = 1;
59
+ const totals = new Array(cats.length).fill(0);
60
+ if (stacked) {
61
+ for (const s of def.series)
62
+ for (let i = 0; i < s.values.length; i++)
63
+ totals[i] = (totals[i] ?? 0) + (s.values[i] ?? 0);
64
+ yMax = Math.max(...totals, 0);
65
+ yMin = Math.min(0, ...totals);
66
+ }
67
+ else {
68
+ for (const s of def.series) {
69
+ for (const v of s.values) {
70
+ if (v > yMax)
71
+ yMax = v;
72
+ if (v < yMin)
73
+ yMin = v;
74
+ }
75
+ }
76
+ }
77
+ if (def.percentStacked) {
78
+ yMin = 0;
79
+ yMax = 1;
80
+ }
81
+ else {
82
+ ({ yMin, yMax } = applyValueAxisScale(yMin, yMax, def));
83
+ }
84
+ const ticks = sharedTicks ?? niceTicks(yMin, yMax, 5);
85
+ const groupStep = (isHoriz ? plot.h : plot.w) / Math.max(cats.length, 1);
86
+ const inner = groupStep * 0.7;
87
+ const barW = stacked ? inner : inner / Math.max(def.series.length, 1);
88
+ return { isHoriz, stacked, ticks, tmin: ticks[0], tmax: ticks[ticks.length - 1], groupStep, inner, barW, totals };
89
+ }
90
+ /**
91
+ * Where the value label of series `si`, category `ci` goes: just past the free end of a
92
+ * clustered bar (above a column, right of a horizontal bar), or centred in a stacked segment.
93
+ * `stackBefore` is the signed running total of the segments under this one (stacked only).
94
+ */
95
+ export function barLabelAnchor(geo, plot, si, ci, v, stackBefore = 0) {
96
+ const span = geo.tmax - geo.tmin || 1;
97
+ const gap = 3;
98
+ if (geo.isHoriz) {
99
+ const yCat = plot.y + geo.groupStep * ci + (geo.groupStep - geo.inner) / 2;
100
+ const yMid = (geo.stacked ? yCat : yCat + si * geo.barW) + geo.barW / 2;
101
+ if (geo.stacked) {
102
+ const mid = stackBefore + v / 2;
103
+ return { x: plot.x + ((mid - geo.tmin) / span) * plot.w, y: yMid, anchor: "middle", baseline: "middle" };
104
+ }
105
+ const end = plot.x + ((v - geo.tmin) / span) * plot.w;
106
+ return { x: v >= 0 ? end + gap : end - gap, y: yMid, anchor: v >= 0 ? "start" : "end", baseline: "middle" };
107
+ }
108
+ const xCat = plot.x + geo.groupStep * ci + (geo.groupStep - geo.inner) / 2;
109
+ const xMid = (geo.stacked ? xCat : xCat + si * geo.barW) + geo.barW / 2;
110
+ const yOf = (val) => plot.y + plot.h - ((val - geo.tmin) / span) * plot.h;
111
+ if (geo.stacked)
112
+ return { x: xMid, y: yOf(stackBefore + v / 2), anchor: "middle", baseline: "middle" };
113
+ // Negative columns hang below the baseline; their label goes under the bar's free end.
114
+ return v >= 0 ? { x: xMid, y: yOf(v) - gap, anchor: "middle", baseline: "bottom" } : { x: xMid, y: yOf(v) + gap, anchor: "middle", baseline: "top" };
115
+ }
116
+ export function applyValueAxisScale(yMin, yMax, def) {
117
+ const val = def.axes?.find((a) => a.type === "val" && (a.position === "l" || a.position === "r")) ??
118
+ def.axes?.find((a) => a.type === "val");
119
+ if (val?.scaling?.min != null)
120
+ yMin = val.scaling.min;
121
+ if (val?.scaling?.max != null)
122
+ yMax = val.scaling.max;
123
+ return { yMin, yMax };
124
+ }
125
+ /**
126
+ * 曲面图等距网格:系列 = Y 行,分类 = X 列,values = Z。
127
+ * 单系列时沿 Y 挤一条薄带,避免退化成折线。
128
+ */
129
+ export function surfaceIsoQuads(def, plot) {
130
+ const nX = Math.max(2, def.categories?.length ?? Math.max(0, ...def.series.map((s) => s.values.length)));
131
+ const nYraw = def.series.length;
132
+ const nY = Math.max(2, nYraw);
133
+ let zMin = 0;
134
+ let zMax = 1;
135
+ for (const s of def.series) {
136
+ for (const v of s.values) {
137
+ if (v < zMin)
138
+ zMin = v;
139
+ if (v > zMax)
140
+ zMax = v;
141
+ }
142
+ }
143
+ const zAt = (j, i) => {
144
+ const row = def.series[Math.min(j, nYraw - 1)] ?? def.series[0];
145
+ if (!row)
146
+ return 0;
147
+ return row.values[Math.min(i, Math.max(0, row.values.length - 1))] ?? 0;
148
+ };
149
+ const project = (i, j, z) => {
150
+ const nx = (nX <= 1 ? 0 : i / (nX - 1));
151
+ const ny = (nY <= 1 ? 0 : j / (nY - 1));
152
+ const nz = (z - zMin) / (zMax - zMin || 1);
153
+ return {
154
+ x: plot.x + plot.w * 0.18 + (nx - ny) * plot.w * 0.4 + ny * plot.w * 0.12,
155
+ y: plot.y + plot.h * 0.78 - (nx + ny) * plot.h * 0.22 - nz * plot.h * 0.48,
156
+ };
157
+ };
158
+ const quads = [];
159
+ for (let j = nY - 2; j >= 0; j--) {
160
+ for (let i = 0; i < nX - 1; i++) {
161
+ const z00 = zAt(j, i);
162
+ const z10 = zAt(j, i + 1);
163
+ const z11 = zAt(j + 1, i + 1);
164
+ const z01 = zAt(j + 1, i);
165
+ const color = def.series[Math.min(j, nYraw - 1)]?.color ?? "#4E79A7";
166
+ quads.push({
167
+ pts: [project(i, j, z00), project(i + 1, j, z10), project(i + 1, j + 1, z11), project(i, j + 1, z01)],
168
+ color,
169
+ stroke: !!def.wireframe,
170
+ });
171
+ }
172
+ }
173
+ return quads;
174
+ }
175
+ export function comboXyPlotX(xVal, cats, plot, xs) {
176
+ const n = Math.max(1, cats.length);
177
+ if (!cats.length) {
178
+ const xMin = xs.length ? Math.min(...xs) : 0;
179
+ const xMax = xs.length ? Math.max(...xs) : 1;
180
+ return plot.x + ((xVal - xMin) / (xMax - xMin || 1)) * plot.w;
181
+ }
182
+ const xMin = xs.length ? Math.min(...xs) : 0;
183
+ const xMax = xs.length ? Math.max(...xs) : 0;
184
+ const asCategory = xMin >= -0.5 && xMax <= Math.max(n, 1) && xMax - xMin <= n + 0.5;
185
+ if (asCategory) {
186
+ if (n === 1)
187
+ return plot.x + plot.w / 2;
188
+ return plot.x + (xVal / Math.max(1, n - 1)) * plot.w;
189
+ }
190
+ return plot.x + ((xVal - xMin) / (xMax - xMin || 1)) * plot.w;
191
+ }
192
+ export function pointLabelText(def, s, i, v) {
193
+ const d = s.dataLabels ?? def.dataLabels;
194
+ const showVal = d?.showVal ?? !!def.showValues;
195
+ if (!d && !showVal)
196
+ return undefined;
197
+ const parts = [];
198
+ if (d?.showSerName && s.name)
199
+ parts.push(s.name);
200
+ if (d?.showCatName && def.categories?.[i])
201
+ parts.push(def.categories[i]);
202
+ if (showVal) {
203
+ parts.push(Number.isInteger(v) ? String(v) : String(Math.round(v * 10) / 10));
204
+ }
205
+ if (d?.showPercent) {
206
+ const total = s.values.reduce((a, b) => a + Math.abs(b), 0);
207
+ parts.push(`${total ? Math.round((Math.abs(v) / total) * 100) : 0}%`);
208
+ }
209
+ if (d?.showBubbleSize && s.points?.[i]?.z != null)
210
+ parts.push(String(s.points[i].z));
211
+ const text = parts.filter(Boolean).join(d?.separator ?? ", ");
212
+ return text || undefined;
213
+ }
214
+ /** 3D 柱/条预览的挤出偏移(view3D rotX/rotY → 屏幕位移)。 */
215
+ export function chart3DOffset(def) {
216
+ if (!def.chart3D && !def.view3D)
217
+ return null;
218
+ const rotY = def.view3D?.rotY ?? 20;
219
+ const rotX = def.view3D?.rotX ?? 15;
220
+ const depth = Math.max(4, Math.min(16, (def.view3D?.depthPercent ?? 100) / 12));
221
+ return {
222
+ dx: depth * Math.sin((rotY * Math.PI) / 180),
223
+ dy: -depth * Math.sin((rotX * Math.PI) / 180),
224
+ };
225
+ }
226
+ /**
227
+ * Chart-wide text and line colours. `def.textColor` / `def.lineColor` (chartSpace txPr, axis spPr)
228
+ * win; otherwise the neutral greys PowerPoint uses on a white chart area. A single text colour
229
+ * gives labels at full strength and ticks / legend at 75% towards the line colour so a dark-theme
230
+ * chart keeps the same hierarchy a light one has.
231
+ */
232
+ export function chartTextStyle(def) {
233
+ const size = def.textSize && def.textSize > 0 ? def.textSize : 13;
234
+ if (!def.textColor && !def.lineColor)
235
+ return { text: "#111827", muted: "#6b7280", axis: "#9ca3af", grid: "#e5e7eb", size };
236
+ const text = def.textColor ?? "#111827";
237
+ const line = def.lineColor ?? mixHex(text, "#808080", 0.6);
238
+ return { text, muted: def.textColor ? mixHex(text, line, 0.35) : "#6b7280", axis: line, grid: line, size };
239
+ }
240
+ /** Linear mix of two #rrggbb colours; `t` = weight of `b`. */
241
+ export function mixHex(a, b, t) {
242
+ const pa = parseHex(a);
243
+ const pb = parseHex(b);
244
+ if (!pa || !pb)
245
+ return a;
246
+ const ch = (i) => Math.round(pa[i] * (1 - t) + pb[i] * t).toString(16).padStart(2, "0");
247
+ return `#${ch(0)}${ch(1)}${ch(2)}`;
248
+ }
249
+ function parseHex(color) {
250
+ const hex = color.replace("#", "");
251
+ if (hex.length !== 6 && hex.length !== 3)
252
+ return null;
253
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
254
+ const n = parseInt(full, 16);
255
+ if (!Number.isFinite(n))
256
+ return null;
257
+ return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
258
+ }
259
+ /**
260
+ * "Nice" value-axis ticks: about `count` steps at 1 / 2 / 5 × 10ⁿ. (0..1500 → 0, 500, 1000, 1500;
261
+ * not one line per hundred.)
262
+ */
263
+ export function niceTicks(min, max, count = 5) {
264
+ if (min === max) {
265
+ if (min === 0)
266
+ return [0, 1];
267
+ min = min - Math.abs(min) * 0.1;
268
+ max = max + Math.abs(max) * 0.1;
269
+ }
270
+ const range = max - min;
271
+ const rough = range / count;
272
+ const step = Math.pow(10, Math.floor(Math.log10(rough)));
273
+ const frac = rough / step;
274
+ const m = frac > 5 ? 10 : frac > 2 ? 5 : frac > 1 ? 2 : 1;
275
+ const s = step * m;
276
+ const lo = Math.floor(min / s) * s;
277
+ const hi = Math.ceil(max / s) * s;
278
+ const out = [];
279
+ for (let v = lo; v <= hi + 1e-9; v += s)
280
+ out.push(Number(v.toFixed(10)));
281
+ return out;
282
+ }
283
+ export function shadeHex(color, factor) {
284
+ const hex = color.replace("#", "");
285
+ if (hex.length !== 6 && hex.length !== 3)
286
+ return color;
287
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
288
+ const n = parseInt(full, 16);
289
+ if (!Number.isFinite(n))
290
+ return color;
291
+ const ch = (shift) => {
292
+ const v = Math.max(0, Math.min(255, Math.round(((n >> shift) & 0xff) * factor)));
293
+ return v.toString(16).padStart(2, "0");
294
+ };
295
+ return `#${ch(16)}${ch(8)}${ch(0)}`;
296
+ }