@lemonppt/renderer 0.2.0 → 0.2.2

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 (72) hide show
  1. package/dist/client/client-render.js +65 -5401
  2. package/dist/client/editor-script.js +3355 -0
  3. package/dist/client/theme-echarts.js +65 -25
  4. package/dist/client-render.d.ts +1 -7
  5. package/dist/client-render.d.ts.map +1 -1
  6. package/dist/client-render.js +1 -11
  7. package/dist/client-render.js.map +1 -1
  8. package/dist/editor-script.d.ts +1 -1
  9. package/dist/editor-script.d.ts.map +1 -1
  10. package/dist/editor-script.js +80 -77
  11. package/dist/editor-script.js.map +1 -1
  12. package/dist/export-pdf.js +1 -1
  13. package/dist/export-pdf.js.map +1 -1
  14. package/dist/export-pptx.d.ts +1132 -2
  15. package/dist/export-pptx.d.ts.map +1 -1
  16. package/dist/export-pptx.js +1208 -19525
  17. package/dist/export-pptx.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/render-editor-data.d.ts +21 -0
  23. package/dist/render-editor-data.d.ts.map +1 -0
  24. package/dist/render-editor-data.js +1068 -0
  25. package/dist/render-editor-data.js.map +1 -0
  26. package/dist/render.d.ts +0 -2
  27. package/dist/render.d.ts.map +1 -1
  28. package/dist/render.js +7 -323
  29. package/dist/render.js.map +1 -1
  30. package/dist/theme01-pptx.d.ts +12 -0
  31. package/dist/theme01-pptx.d.ts.map +1 -0
  32. package/dist/theme01-pptx.js +2954 -0
  33. package/dist/theme01-pptx.js.map +1 -0
  34. package/dist/theme02-pptx.d.ts +5 -0
  35. package/dist/theme02-pptx.d.ts.map +1 -0
  36. package/dist/theme02-pptx.js +1063 -0
  37. package/dist/theme02-pptx.js.map +1 -0
  38. package/dist/theme03-pptx.d.ts +5 -0
  39. package/dist/theme03-pptx.d.ts.map +1 -0
  40. package/dist/theme03-pptx.js +3279 -0
  41. package/dist/theme03-pptx.js.map +1 -0
  42. package/dist/theme04-pptx.d.ts +6 -0
  43. package/dist/theme04-pptx.d.ts.map +1 -0
  44. package/dist/theme04-pptx.js +5496 -0
  45. package/dist/theme04-pptx.js.map +1 -0
  46. package/dist/theme05-pptx.d.ts +5 -0
  47. package/dist/theme05-pptx.d.ts.map +1 -0
  48. package/dist/theme05-pptx.js +3152 -0
  49. package/dist/theme05-pptx.js.map +1 -0
  50. package/dist/theme06-pptx.d.ts +124 -0
  51. package/dist/theme06-pptx.d.ts.map +1 -0
  52. package/dist/theme06-pptx.js +8154 -0
  53. package/dist/theme06-pptx.js.map +1 -0
  54. package/dist/theme07-pptx.d.ts +49 -0
  55. package/dist/theme07-pptx.d.ts.map +1 -0
  56. package/dist/theme07-pptx.js +2579 -0
  57. package/dist/theme07-pptx.js.map +1 -0
  58. package/dist/theme08-pptx.d.ts +19 -0
  59. package/dist/theme08-pptx.d.ts.map +1 -0
  60. package/dist/theme08-pptx.js +1411 -0
  61. package/dist/theme08-pptx.js.map +1 -0
  62. package/dist/theme09-pptx.d.ts +18 -0
  63. package/dist/theme09-pptx.d.ts.map +1 -0
  64. package/dist/theme09-pptx.js +1513 -0
  65. package/dist/theme09-pptx.js.map +1 -0
  66. package/dist/theme10-pptx.d.ts +18 -0
  67. package/dist/theme10-pptx.d.ts.map +1 -0
  68. package/dist/theme10-pptx.js +2889 -0
  69. package/dist/theme10-pptx.js.map +1 -0
  70. package/package.json +7 -6
  71. package/templates/editor.html +580 -0
  72. package/templates/editor.js +222 -0
@@ -0,0 +1,2889 @@
1
+ // lemonPPT - AI-powered presentation generation
2
+ // Copyright (c) 2026 lemonforme
3
+ // SPDX-License-Identifier: AGPL-3.0-or-later
4
+ //
5
+ // theme10 金色指数 · 金融编辑风 — PPTX 版式专属渲染器。
6
+ // 解决 theme10 在 PPTX 导出中落到 "Unknown layout" 占位的问题。
7
+ //
8
+ // 设计要点:
9
+ // - 单一深色基底(墨黑金线底),但按 mood(aurora / obsidian / ember)给三档背景色,
10
+ // 翻页形成金融终端的明暗呼吸(与 Web 端三情绪渐变同源,PPTX 不支持渐变故取渐变首色)。
11
+ // - 强调色系统:冰蓝主 #4A7FD4 + 香槟金 #D9B977 / 铜 #C97A52 点睛,
12
+ // 全部经 WCAG 预校验,未复制 third-party theme10 实现。
13
+ // - appearance(primary / muted)仅调强调色浓度,不翻转明暗。
14
+ let S = {
15
+ appearance: 'primary',
16
+ fonts: { heading: 'Noto Sans SC', body: 'Noto Sans SC', mono: 'Space Mono' },
17
+ chartColors: ['4A7FD4', 'D9B977', 'E8A23A', 'C97A52', '9A82DC', '6F9BD8'],
18
+ };
19
+ /** 每次导出开始时由 export-pptx.ts 注入 appearance / 字体 / 图表色。 */
20
+ export function configureTheme10(state) {
21
+ S = state;
22
+ }
23
+ // ────────────────────────────────────────────────────────────────
24
+ // 三情绪基底背景(取 Web 端渐变首色,PPTX 不支持渐变故固化)
25
+ // ────────────────────────────────────────────────────────────────
26
+ const MOOD_BG = {
27
+ aurora: {
28
+ bg: '0A0E14', ink: 'EEF1F6', ink2: 'A9B2C0', ink3: '6E7889',
29
+ surface: '121823', surfaceElevated: '1A2230', border: '2A3340', borderStrong: '3D4A5C',
30
+ },
31
+ obsidian: {
32
+ bg: '05070B', ink: 'EEF1F6', ink2: 'A9B2C0', ink3: '6E7889',
33
+ surface: '0E141E', surfaceElevated: '16202C', border: '1F2733', borderStrong: '33404F',
34
+ },
35
+ ember: {
36
+ bg: '171320', ink: 'F3EDE6', ink2: 'C9B6AE', ink3: '8A7A82',
37
+ surface: '211A2B', surfaceElevated: '2E2438', border: '3A2F44', borderStrong: '4E3F5C',
38
+ },
39
+ };
40
+ const SPOT = {
41
+ primary: {
42
+ accent: '4A7FD4', accent2: '6F9BD8', gold: 'D9B977', goldAmber: 'E8A23A',
43
+ copper: 'C97A52', violet: '9A82DC', red: 'E0615A', green: '5FB89A',
44
+ },
45
+ muted: {
46
+ accent: '7CA0DE', accent2: '94B4E2', gold: 'C9AE83', goldAmber: 'D4943C',
47
+ copper: 'B97A5C', violet: 'AE9AE0', red: 'E07E78', green: '84C2AC',
48
+ },
49
+ };
50
+ /** 版式 → 默认 mood(与 Web 端组件默认情绪一致;props.mood 优先)。 */
51
+ const SUBSTRATE = {
52
+ theme10_cover_dusk_v1: 'aurora',
53
+ theme10_cover_field_v1: 'aurora',
54
+ theme10_cover_atmos_v1: 'obsidian',
55
+ theme10_cover_horizon_v1: 'ember',
56
+ theme10_cover_standard_v1: 'aurora',
57
+ theme10_cover_dawn_v1: 'ember',
58
+ theme10_chapter_v1: 'obsidian',
59
+ theme10_divider_v1: 'ember',
60
+ theme10_statement_section_v1: 'obsidian',
61
+ theme10_statement_v1: 'ember',
62
+ theme10_principles_v1: 'obsidian',
63
+ theme10_closing_v1: 'aurora',
64
+ // P1 影像图文 批次A
65
+ theme10_profile_v1: 'obsidian',
66
+ theme10_team_v1: 'ember',
67
+ theme10_quote_v1: 'obsidian',
68
+ theme10_editorial_v1: 'ember',
69
+ theme10_magazine_v1: 'obsidian',
70
+ // P1 影像图文 批次B
71
+ theme10_triptych_v1: 'ember',
72
+ theme10_strata_v1: 'obsidian',
73
+ theme10_spark_v1: 'obsidian',
74
+ theme10_testimonials_v1: 'ember',
75
+ theme10_feature_v1: 'obsidian',
76
+ // P1 影像图文 批次C
77
+ theme10_compareimg_v1: 'ember',
78
+ theme10_pinboard_v1: 'obsidian',
79
+ theme10_filmstrip_v1: 'obsidian',
80
+ theme10_inset_v1: 'aurora',
81
+ theme10_gallery2_v1: 'ember',
82
+ // P1 影像图文 批次D
83
+ theme10_mosaic_v1: 'aurora',
84
+ theme10_collage_v1: 'ember',
85
+ theme10_captioned_v1: 'obsidian',
86
+ theme10_showcase_v1: 'aurora',
87
+ theme10_poster_v1: 'ember',
88
+ // P1 影像图文 批次E
89
+ theme10_annotated_v1: 'aurora',
90
+ theme10_quoteimg_v1: 'ember',
91
+ theme10_quilt_v1: 'obsidian',
92
+ theme10_exhibit_v1: 'aurora',
93
+ theme10_medallions_v1: 'ember',
94
+ // P2 数据图表 批次F(基础图表 5 版式)
95
+ theme10_bar_v1: 'aurora',
96
+ theme10_hbar_v1: 'aurora',
97
+ theme10_line_v1: 'obsidian',
98
+ theme10_area_v1: 'aurora',
99
+ theme10_kpis_v1: 'ember',
100
+ // P2 数据图表 批次G(对比与构成 5 版式)
101
+ theme10_grouped_v1: 'aurora',
102
+ theme10_stack_v1: 'aurora',
103
+ theme10_donut_v1: 'ember',
104
+ theme10_pie_v1: 'ember',
105
+ theme10_waterfall_v1: 'obsidian',
106
+ // P2 数据图表 批次H(分布与关系 5 版式)
107
+ theme10_scatter_v1: 'obsidian',
108
+ theme10_bubble_v1: 'aurora',
109
+ theme10_radar_v1: 'obsidian',
110
+ theme10_radial_v1: 'ember',
111
+ theme10_heat_v1: 'aurora',
112
+ // P2 数据图表 批次I(趋势与时间 5 版式)
113
+ theme10_trend_v1: 'aurora',
114
+ theme10_range_v1: 'aurora',
115
+ theme10_candlestick_v1: 'obsidian',
116
+ theme10_ridgeline_v1: 'aurora',
117
+ theme10_calendar_v1: 'ember',
118
+ // P2 数据图表 批次J(分布与构成进阶 5 版式)
119
+ theme10_funnel_v1: 'aurora',
120
+ theme10_gauge_v1: 'ember',
121
+ theme10_bullet_v1: 'aurora',
122
+ theme10_box_v1: 'obsidian',
123
+ theme10_treemap_v1: 'aurora',
124
+ // P2 数据图表 批次K(关系与构成进阶 5 版式)
125
+ theme10_sankey_v1: 'aurora',
126
+ theme10_dumbbell_v1: 'obsidian',
127
+ theme10_histogram_v1: 'aurora',
128
+ theme10_slope_v1: 'aurora',
129
+ theme10_waffle_v1: 'ember',
130
+ // P2 数据图表 批次L(时序与分布 5 版式)
131
+ theme10_gantt_v1: 'aurora',
132
+ theme10_bump_v1: 'obsidian',
133
+ theme10_rose_v1: 'aurora',
134
+ theme10_dotplot_v1: 'obsidian',
135
+ theme10_timeline_v1: 'ember',
136
+ // P2 数据图表 批次M(结构与关系 5 版式)
137
+ theme10_orgchart_v1: 'aurora',
138
+ theme10_parallel_v1: 'obsidian',
139
+ theme10_circlepack_v1: 'aurora',
140
+ theme10_cscatter_v1: 'obsidian',
141
+ theme10_marimekko_v1: 'ember',
142
+ // P3 结构 / 流程 / 长尾(11 版式)
143
+ theme10_steps_v1: 'obsidian',
144
+ theme10_cycle_v1: 'obsidian',
145
+ theme10_swimlane_v1: 'ember',
146
+ theme10_checklist_v1: 'obsidian',
147
+ theme10_plans_v1: 'ember',
148
+ theme10_journey_v1: 'ember',
149
+ theme10_goals_v1: 'ember',
150
+ theme10_glossary_v1: 'obsidian',
151
+ theme10_faq_v1: 'ember',
152
+ theme10_isotype_v1: 'obsidian',
153
+ theme10_venn_v1: 'ember',
154
+ };
155
+ function P(layoutId, p) {
156
+ const mood = (p && p.mood) || SUBSTRATE[layoutId] || 'obsidian';
157
+ const base = MOOD_BG[mood];
158
+ const spot = SPOT[S.appearance];
159
+ return { mood, ...base, ...spot };
160
+ }
161
+ function series(pal) {
162
+ return [pal.accent, pal.gold, pal.goldAmber, pal.copper, pal.violet, pal.accent2];
163
+ }
164
+ // ────────────────────────────────────────────────────────────────
165
+ // P2 图表辅助(手绘 SVG 图表的 PPTX 等价渲染)
166
+ // ────────────────────────────────────────────────────────────────
167
+ function t10Fmt(n) {
168
+ const a = Math.abs(n);
169
+ if (a >= 1e9)
170
+ return (n / 1e9).toFixed(a >= 1e10 ? 0 : 1) + 'B';
171
+ if (a >= 1e6)
172
+ return (n / 1e6).toFixed(a >= 1e7 ? 0 : 1) + 'M';
173
+ if (a >= 1e3)
174
+ return (n / 1e3).toFixed(a >= 1e4 ? 0 : 1) + 'k';
175
+ return String(Math.round(n * 100) / 100);
176
+ }
177
+ function t10Nice(min, max, ticks = 4) {
178
+ if (!isFinite(min) || !isFinite(max))
179
+ return { min: 0, max: 1, step: 1, ticks: [0, 1] };
180
+ if (min === max)
181
+ max = min + 1;
182
+ const lo = Math.min(min, 0), hi = Math.max(max, 0);
183
+ const range = hi - lo, raw = range / ticks;
184
+ const mag = Math.pow(10, Math.floor(Math.log10(raw || 1)));
185
+ const norm = raw / mag;
186
+ let nn;
187
+ if (norm <= 1)
188
+ nn = 1;
189
+ else if (norm <= 2)
190
+ nn = 2;
191
+ else if (norm <= 2.5)
192
+ nn = 2.5;
193
+ else if (norm <= 5)
194
+ nn = 5;
195
+ else
196
+ nn = 10;
197
+ const step = nn * mag;
198
+ const nmin = Math.floor(lo / step) * step, nmax = Math.ceil(hi / step) * step;
199
+ const arr = [];
200
+ for (let v = nmin; v <= nmax + step * 1e-6; v += step)
201
+ arr.push(Number(v.toFixed(6)));
202
+ return { min: nmin, max: nmax, step, ticks: arr };
203
+ }
204
+ /** 横向网格线 + 左轴刻度标签(值越大越靠上) */
205
+ function t10Grid(slide, pal, x0, x1, yTop, yBot, sc) {
206
+ for (const t of sc.ticks) {
207
+ const y = yBot - (t - sc.min) / (sc.max - sc.min || 1) * (yBot - yTop);
208
+ const zero = Math.abs(t) < 1e-9;
209
+ slide.addShape('line', { x: x0, y, w: x1 - x0, h: 0, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1 : 0.75 } });
210
+ txt(slide, t10Fmt(t), { x: x0 - 0.05, y: y - 0.12, w: 0.82, h: 0.24, fontSize: 9, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
211
+ }
212
+ }
213
+ /** 折线线段(含上行翻转) */
214
+ function t10Seg(slide, x1, y1, x2, y2, color, width) {
215
+ const top = Math.min(y1, y2), bot = Math.max(y1, y2);
216
+ slide.addShape('line', { x: x1, y: top, w: Math.abs(x2 - x1), h: bot - top, line: { color, width }, flipV: y2 < y1 });
217
+ }
218
+ function t10Legend(slide, pal, items, x, y) {
219
+ let cx = x;
220
+ for (const it of items) {
221
+ slide.addShape('rect', { x: cx, y: y + 0.02, w: 0.16, h: 0.16, fill: { color: it.color } });
222
+ txt(slide, it.name, { x: cx + 0.22, y: y - 0.05, w: 1.5, h: 0.26, fontSize: 10, color: pal.ink2, fontFace: S.fonts.body });
223
+ cx += 0.22 + Math.min(String(it.name).length * 0.08 + 0.5, 1.7) + 0.28;
224
+ }
225
+ }
226
+ function t10NormCats(v) {
227
+ if (!Array.isArray(v))
228
+ return [];
229
+ return v.map((c) => (typeof c === 'string' ? c : String(c?.text ?? ''))).filter(Boolean);
230
+ }
231
+ function t10ParseVals(s) {
232
+ if (!s)
233
+ return [];
234
+ return s.split(',').map((x) => Number(x.trim())).filter((n) => isFinite(n));
235
+ }
236
+ function t10ParseXY(s) {
237
+ if (!s)
238
+ return [];
239
+ return s.split(/[\s;]+/).map((p) => p.split(',').map(Number)).filter((q) => q.length >= 2 && isFinite(q[0]) && isFinite(q[1]));
240
+ }
241
+ function t10ParseXYS(s) {
242
+ if (!s)
243
+ return [];
244
+ return s.split(/[\s;]+/).map((p) => p.split(',').map(Number)).filter((q) => q.length >= 3 && q.every((n) => isFinite(n)));
245
+ }
246
+ /** 两个 hex 之间线性插值(热力顺色阶) */
247
+ function t10MixHex(a, b, t) {
248
+ const pa = t10Hex(a), pb = t10Hex(b);
249
+ if (!pa || !pb)
250
+ return a;
251
+ const k = Math.max(0, Math.min(1, t));
252
+ const r = Math.round(pa[0] + (pb[0] - pa[0]) * k);
253
+ const g = Math.round(pa[1] + (pb[1] - pa[1]) * k);
254
+ const bl = Math.round(pa[2] + (pb[2] - pa[2]) * k);
255
+ return '#' + [r, g, bl].map((x) => x.toString(16).padStart(2, '0')).join('');
256
+ }
257
+ function t10Hex(h) {
258
+ const m = /^#?([0-9a-f]{6})$/i.exec(String(h).trim());
259
+ if (!m)
260
+ return null;
261
+ const n = parseInt(m[1], 16);
262
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
263
+ }
264
+ function t10Lum(hex) {
265
+ const c = t10Hex(hex);
266
+ if (!c)
267
+ return 0.5;
268
+ return (0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]) / 255;
269
+ }
270
+ // ────────────────────────────────────────────────────────────────
271
+ // 画布常量(16:9,10 × 5.625 英寸)
272
+ // ────────────────────────────────────────────────────────────────
273
+ const MX = 0.7;
274
+ const CW = 10 - MX * 2;
275
+ const TOP = 0.62;
276
+ const BOT = 5.18;
277
+ function lines(v) {
278
+ if (v == null)
279
+ return [];
280
+ if (Array.isArray(v))
281
+ return v.map(String);
282
+ return String(v).split(/\r?\n/).filter((s) => s.length > 0);
283
+ }
284
+ /** 归一化数组字段(编辑器可能存成 {item} 对象数组或纯字符串数组)。 */
285
+ function normStrings(v) {
286
+ if (!Array.isArray(v))
287
+ return [];
288
+ return v
289
+ .map((t) => (typeof t === 'string' ? t : (t?.item ?? '')))
290
+ .filter(Boolean);
291
+ }
292
+ /** 行情带单项 → mono 串(▲ 涨金 / ▼ 跌铜)。 */
293
+ function tickStr(t) {
294
+ if (t == null)
295
+ return '';
296
+ if (typeof t === 'string')
297
+ return t;
298
+ const o = t;
299
+ const code = o.code ?? '';
300
+ const val = o.value ?? '';
301
+ const d = o.delta == null ? '' : ` ${o.delta >= 0 ? '▲' : '▼'}${Math.abs(o.delta).toFixed(2)}%`;
302
+ return [code, val, d].filter(Boolean).join(' ');
303
+ }
304
+ function tickLine(v) {
305
+ if (!Array.isArray(v))
306
+ return '';
307
+ return v.map(tickStr).filter(Boolean).join(' ');
308
+ }
309
+ function txt(slide, text, o) {
310
+ if (text == null || text === '')
311
+ return;
312
+ if (Array.isArray(text)) {
313
+ if (text.length === 0)
314
+ return;
315
+ text = text.map(String).join('\n');
316
+ }
317
+ slide.addText(text, o);
318
+ }
319
+ function frame(slide, pal, opts = {}) {
320
+ slide.background = { color: pal.bg };
321
+ slide.addShape('line', { x: MX, y: TOP, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
322
+ if (opts.bottom !== false) {
323
+ slide.addShape('line', { x: MX, y: BOT, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
324
+ }
325
+ }
326
+ function kicker(slide, text, pal, o = {}) {
327
+ txt(slide, text, {
328
+ x: o.x ?? MX, y: o.y ?? 0.74, w: o.w ?? CW, h: 0.3,
329
+ fontSize: o.size ?? 12, color: pal.gold, align: 'left',
330
+ fontFace: S.fonts.mono, charSpacing: 2, bold: true,
331
+ });
332
+ }
333
+ function title(slide, text, pal, o = {}) {
334
+ const ls = lines(text);
335
+ const size = o.size ?? 40;
336
+ const h = o.h ?? ls.length * (size / 72) * 1.18 + 0.15;
337
+ slide.addText(ls.join('\n'), {
338
+ x: o.x ?? MX, y: o.y ?? 1.0, w: o.w ?? CW, h,
339
+ fontSize: size, color: o.color ?? pal.ink, align: o.align ?? 'left',
340
+ fontFace: S.fonts.heading, bold: true, valign: o.valign ?? 'top',
341
+ lineSpacingMultiple: 1.05, rotate: o.rotate, italic: o.italic,
342
+ });
343
+ }
344
+ function footer(slide, left, right, pal) {
345
+ txt(slide, left, { x: MX, y: BOT + 0.04, w: CW / 2, h: 0.3, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
346
+ txt(slide, right, { x: MX + CW / 2, y: BOT + 0.04, w: CW / 2, h: 0.3, fontSize: 9, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
347
+ }
348
+ function card(slide, x, y, w, h, pal, fill, stroke) {
349
+ slide.addShape('rect', { x, y, w, h, fill: { color: fill ?? pal.surfaceElevated }, line: { color: stroke ?? pal.borderStrong, width: 1 } });
350
+ }
351
+ function colorBar(slide, x, y, w, pal) {
352
+ const n = 6;
353
+ const g = 0.08;
354
+ const cw = (w - g * (n - 1)) / n;
355
+ const s = series(pal);
356
+ for (let i = 0; i < n; i++) {
357
+ slide.addShape('rect', { x: x + i * (cw + g), y, w: cw, h: 0.16, fill: { color: s[i] } });
358
+ }
359
+ }
360
+ function tryImage(slide, url, x, y, w, h, fallback) {
361
+ if (!url) {
362
+ fallback();
363
+ return;
364
+ }
365
+ try {
366
+ slide.addImage({ path: url, x, y, w, h });
367
+ }
368
+ catch {
369
+ fallback();
370
+ }
371
+ }
372
+ /** 内容页通用页眉(栏标 + 标题),返回正文起始 y。 */
373
+ function t10Head(slide, p, pal, o = {}) {
374
+ kicker(slide, p.kicker, pal, { y: 0.78 });
375
+ const size = o.size ?? 30;
376
+ title(slide, p.title, pal, { x: MX, y: 1.14, w: CW, size });
377
+ return 1.14 + size / 72 * 1.18 + 0.45;
378
+ }
379
+ // =====================================================================
380
+ // 1. cover_dusk · 暮光对角封面(aurora)
381
+ // =====================================================================
382
+ function renderCoverDusk(slide, p) {
383
+ const pal = P('theme10_cover_dusk_v1', p);
384
+ slide.background = { color: pal.bg };
385
+ // 顶部行情带
386
+ const tk = tickLine(p.ticker);
387
+ if (tk)
388
+ txt(slide, tk, { x: MX, y: 0.2, w: CW, h: 0.3, fontSize: 11, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
389
+ // 右侧金线(呼应 Web 端对角金线)
390
+ slide.addShape('line', { x: 6.6, y: 0, w: 0, h: 5.625, line: { color: pal.gold, width: 1.25 } });
391
+ kicker(slide, p.kicker, pal, { y: 1.25 });
392
+ title(slide, p.title, pal, { x: MX, y: 1.75, w: CW * 0.62, size: 44 });
393
+ txt(slide, p.subtitle, { x: MX, y: 3.5, w: CW * 0.56, h: 1.2, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.25 });
394
+ }
395
+ // =====================================================================
396
+ // 2. cover_field · 色场分栏封面(aurora)
397
+ // =====================================================================
398
+ function renderCoverField(slide, p) {
399
+ const pal = P('theme10_cover_field_v1', p);
400
+ slide.background = { color: pal.bg };
401
+ kicker(slide, p.kicker, pal, { y: 1.0 });
402
+ title(slide, p.title, pal, { x: MX, y: 1.5, w: CW * 0.5, size: 40 });
403
+ txt(slide, p.subtitle, { x: MX, y: 3.4, w: CW * 0.48, h: 1.2, fontSize: 13, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
404
+ // 右侧色场卡(冰蓝→金渐变以纯色金线描边近似)
405
+ card(slide, 5.9, 1.0, 3.4, 3.6, pal, pal.surface, pal.gold);
406
+ txt(slide, p.fieldLabel ?? 'FIELD', { x: 6.1, y: 1.2, w: 3.0, h: 0.3, fontSize: 11, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 2 });
407
+ txt(slide, String(p.fieldNo ?? '10'), { x: 6.1, y: 1.7, w: 3.0, h: 1.7, fontSize: 72, color: pal.ink, bold: true, align: 'left', fontFace: S.fonts.mono });
408
+ }
409
+ // =====================================================================
410
+ // 3. cover_atmos · 满版渐变大字(obsidian)
411
+ // =====================================================================
412
+ function renderCoverAtmos(slide, p) {
413
+ const pal = P('theme10_cover_atmos_v1', p);
414
+ slide.background = { color: pal.bg };
415
+ kicker(slide, p.kicker, pal, { y: 1.0 });
416
+ title(slide, p.title, pal, { x: MX, y: 2.0, w: CW, size: 54 });
417
+ if (p.en)
418
+ txt(slide, p.en, { x: MX, y: 3.8, w: CW, h: 0.4, fontSize: 16, color: pal.gold, italic: true, align: 'left', fontFace: S.fonts.mono });
419
+ }
420
+ // =====================================================================
421
+ // 4. cover_horizon · 地平线封面(ember)
422
+ // =====================================================================
423
+ function renderCoverHorizon(slide, p) {
424
+ const pal = P('theme10_cover_horizon_v1', p);
425
+ slide.background = { color: pal.bg };
426
+ txt(slide, p.issue ?? '', { x: MX + CW - 2, y: 0.55, w: 2, h: 0.3, fontSize: 12, color: pal.ink2, align: 'right', fontFace: S.fonts.mono });
427
+ // 地平线金线
428
+ slide.addShape('line', { x: 0, y: 3.35, w: 10, h: 0, line: { color: pal.gold, width: 1 } });
429
+ kicker(slide, p.kicker, pal, { y: 1.0 });
430
+ title(slide, p.title, pal, { x: MX, y: 3.7, w: CW, size: 40 });
431
+ txt(slide, p.subtitle, { x: MX, y: 4.5, w: CW * 0.7, h: 0.6, fontSize: 13, color: pal.ink2, align: 'left', fontFace: S.fonts.body, lineSpacingMultiple: 1.2 });
432
+ }
433
+ // =====================================================================
434
+ // 5. cover_standard · 标准封面(aurora)
435
+ // =====================================================================
436
+ function renderCoverStandard(slide, p) {
437
+ const pal = P('theme10_cover_standard_v1', p);
438
+ slide.background = { color: pal.bg };
439
+ if (p.stamp)
440
+ txt(slide, p.stamp, { x: MX + CW - 3, y: 0.55, w: 3, h: 0.3, fontSize: 11, color: pal.gold, align: 'right', bold: true, fontFace: S.fonts.mono });
441
+ kicker(slide, p.kicker, pal, { y: 1.7 });
442
+ title(slide, p.title, pal, { x: MX, y: 2.2, w: CW, size: 46, align: 'center' });
443
+ txt(slide, p.subtitle, { x: MX + CW * 0.15, y: 3.5, w: CW * 0.7, h: 0.8, fontSize: 14, color: pal.ink2, align: 'center', fontFace: S.fonts.body, lineSpacingMultiple: 1.2 });
444
+ const tk = tickLine(p.ticker);
445
+ if (tk)
446
+ txt(slide, tk, { x: MX, y: 4.7, w: CW, h: 0.3, fontSize: 11, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
447
+ colorBar(slide, MX, 4.98, CW, pal);
448
+ }
449
+ // =====================================================================
450
+ // 6. cover_dawn · 晨光卡封面(ember,含角嵌影像)
451
+ // =====================================================================
452
+ function renderCoverDawn(slide, p) {
453
+ const pal = P('theme10_cover_dawn_v1', p);
454
+ slide.background = { color: pal.bg };
455
+ // 角嵌影像
456
+ tryImage(slide, p.imageUrl, 7.5, 0.7, 1.8, 1.35, () => {
457
+ card(slide, 7.5, 0.7, 1.8, 1.35, pal, pal.surface, pal.borderStrong);
458
+ });
459
+ card(slide, MX, 1.5, CW - 0.1, 3.2, pal, pal.surface, pal.borderStrong);
460
+ kicker(slide, p.kicker, pal, { y: 1.75 });
461
+ title(slide, p.title, pal, { x: MX + 0.2, y: 2.2, w: CW * 0.6, size: 38 });
462
+ txt(slide, p.subtitle, { x: MX + 0.2, y: 3.7, w: CW * 0.6, h: 0.8, fontSize: 13, color: pal.ink2, align: 'left', fontFace: S.fonts.body, lineSpacingMultiple: 1.2 });
463
+ }
464
+ // =====================================================================
465
+ // 7. chapter · 章节索引(obsidian)
466
+ // =====================================================================
467
+ function renderChapter(slide, p) {
468
+ const pal = P('theme10_chapter_v1', p);
469
+ frame(slide, pal);
470
+ const no = p.no || '01';
471
+ txt(slide, no, { x: MX, y: 0.85, w: 3, h: 1.9, fontSize: 96, color: pal.gold, bold: true, align: 'left', fontFace: S.fonts.mono });
472
+ title(slide, p.name, pal, { x: MX, y: 2.95, w: CW, size: 38 });
473
+ const items = normStrings(p.items);
474
+ items.forEach((it, i) => {
475
+ const y = 3.55 + i * 0.42;
476
+ txt(slide, String(i + 1).padStart(2, '0'), { x: MX, y, w: 0.5, h: 0.3, fontSize: 12, color: pal.accent, bold: true, fontFace: S.fonts.mono });
477
+ txt(slide, it, { x: MX + 0.6, y, w: CW - 0.6, h: 0.3, fontSize: 13, color: pal.ink, fontFace: S.fonts.body });
478
+ slide.addShape('line', { x: MX, y: y + 0.34, w: CW, h: 0, line: { color: pal.border, width: 0.5 } });
479
+ });
480
+ footer(slide, 'CHAPTER', p.no ? `No. ${no}` : '', pal);
481
+ }
482
+ // =====================================================================
483
+ // 8. divider · 序号分章(ember)
484
+ // =====================================================================
485
+ function renderDivider(slide, p) {
486
+ const pal = P('theme10_divider_v1', p);
487
+ slide.background = { color: pal.bg };
488
+ txt(slide, p.no || '01', { x: CW / 2 - 1.2, y: 1.5, w: 2.4, h: 1.5, fontSize: 90, color: pal.gold, bold: true, align: 'center', fontFace: S.fonts.mono });
489
+ slide.addShape('rect', { x: 4.0, y: 3.15, w: 2.0, h: 0.06, fill: { color: pal.gold } });
490
+ title(slide, p.name, pal, { x: CW / 2 - 3, y: 3.35, w: 6, size: 32, align: 'center' });
491
+ }
492
+ // =====================================================================
493
+ // 9. statement_section · 宣言章节(obsidian,左宣言右索引)
494
+ // =====================================================================
495
+ function renderStatementSection(slide, p) {
496
+ const pal = P('theme10_statement_section_v1', p);
497
+ frame(slide, pal);
498
+ title(slide, p.quote, pal, { x: MX, y: 1.5, w: CW * 0.5, size: 28, valign: 'top' });
499
+ const items = normStrings(p.items);
500
+ const rx = 5.4;
501
+ const rw = CW - (rx - MX);
502
+ items.forEach((it, i) => {
503
+ const y = 1.5 + i * 0.52;
504
+ txt(slide, String(i + 1).padStart(2, '0'), { x: rx, y, w: 0.5, h: 0.3, fontSize: 12, color: pal.accent, bold: true, fontFace: S.fonts.mono });
505
+ txt(slide, it, { x: rx + 0.6, y, w: rw - 0.6, h: 0.3, fontSize: 14, color: pal.ink, fontFace: S.fonts.body });
506
+ slide.addShape('line', { x: rx, y: y + 0.38, w: rw, h: 0, line: { color: pal.border, width: 0.5 } });
507
+ });
508
+ }
509
+ // =====================================================================
510
+ // 10. statement · 声明金句(ember)
511
+ // =====================================================================
512
+ function renderStatement(slide, p) {
513
+ const pal = P('theme10_statement_v1', p);
514
+ slide.background = { color: pal.bg };
515
+ title(slide, p.word, pal, { x: MX, y: 1.8, w: CW * 0.82, size: 40, valign: 'top' });
516
+ if (p.sign)
517
+ txt(slide, p.sign, { x: MX, y: 4.5, w: CW, h: 0.3, fontSize: 13, color: pal.gold, align: 'left', fontFace: S.fonts.mono });
518
+ }
519
+ // =====================================================================
520
+ // 11. principles · 投资原则(obsidian,编号 + 刻度尺)
521
+ // =====================================================================
522
+ function renderPrinciples(slide, p) {
523
+ const pal = P('theme10_principles_v1', p);
524
+ frame(slide, pal);
525
+ const y0 = t10Head(slide, p, pal, { size: 30 });
526
+ const items = (Array.isArray(p.items) ? p.items : []).slice(0, 6);
527
+ const colW = (CW - 0.8) / 3;
528
+ items.forEach((it, i) => {
529
+ const col = i % 3;
530
+ const row = Math.floor(i / 3);
531
+ const x = MX + col * (colW + 0.4);
532
+ const y = y0 + row * (BOT - y0 - 0.3) / 2;
533
+ slide.addShape('rect', { x, y, w: colW, h: 0.04, fill: { color: pal.gold } });
534
+ txt(slide, it.no ?? String(i + 1).padStart(2, '0'), { x, y: y + 0.12, w: colW, h: 0.4, fontSize: 24, color: pal.accent, bold: true, fontFace: S.fonts.mono });
535
+ txt(slide, it.name ?? '', { x, y: y + 0.58, w: colW, h: 0.3, fontSize: 16, color: pal.ink, bold: true, fontFace: S.fonts.heading });
536
+ txt(slide, it.desc ?? '', { x, y: y + 0.92, w: colW, h: 0.9, fontSize: 11, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
537
+ });
538
+ }
539
+ // =====================================================================
540
+ // 12. closing · 封底结语(aurora,含满版影像)
541
+ // =====================================================================
542
+ function renderClosing(slide, p) {
543
+ const pal = P('theme10_closing_v1', p);
544
+ slide.background = { color: pal.bg };
545
+ // 满版影像 + 压暗
546
+ tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => {
547
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.surface } });
548
+ });
549
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.bg, transparency: 38 } });
550
+ kicker(slide, p.mark, pal, { y: 1.7, align: 'center', x: CW / 2 - 1.5, w: 3 });
551
+ title(slide, p.word, pal, { x: CW / 2 - 3.5, y: 2.2, w: 7, size: 40, align: 'center' });
552
+ txt(slide, p.subtitle, { x: CW / 2 - 3, y: 3.5, w: 6, h: 0.8, fontSize: 13, color: pal.ink2, align: 'center', fontFace: S.fonts.body, lineSpacingMultiple: 1.2 });
553
+ const colophon = normStrings(p.colophon);
554
+ if (colophon.length) {
555
+ txt(slide, colophon.join(' · '), { x: CW / 2 - 3, y: 4.5, w: 6, h: 0.3, fontSize: 11, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
556
+ }
557
+ }
558
+ // =====================================================================
559
+ // 13. profile · 人物特写(obsidian,左肖像出血 + 右履历条)
560
+ // =====================================================================
561
+ function renderProfile(slide, p) {
562
+ const pal = P('theme10_profile_v1', p);
563
+ slide.background = { color: pal.bg };
564
+ tryImage(slide, p.imageUrl, 0, 0, 4.2, 5.625, () => {
565
+ slide.addShape('rect', { x: 0, y: 0, w: 4.2, h: 5.625, fill: { color: pal.surface } });
566
+ });
567
+ slide.addShape('line', { x: 4.2, y: 0, w: 0, h: 5.625, line: { color: pal.gold, width: 1 } });
568
+ kicker(slide, p.section, pal, { x: 4.6, y: 0.55 });
569
+ title(slide, p.name, pal, { x: 4.6, y: 1.0, w: 5.0, size: 34 });
570
+ txt(slide, p.en, { x: 4.6, y: 1.95, w: 5.0, h: 0.3, fontSize: 13, color: pal.ink2, fontFace: S.fonts.mono });
571
+ txt(slide, p.title, { x: 4.6, y: 2.35, w: 5.0, h: 0.4, fontSize: 16, color: pal.gold, bold: true, fontFace: S.fonts.heading });
572
+ const rows = [['职务', p.role], ['年限', p.years], ['方向', p.focus]];
573
+ rows.forEach((r, i) => {
574
+ const y = 2.95 + i * 0.62;
575
+ txt(slide, r[0], { x: 4.6, y, w: 1.0, h: 0.3, fontSize: 11, color: pal.ink3, fontFace: S.fonts.mono });
576
+ txt(slide, r[1], { x: 5.7, y, w: 3.9, h: 0.3, fontSize: 13, color: pal.ink, fontFace: S.fonts.body });
577
+ slide.addShape('line', { x: 4.6, y: y + 0.42, w: 5.0, h: 0, line: { color: pal.border, width: 0.5 } });
578
+ });
579
+ txt(slide, p.bio, { x: 4.6, y: 4.95, w: 5.0, h: 0.55, fontSize: 11, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
580
+ }
581
+ // =====================================================================
582
+ // 14. team · 内容墙 / 团队(ember,3×2 网格 + 展签)
583
+ // =====================================================================
584
+ function renderTeam(slide, p) {
585
+ const pal = P('theme10_team_v1', p);
586
+ frame(slide, pal);
587
+ const y0 = t10Head(slide, p, pal, { size: 26 });
588
+ const cols = 3, rows = 2, gx = 0.3, gy = 0.3;
589
+ const cw = (CW - gx * (cols - 1)) / cols;
590
+ const areaH = BOT - y0 - 0.15;
591
+ const ch = (areaH - gy * (rows - 1)) / rows;
592
+ for (let i = 0; i < 6; i++) {
593
+ const c = i % cols, r = Math.floor(i / cols);
594
+ const x = MX + c * (cw + gx);
595
+ const y = y0 + r * (ch + gy);
596
+ card(slide, x, y, cw, ch, pal);
597
+ const imgH = ch * 0.6;
598
+ tryImage(slide, p.images?.[i]?.url, x + 0.12, y + 0.12, cw - 0.24, imgH, () => { });
599
+ txt(slide, p.images?.[i]?.name ?? '', { x: x + 0.12, y: y + imgH + 0.2, w: cw - 0.24, h: 0.3, fontSize: 14, color: pal.ink, bold: true, fontFace: S.fonts.heading });
600
+ txt(slide, p.images?.[i]?.role ?? '', { x: x + 0.12, y: y + imgH + 0.52, w: cw - 0.24, h: 0.3, fontSize: 11, color: pal.gold, fontFace: S.fonts.mono });
601
+ }
602
+ }
603
+ // =====================================================================
604
+ // 15. quote · 引言(obsidian,巨型引号 + mono 署名)
605
+ // =====================================================================
606
+ function renderQuote(slide, p) {
607
+ const pal = P('theme10_quote_v1', p);
608
+ slide.background = { color: pal.bg };
609
+ kicker(slide, p.section, pal, { y: 0.7 });
610
+ txt(slide, '“', { x: MX - 0.05, y: 0.95, w: 2, h: 1.1, fontSize: 86, color: pal.gold, bold: true, fontFace: S.fonts.heading, align: 'left' });
611
+ title(slide, p.quote, pal, { x: MX, y: 2.05, w: CW, size: 30, valign: 'top' });
612
+ txt(slide, p.sign, { x: MX, y: 4.45, w: CW, h: 0.3, fontSize: 15, color: pal.gold, bold: true, fontFace: S.fonts.heading });
613
+ if (p.source)
614
+ txt(slide, p.source, { x: MX, y: 4.8, w: CW, h: 0.3, fontSize: 12, color: pal.ink3, fontFace: S.fonts.mono });
615
+ }
616
+ // =====================================================================
617
+ // 16. editorial · 编排图文(ember,三栏文字 + 右出血图)
618
+ // =====================================================================
619
+ function renderEditorial(slide, p) {
620
+ const pal = P('theme10_editorial_v1', p);
621
+ slide.background = { color: pal.bg };
622
+ tryImage(slide, p.imageUrl, 6.2, 0, 3.8, 5.625, () => {
623
+ slide.addShape('rect', { x: 6.2, y: 0, w: 3.8, h: 5.625, fill: { color: pal.surface } });
624
+ });
625
+ slide.addShape('line', { x: 6.2, y: 0, w: 0, h: 5.625, line: { color: pal.gold, width: 1 } });
626
+ kicker(slide, p.section, pal, { x: MX, y: 0.6 });
627
+ title(slide, p.title, pal, { x: MX, y: 1.05, w: 5.0, size: 28 });
628
+ txt(slide, p.lead, { x: MX, y: 2.1, w: 5.0, h: 0.9, fontSize: 13, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
629
+ const body = normStrings(p.body).slice(0, 3);
630
+ const colW = (5.0 - 0.6) / 3;
631
+ body.forEach((t, i) => {
632
+ const x = MX + i * (colW + 0.3);
633
+ txt(slide, String(i + 1).padStart(2, '0'), { x, y: 3.3, w: colW, h: 0.3, fontSize: 12, color: pal.accent, bold: true, fontFace: S.fonts.mono });
634
+ txt(slide, t, { x, y: 3.65, w: colW, h: 1.4, fontSize: 11, color: pal.ink, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
635
+ });
636
+ }
637
+ // =====================================================================
638
+ // 17. magazine · 杂志图文(obsidian,左大图 + 右文 + 右下小图)
639
+ // =====================================================================
640
+ function renderMagazine(slide, p) {
641
+ const pal = P('theme10_magazine_v1', p);
642
+ slide.background = { color: pal.bg };
643
+ tryImage(slide, p.images?.[0]?.url, 0, 0, 4.6, 5.625, () => {
644
+ slide.addShape('rect', { x: 0, y: 0, w: 4.6, h: 5.625, fill: { color: pal.surface } });
645
+ });
646
+ slide.addShape('line', { x: 4.6, y: 0, w: 0, h: 5.625, line: { color: pal.gold, width: 1 } });
647
+ kicker(slide, p.section, pal, { x: 4.9, y: 0.5 });
648
+ title(slide, p.title, pal, { x: 4.9, y: 0.95, w: 4.6, size: 26 });
649
+ txt(slide, p.lead, { x: 4.9, y: 2.0, w: 4.6, h: 1.3, fontSize: 12, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
650
+ if (p.pull)
651
+ txt(slide, p.pull, { x: 4.9, y: 3.5, w: 4.6, h: 0.9, fontSize: 15, color: pal.gold, italic: true, fontFace: S.fonts.heading, valign: 'top', lineSpacingMultiple: 1.15 });
652
+ if (p.images?.[1]?.url)
653
+ tryImage(slide, p.images[1].url, 4.9, 4.55, 2.2, 0.9, () => { });
654
+ }
655
+ // =====================================================================
656
+ // 18. triptych · 三联影像(ember,3 等高图 + 图注)
657
+ // =====================================================================
658
+ function renderTriptych(slide, p) {
659
+ const pal = P('theme10_triptych_v1', p);
660
+ frame(slide, pal);
661
+ const y0 = t10Head(slide, p, pal, { size: 26 });
662
+ const cols = 3, gx = 0.3;
663
+ const cw = (CW - gx * (cols - 1)) / cols;
664
+ const areaH = BOT - y0 - 0.5;
665
+ for (let i = 0; i < 3; i++) {
666
+ const x = MX + i * (cw + gx);
667
+ card(slide, x, y0, cw, areaH, pal);
668
+ tryImage(slide, p.images?.[i]?.url, x + 0.12, y0 + 0.12, cw - 0.24, areaH - 0.6, () => { });
669
+ txt(slide, p.images?.[i]?.caption ?? '', { x: x + 0.12, y: y0 + areaH - 0.42, w: cw - 0.24, h: 0.34, fontSize: 11, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.1 });
670
+ }
671
+ }
672
+ // =====================================================================
673
+ // 19. strata · 横向影像带(obsidian,满宽 4 联 + 图注)
674
+ // =====================================================================
675
+ function renderStrata(slide, p) {
676
+ const pal = P('theme10_strata_v1', p);
677
+ frame(slide, pal);
678
+ const y0 = t10Head(slide, p, pal, { size: 26 });
679
+ const cols = 4, gx = 0.22;
680
+ const cw = (CW - gx * (cols - 1)) / cols;
681
+ const bandH = BOT - y0 - 0.5;
682
+ for (let i = 0; i < 4; i++) {
683
+ const x = MX + i * (cw + gx);
684
+ tryImage(slide, p.images?.[i]?.url, x, y0, cw, bandH - 0.4, () => {
685
+ card(slide, x, y0, cw, bandH - 0.4, pal);
686
+ });
687
+ txt(slide, String(i + 1).padStart(2, '0'), { x, y: y0 + bandH - 0.34, w: 0.5, h: 0.28, fontSize: 11, color: pal.gold, bold: true, fontFace: S.fonts.mono });
688
+ txt(slide, p.images?.[i]?.caption ?? '', { x: x + 0.4, y: y0 + bandH - 0.34, w: cw - 0.4, h: 0.28, fontSize: 10, color: pal.ink2, fontFace: S.fonts.body });
689
+ }
690
+ }
691
+ // =====================================================================
692
+ // 20. spark · 持仓小图集(obsidian,2×3 + 涨跌幅)
693
+ // =====================================================================
694
+ function renderSpark(slide, p) {
695
+ const pal = P('theme10_spark_v1', p);
696
+ frame(slide, pal);
697
+ const y0 = t10Head(slide, p, pal, { size: 26 });
698
+ const cols = 3, rows = 2, gx = 0.3, gy = 0.3;
699
+ const cw = (CW - gx * (cols - 1)) / cols;
700
+ const areaH = BOT - y0 - 0.55;
701
+ const ch = (areaH - gy * (rows - 1)) / rows;
702
+ for (let i = 0; i < 6; i++) {
703
+ const c = i % cols, r = Math.floor(i / cols);
704
+ const x = MX + c * (cw + gx);
705
+ const y = y0 + r * (ch + gy);
706
+ card(slide, x, y, cw, ch, pal);
707
+ const imgH = ch * 0.6;
708
+ tryImage(slide, p.images?.[i]?.url, x + 0.12, y + 0.12, cw - 0.24, imgH, () => { });
709
+ const up = !/^-/.test(String(p.images?.[i]?.value ?? '').trim());
710
+ txt(slide, p.images?.[i]?.name ?? '', { x: x + 0.12, y: y + imgH + 0.18, w: cw - 0.24, h: 0.28, fontSize: 13, color: pal.ink, bold: true, fontFace: S.fonts.heading });
711
+ txt(slide, p.images?.[i]?.value ?? '', { x: x + 0.12, y: y + imgH + 0.46, w: cw - 0.24, h: 0.26, fontSize: 12, color: up ? pal.green : pal.copper, bold: true, fontFace: S.fonts.mono });
712
+ }
713
+ txt(slide, '组合表现 · SPARK', { x: MX, y: BOT + 0.02, w: CW, h: 0.3, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 2 });
714
+ }
715
+ // =====================================================================
716
+ // 21. testimonials · 引述清单(ember,头像卡 + 三联短引述)
717
+ // =====================================================================
718
+ function renderTestimonials(slide, p) {
719
+ const pal = P('theme10_testimonials_v1', p);
720
+ frame(slide, pal);
721
+ kicker(slide, p.section, pal, { y: 0.78 });
722
+ // 主引述:左头像 + 右金句
723
+ tryImage(slide, p.imageUrl, MX, 1.25, 1.5, 1.5, () => {
724
+ card(slide, MX, 1.25, 1.5, 1.5, pal);
725
+ });
726
+ txt(slide, '“', { x: 2.1, y: 1.05, w: 1, h: 0.8, fontSize: 54, color: pal.gold, bold: true, fontFace: S.fonts.heading });
727
+ txt(slide, p.quote, { x: 2.4, y: 1.35, w: 7.0, h: 1.2, fontSize: 17, color: pal.ink, fontFace: S.fonts.heading, bold: true, valign: 'top', lineSpacingMultiple: 1.15 });
728
+ txt(slide, p.name, { x: 2.4, y: 2.6, w: 7.0, h: 0.3, fontSize: 14, color: pal.gold, bold: true, fontFace: S.fonts.heading });
729
+ txt(slide, p.title, { x: 2.4, y: 2.92, w: 7.0, h: 0.3, fontSize: 11, color: pal.ink2, fontFace: S.fonts.mono });
730
+ // 底部三联短引述
731
+ const sec = (Array.isArray(p.quotes) ? p.quotes : []).slice(0, 3);
732
+ const yS = 3.55;
733
+ const scw = (CW - 0.6) / 3;
734
+ sec.forEach((q, i) => {
735
+ const x = MX + i * (scw + 0.3);
736
+ slide.addShape('line', { x, y: yS, w: scw, h: 0, line: { color: pal.gold, width: 1 } });
737
+ txt(slide, q?.text ?? '', { x, y: yS + 0.12, w: scw, h: 1.0, fontSize: 11, color: pal.ink, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
738
+ if (q?.who)
739
+ txt(slide, q.who, { x, y: yS + 1.15, w: scw, h: 0.28, fontSize: 10, color: pal.ink3, fontFace: S.fonts.mono });
740
+ });
741
+ }
742
+ // =====================================================================
743
+ // 22. feature · 图文特写(obsidian,左大图出血 + 右批注)
744
+ // =====================================================================
745
+ function renderFeature(slide, p) {
746
+ const pal = P('theme10_feature_v1', p);
747
+ slide.background = { color: pal.bg };
748
+ tryImage(slide, p.imageUrl, 0, 0, 4.6, 5.625, () => {
749
+ slide.addShape('rect', { x: 0, y: 0, w: 4.6, h: 5.625, fill: { color: pal.surface } });
750
+ });
751
+ slide.addShape('line', { x: 4.6, y: 0, w: 0, h: 5.625, line: { color: pal.gold, width: 1 } });
752
+ kicker(slide, p.section, pal, { x: 4.9, y: 0.5 });
753
+ title(slide, p.title, pal, { x: 4.9, y: 0.95, w: 4.6, size: 26 });
754
+ txt(slide, p.lead, { x: 4.9, y: 2.0, w: 4.6, h: 1.1, fontSize: 12, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
755
+ const notes = (Array.isArray(p.notes) ? p.notes : []).slice(0, 3);
756
+ notes.forEach((n, i) => {
757
+ const y = 3.25 + i * 0.62;
758
+ slide.addShape('rect', { x: 4.9, y, w: 0.12, h: 0.5, fill: { color: pal.gold } });
759
+ txt(slide, n?.label ?? '', { x: 5.15, y, w: 4.35, h: 0.26, fontSize: 11, color: pal.gold, bold: true, fontFace: S.fonts.mono, charSpacing: 1 });
760
+ txt(slide, n?.text ?? '', { x: 5.15, y: y + 0.26, w: 4.35, h: 0.3, fontSize: 12, color: pal.ink, fontFace: S.fonts.body });
761
+ });
762
+ }
763
+ // =====================================================================
764
+ // 23. compareimg · 对照双图(ember,左右双图 + 角标图注)
765
+ // =====================================================================
766
+ function renderCompareImg(slide, p) {
767
+ const pal = P('theme10_compareimg_v1', p);
768
+ frame(slide, pal);
769
+ const y0 = t10Head(slide, p, pal, { size: 26 });
770
+ const list = Array.isArray(p.images) ? p.images : [];
771
+ const cols = list.length >= 2 ? 2 : 1;
772
+ const gx = 0.4;
773
+ const cw = (CW - gx * (cols - 1)) / cols;
774
+ const areaH = BOT - y0 - 0.5;
775
+ list.forEach((img, i) => {
776
+ const x = MX + i * (cw + gx);
777
+ card(slide, x, y0, cw, areaH, pal);
778
+ txt(slide, img?.tag ?? (i === 0 ? '对比 A' : '对比 B'), { x: x + 0.12, y: y0 + 0.12, w: cw - 0.24, h: 0.28, fontSize: 11, color: pal.gold, bold: true, fontFace: S.fonts.mono, charSpacing: 1 });
779
+ tryImage(slide, img?.url, x + 0.12, y0 + 0.46, cw - 0.24, areaH - 0.9, () => { });
780
+ txt(slide, img?.caption ?? '', { x: x + 0.12, y: y0 + areaH - 0.34, w: cw - 0.24, h: 0.3, fontSize: 11, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.1 });
781
+ });
782
+ }
783
+ // =====================================================================
784
+ // 24. pinboard · 钉板九宫格(obsidian,3×3 + 图注)
785
+ // =====================================================================
786
+ function renderPinboard(slide, p) {
787
+ const pal = P('theme10_pinboard_v1', p);
788
+ frame(slide, pal);
789
+ const y0 = t10Head(slide, p, pal, { size: 26 });
790
+ const cols = 3, rows = 3, gx = 0.22, gy = 0.22;
791
+ const cw = (CW - gx * (cols - 1)) / cols;
792
+ const areaH = BOT - y0 - 0.45;
793
+ const ch = (areaH - gy * (rows - 1)) / rows;
794
+ for (let i = 0; i < 9; i++) {
795
+ const c = i % cols, r = Math.floor(i / cols);
796
+ const x = MX + c * (cw + gx);
797
+ const y = y0 + r * (ch + gy);
798
+ card(slide, x, y, cw, ch, pal);
799
+ tryImage(slide, p.images?.[i]?.url, x + 0.1, y + 0.1, cw - 0.2, ch - 0.42, () => { });
800
+ txt(slide, p.images?.[i]?.caption ?? '', { x: x + 0.1, y: y + ch - 0.3, w: cw - 0.2, h: 0.26, fontSize: 9, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.05 });
801
+ }
802
+ }
803
+ // =====================================================================
804
+ // 25. filmstrip · 横向胶片条(obsidian,满宽七联 + 帧号)
805
+ // =====================================================================
806
+ function renderFilmstrip(slide, p) {
807
+ const pal = P('theme10_filmstrip_v1', p);
808
+ slide.background = { color: pal.bg };
809
+ const y0 = t10Head(slide, p, pal, { size: 26 });
810
+ const n = 7, gx = 0.16;
811
+ const cw = (CW - gx * (n - 1)) / n;
812
+ const areaH = BOT - y0 - 0.55;
813
+ slide.addShape('line', { x: MX, y: y0 - 0.08, w: CW, h: 0, line: { color: pal.gold, width: 1 } });
814
+ for (let i = 0; i < n; i++) {
815
+ const x = MX + i * (cw + gx);
816
+ txt(slide, String(i + 1).padStart(2, '0'), { x, y: y0 + 0.02, w: cw, h: 0.26, fontSize: 10, color: pal.gold, bold: true, fontFace: S.fonts.mono, align: 'left' });
817
+ tryImage(slide, p.images?.[i]?.url, x, y0 + 0.32, cw, areaH - 0.4, () => {
818
+ card(slide, x, y0 + 0.32, cw, areaH - 0.4, pal);
819
+ });
820
+ txt(slide, p.images?.[i]?.caption ?? '', { x, y: y0 + areaH - 0.04, w: cw, h: 0.28, fontSize: 8.5, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.0 });
821
+ }
822
+ slide.addShape('line', { x: MX, y: y0 + areaH + 0.04, w: CW, h: 0, line: { color: pal.gold, width: 1 } });
823
+ }
824
+ // =====================================================================
825
+ // 26. inset · 满版内嵌(aurora,满版影像 + 内嵌文字卡)
826
+ // =====================================================================
827
+ function renderInset(slide, p) {
828
+ const pal = P('theme10_inset_v1', p);
829
+ slide.background = { color: pal.bg };
830
+ tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => {
831
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.surface } });
832
+ });
833
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.bg, transparency: 34 } });
834
+ // 内嵌文字卡(左下)
835
+ const cx = MX, cw = 5.6;
836
+ kicker(slide, p.section, pal, { x: cx, y: 1.5 });
837
+ title(slide, p.title, pal, { x: cx, y: 1.95, w: cw, size: 30 });
838
+ txt(slide, p.lead, { x: cx, y: 3.15, w: cw, h: 0.95, fontSize: 13, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
839
+ const notes = (Array.isArray(p.notes) ? p.notes : []).slice(0, 3).filter((n) => n && n.text);
840
+ notes.forEach((n, i) => {
841
+ const y = 4.25 + i * 0.42;
842
+ slide.addShape('rect', { x: cx, y, w: 0.1, h: 0.34, fill: { color: pal.gold } });
843
+ txt(slide, n.label, { x: cx + 0.2, y, w: cw - 0.2, h: 0.22, fontSize: 9, color: pal.gold, bold: true, fontFace: S.fonts.mono, charSpacing: 1 });
844
+ txt(slide, n.text, { x: cx + 0.2, y: y + 0.2, w: cw - 0.2, h: 0.22, fontSize: 11, color: pal.ink, fontFace: S.fonts.body });
845
+ });
846
+ }
847
+ // =====================================================================
848
+ // 27. gallery2 · 画廊六格(ember,3×2 + 图注)
849
+ // =====================================================================
850
+ function renderGallery2(slide, p) {
851
+ const pal = P('theme10_gallery2_v1', p);
852
+ frame(slide, pal);
853
+ const y0 = t10Head(slide, p, pal, { size: 26 });
854
+ const cols = 3, rows = 2, gx = 0.3, gy = 0.3;
855
+ const cw = (CW - gx * (cols - 1)) / cols;
856
+ const areaH = BOT - y0 - 0.5;
857
+ const ch = (areaH - gy * (rows - 1)) / rows;
858
+ for (let i = 0; i < 6; i++) {
859
+ const c = i % cols, r = Math.floor(i / cols);
860
+ const x = MX + c * (cw + gx);
861
+ const y = y0 + r * (ch + gy);
862
+ card(slide, x, y, cw, ch, pal);
863
+ tryImage(slide, p.images?.[i]?.url, x + 0.12, y + 0.12, cw - 0.24, ch - 0.55, () => { });
864
+ txt(slide, p.images?.[i]?.caption ?? '', { x: x + 0.12, y: y + ch - 0.38, w: cw - 0.24, h: 0.3, fontSize: 10, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.1 });
865
+ }
866
+ }
867
+ // =====================================================================
868
+ // 28. mosaic · 马赛克拼贴(aurora,第一列加宽的不规则 3×2)
869
+ // =====================================================================
870
+ function renderMosaic(slide, p) {
871
+ const pal = P('theme10_mosaic_v1', p);
872
+ frame(slide, pal);
873
+ const y0 = t10Head(slide, p, pal, { size: 26 });
874
+ const gx = 0.22, gy = 0.22;
875
+ const cw0 = (CW - gx * 2) * 0.42;
876
+ const cw1 = (CW - gx * 2 - cw0) / 2;
877
+ const xs = [MX, MX + cw0 + gx, MX + cw0 + gx + cw1 + gx];
878
+ const cws = [cw0, cw1, cw1];
879
+ const areaH = BOT - y0 - 0.45;
880
+ const ch = (areaH - gy) / 2;
881
+ for (let i = 0; i < 6; i++) {
882
+ const c = i % 3, r = Math.floor(i / 3);
883
+ const x = xs[c], y = y0 + r * (ch + gy), w = cws[c];
884
+ card(slide, x, y, w, ch, pal);
885
+ tryImage(slide, p.images?.[i]?.url, x + 0.1, y + 0.1, w - 0.2, ch - 0.42, () => { });
886
+ txt(slide, p.images?.[i]?.caption ?? '', { x: x + 0.1, y: y + ch - 0.3, w: w - 0.2, h: 0.26, fontSize: 9, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.05 });
887
+ }
888
+ }
889
+ // =====================================================================
890
+ // 29. collage · 自由拼贴(ember,主图左通栏高 + 右三图竖叠)
891
+ // =====================================================================
892
+ function renderCollage(slide, p) {
893
+ const pal = P('theme10_collage_v1', p);
894
+ frame(slide, pal);
895
+ const y0 = t10Head(slide, p, pal, { size: 26 });
896
+ const gx = 0.3;
897
+ const areaH = BOT - y0 - 0.45;
898
+ const cw0 = (CW - gx) * 0.58;
899
+ const cw1 = CW - gx - cw0;
900
+ const x1 = MX + cw0 + gx;
901
+ card(slide, MX, y0, cw0, areaH, pal);
902
+ tryImage(slide, p.images?.[0]?.url, MX + 0.12, y0 + 0.12, cw0 - 0.24, areaH - 0.42, () => { });
903
+ txt(slide, p.images?.[0]?.caption ?? '', { x: MX + 0.12, y: y0 + areaH - 0.3, w: cw0 - 0.24, h: 0.26, fontSize: 10, color: pal.gold, bold: true, fontFace: S.fonts.mono, valign: 'top' });
904
+ const gy = 0.22;
905
+ const ch = (areaH - gy * 2) / 3;
906
+ for (let i = 0; i < 3; i++) {
907
+ const y = y0 + i * (ch + gy);
908
+ card(slide, x1, y, cw1, ch, pal);
909
+ tryImage(slide, p.images?.[i + 1]?.url, x1 + 0.1, y + 0.1, cw1 - 0.2, ch - 0.4, () => { });
910
+ txt(slide, p.images?.[i + 1]?.caption ?? '', { x: x1 + 0.1, y: y + ch - 0.28, w: cw1 - 0.2, h: 0.24, fontSize: 9, color: pal.ink2, fontFace: S.fonts.body, valign: 'top' });
911
+ }
912
+ }
913
+ // =====================================================================
914
+ // 30. captioned · 图注组(obsidian,三联大图 + 标题/说明图注)
915
+ // =====================================================================
916
+ function renderCaptioned(slide, p) {
917
+ const pal = P('theme10_captioned_v1', p);
918
+ frame(slide, pal);
919
+ const y0 = t10Head(slide, p, pal, { size: 26 });
920
+ const gx = 0.3, cols = 3;
921
+ const cw = (CW - gx * (cols - 1)) / cols;
922
+ const areaH = BOT - y0 - 0.45;
923
+ const imgH = areaH * 0.66;
924
+ const capH = areaH - imgH;
925
+ for (let i = 0; i < 3; i++) {
926
+ const x = MX + i * (cw + gx);
927
+ card(slide, x, y0, cw, areaH, pal);
928
+ tryImage(slide, p.images?.[i]?.url, x + 0.12, y0 + 0.12, cw - 0.24, imgH - 0.24, () => { });
929
+ txt(slide, p.images?.[i]?.caption ?? '', { x: x + 0.12, y: y0 + imgH, w: cw - 0.24, h: 0.28, fontSize: 11, bold: true, color: pal.ink, fontFace: S.fonts.heading, valign: 'top' });
930
+ txt(slide, p.images?.[i]?.note ?? '', { x: x + 0.12, y: y0 + imgH + 0.3, w: cw - 0.24, h: capH - 0.36, fontSize: 9.5, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.08 });
931
+ }
932
+ }
933
+ // =====================================================================
934
+ // 31. showcase · 陈列橱窗(aurora,主图左 + 右 2×2 缩略)
935
+ // =====================================================================
936
+ function renderShowcase(slide, p) {
937
+ const pal = P('theme10_showcase_v1', p);
938
+ frame(slide, pal);
939
+ const y0 = t10Head(slide, p, pal, { size: 26 });
940
+ const gx = 0.3;
941
+ const areaH = BOT - y0 - 0.45;
942
+ const cw0 = (CW - gx) * 0.6;
943
+ const cw1 = CW - gx - cw0;
944
+ const x1 = MX + cw0 + gx;
945
+ card(slide, MX, y0, cw0, areaH, pal);
946
+ tryImage(slide, p.images?.[0]?.url, MX + 0.12, y0 + 0.12, cw0 - 0.24, areaH - 0.42, () => { });
947
+ txt(slide, p.images?.[0]?.caption ?? '', { x: MX + 0.12, y: y0 + areaH - 0.3, w: cw0 - 0.24, h: 0.26, fontSize: 10, color: pal.gold, bold: true, fontFace: S.fonts.mono, valign: 'top' });
948
+ const gy = 0.2;
949
+ const ch = (areaH - gy) / 2;
950
+ const w = (cw1 - gy) / 2;
951
+ for (let i = 0; i < 4; i++) {
952
+ const r = Math.floor(i / 2), c = i % 2;
953
+ const x = x1 + c * (w + gy);
954
+ const y = y0 + r * (ch + gy);
955
+ card(slide, x, y, w, ch, pal);
956
+ tryImage(slide, p.images?.[i + 1]?.url, x + 0.08, y + 0.08, w - 0.16, ch - 0.32, () => { });
957
+ txt(slide, p.images?.[i + 1]?.caption ?? '', { x: x + 0.08, y: y + ch - 0.24, w: w - 0.16, h: 0.2, fontSize: 8, color: pal.ink2, fontFace: S.fonts.body, valign: 'top' });
958
+ }
959
+ }
960
+ // =====================================================================
961
+ // 32. poster · 海报(ember,满版影像 + 底部压字大标题)
962
+ // =====================================================================
963
+ function renderPoster(slide, p) {
964
+ const pal = P('theme10_poster_v1', p);
965
+ slide.background = { color: pal.bg };
966
+ tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => {
967
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.surface } });
968
+ });
969
+ slide.addShape('rect', { x: 0, y: 3.35, w: 10, h: 2.275, fill: { color: pal.bg, transparency: 35 } });
970
+ const cx = MX, cw = 8.4;
971
+ kicker(slide, p.section, pal, { x: cx, y: 3.5 });
972
+ title(slide, p.title, pal, { x: cx, y: 3.85, w: cw, size: 38 });
973
+ txt(slide, p.tagline, { x: cx, y: 4.95, w: cw, h: 0.7, fontSize: 13, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
974
+ footer(slide, p.folioLeft, p.folioRight, pal);
975
+ }
976
+ // =====================================================================
977
+ // 33. annotated · 注解图(aurora,底图 + 编号注解列表)
978
+ // =====================================================================
979
+ function renderAnnotated(slide, p) {
980
+ const pal = P('theme10_annotated_v1', p);
981
+ frame(slide, pal);
982
+ const y0 = t10Head(slide, p, pal, { size: 26 });
983
+ const areaH = BOT - y0 - 0.45;
984
+ const imgW = (CW - 0.4) * 0.62;
985
+ const noteX = MX + imgW + 0.4;
986
+ const noteW = CW - imgW - 0.4;
987
+ card(slide, MX, y0, imgW, areaH, pal);
988
+ tryImage(slide, p.imageUrl, MX + 0.12, y0 + 0.12, imgW - 0.24, areaH - 0.24, () => { });
989
+ const notes = (Array.isArray(p.annotations) ? p.annotations : []).slice(0, 3);
990
+ notes.forEach((n, i) => {
991
+ const y = y0 + i * ((areaH - 0.2) / 3) + 0.1;
992
+ slide.addShape('rect', { x: noteX, y: y + 0.04, w: 0.34, h: 0.34, fill: { color: pal.gold } });
993
+ txt(slide, String(i + 1), { x: noteX, y: y + 0.04, w: 0.34, h: 0.34, fontSize: 14, bold: true, color: pal.bg, align: 'center', fontFace: S.fonts.mono });
994
+ txt(slide, n?.text ?? '', { x: noteX + 0.46, y, w: noteW - 0.46, h: areaH / 3 - 0.2, fontSize: 12, color: pal.ink, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
995
+ });
996
+ }
997
+ // =====================================================================
998
+ // 34. quoteimg · 引言图(ember,满版影像 + 居中大引述)
999
+ // =====================================================================
1000
+ function renderQuoteImg(slide, p) {
1001
+ const pal = P('theme10_quoteimg_v1', p);
1002
+ slide.background = { color: pal.bg };
1003
+ tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => {
1004
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.surface } });
1005
+ });
1006
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.bg, transparency: 38 } });
1007
+ kicker(slide, p.section, pal, { x: MX, y: 1.0 });
1008
+ txt(slide, p.quote, { x: MX, y: 1.7, w: CW, h: 2.6, fontSize: 30, bold: true, color: pal.ink, fontFace: S.fonts.heading, valign: 'top', lineSpacingMultiple: 1.12 });
1009
+ txt(slide, p.attribution, { x: MX, y: 4.5, w: CW, h: 0.4, fontSize: 13, color: pal.gold, fontFace: S.fonts.mono, charSpacing: 1 });
1010
+ footer(slide, p.folioLeft, p.folioRight, pal);
1011
+ }
1012
+ // =====================================================================
1013
+ // 35. quilt · 拼布(obsidian,4×2 紧凑影像拼布)
1014
+ // =====================================================================
1015
+ function renderQuilt(slide, p) {
1016
+ const pal = P('theme10_quilt_v1', p);
1017
+ frame(slide, pal);
1018
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1019
+ const cols = 4, rows = 2, gx = 0.14, gy = 0.14;
1020
+ const cw = (CW - gx * (cols - 1)) / cols;
1021
+ const areaH = BOT - y0 - 0.45;
1022
+ const ch = (areaH - gy * (rows - 1)) / rows;
1023
+ for (let i = 0; i < 8; i++) {
1024
+ const c = i % cols, r = Math.floor(i / cols);
1025
+ const x = MX + c * (cw + gx);
1026
+ const y = y0 + r * (ch + gy);
1027
+ card(slide, x, y, cw, ch, pal);
1028
+ tryImage(slide, p.images?.[i]?.url, x + 0.06, y + 0.06, cw - 0.12, ch - 0.12, () => { });
1029
+ }
1030
+ }
1031
+ // =====================================================================
1032
+ // 36. exhibit · 陈列展(aurora,带框展品 + 展签文本)
1033
+ // =====================================================================
1034
+ function renderExhibit(slide, p) {
1035
+ const pal = P('theme10_exhibit_v1', p);
1036
+ frame(slide, pal);
1037
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1038
+ const areaH = BOT - y0 - 0.45;
1039
+ const imgW = (CW - 0.4) * 0.6;
1040
+ const labelX = MX + imgW + 0.4;
1041
+ const labelW = CW - imgW - 0.4;
1042
+ card(slide, MX, y0, imgW, areaH, pal);
1043
+ tryImage(slide, p.imageUrl, MX + 0.12, y0 + 0.12, imgW - 0.24, areaH - 0.24, () => { });
1044
+ txt(slide, p.meta, { x: labelX, y: y0 + 0.1, w: labelW, h: 0.3, fontSize: 11, color: pal.gold, bold: true, fontFace: S.fonts.mono, charSpacing: 1 });
1045
+ slide.addShape('line', { x: labelX, y: y0 + 0.5, w: labelW, h: 0, line: { color: pal.gold, width: 1 } });
1046
+ txt(slide, p.desc, { x: labelX, y: y0 + 0.66, w: labelW, h: areaH - 0.8, fontSize: 14, color: pal.ink, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.4 });
1047
+ }
1048
+ // =====================================================================
1049
+ // 37. medallions · 徽章墙(ember,3×2 圆形徽章 + 标签)
1050
+ // =====================================================================
1051
+ function renderMedallions(slide, p) {
1052
+ const pal = P('theme10_medallions_v1', p);
1053
+ frame(slide, pal);
1054
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1055
+ const cols = 3, gx = 0.5;
1056
+ const cw = (CW - gx * (cols - 1)) / cols;
1057
+ const areaH = BOT - y0 - 0.45;
1058
+ const rowH = (areaH - 0.4) / 2;
1059
+ const d = Math.min(cw, rowH - 0.4);
1060
+ for (let i = 0; i < 6; i++) {
1061
+ const c = i % cols, r = Math.floor(i / cols);
1062
+ const cx = MX + c * (cw + gx) + cw / 2;
1063
+ const cy = y0 + r * rowH + d / 2;
1064
+ const x = cx - d / 2, y = cy - d / 2;
1065
+ slide.addShape('oval', { x, y, w: d, h: d, fill: { color: pal.surfaceElevated }, line: { color: pal.gold, width: 1.5 } });
1066
+ tryImage(slide, p.images?.[i]?.url, x + 0.08, y + 0.08, d - 0.16, d - 0.16, () => { });
1067
+ txt(slide, p.images?.[i]?.label ?? '', { x: cx - cw / 2, y: y + d + 0.08, w: cw, h: 0.3, fontSize: 12, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.heading, valign: 'top' });
1068
+ }
1069
+ }
1070
+ // =====================================================================
1071
+ // 38. bar · 竖向柱状图(aurora,1–3 序列分组)
1072
+ // =====================================================================
1073
+ function renderBar(slide, p) {
1074
+ const pal = P('theme10_bar_v1', p);
1075
+ frame(slide, pal);
1076
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1077
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1078
+ const cats = t10NormCats(p.categories);
1079
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({
1080
+ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color,
1081
+ }));
1082
+ const n = Math.max(cats.length, 1), m = Math.max(sers.length, 1);
1083
+ const all = sers.flatMap((s) => s.values);
1084
+ const max = all.length ? Math.max(...all) : 1;
1085
+ const min = all.length ? Math.min(...all, 0) : 0;
1086
+ const sc = t10Nice(min, max, 4);
1087
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1088
+ const band = (x1 - x0) / n, inner = band * 0.6, barW = inner / m;
1089
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1090
+ const colors = series(pal);
1091
+ for (let i = 0; i < n; i++) {
1092
+ const bx = x0 + i * band + (band - inner) / 2;
1093
+ for (let si = 0; si < m; si++) {
1094
+ const s = sers[si], v = s.values[i] ?? 0;
1095
+ const top = yOf(v), base = yOf(0), h = Math.abs(top - base);
1096
+ slide.addShape('rect', {
1097
+ x: bx + si * barW + 0.02, y: Math.min(top, base), w: Math.max(barW - 0.04, 0.04), h: Math.max(h, 0.02),
1098
+ fill: { color: s.color ?? colors[si] },
1099
+ });
1100
+ }
1101
+ if (cats[i])
1102
+ txt(slide, cats[i], { x: x0 + i * band, y: plotBot + 0.05, w: band, h: 0.26, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1103
+ }
1104
+ if (m > 1)
1105
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), x0, y0 - 0.34);
1106
+ footer(slide, p.folioLeft, p.folioRight, pal);
1107
+ }
1108
+ // =====================================================================
1109
+ // 39. hbar · 横向排名条(aurora,降序)
1110
+ // =====================================================================
1111
+ function renderHBar(slide, p) {
1112
+ const pal = P('theme10_hbar_v1', p);
1113
+ frame(slide, pal);
1114
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1115
+ const xL = MX + 1.7, xR = MX + CW - 0.2, yTop = y0 + 0.12, yBot = BOT - 0.3;
1116
+ const rows = (Array.isArray(p.items) ? p.items : [])
1117
+ .map((it) => ({ name: it?.name ?? '', value: Number(it?.value ?? 0) }))
1118
+ .sort((a, b) => b.value - a.value).slice(0, 8);
1119
+ const max = rows.length ? Math.max(...rows.map((r) => r.value), 1) : 1;
1120
+ const sc = t10Nice(0, max, 4);
1121
+ const xOf = (v) => xL + (v - sc.min) / (sc.max - sc.min || 1) * (xR - xL);
1122
+ for (const t of sc.ticks) {
1123
+ const px = xOf(t), zero = Math.abs(t) < 1e-9;
1124
+ slide.addShape('line', { x: px, y: yTop - 0.1, w: 0, h: yBot - yTop + 0.2, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1 : 0.75 } });
1125
+ txt(slide, t10Fmt(t), { x: px - 0.4, y: yBot + 0.04, w: 0.8, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1126
+ }
1127
+ const colors = series(pal);
1128
+ const rowH = (yBot - yTop) / Math.max(rows.length, 1);
1129
+ rows.forEach((r, i) => {
1130
+ const cy = yTop + i * rowH + rowH / 2;
1131
+ txt(slide, r.name, { x: MX, y: cy - 0.12, w: 1.55, h: 0.26, fontSize: 10, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
1132
+ const bw = Math.max(xOf(r.value) - xL, 0.04);
1133
+ slide.addShape('rect', { x: xL, y: cy - rowH * 0.3, w: bw, h: rowH * 0.6, fill: { color: colors[i % 6] }, line: { color: pal.border, width: 0.5 } });
1134
+ txt(slide, t10Fmt(r.value), { x: xL + bw + 0.08, y: cy - 0.12, w: 1.2, h: 0.26, fontSize: 10, bold: true, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
1135
+ });
1136
+ footer(slide, p.folioLeft, p.folioRight, pal);
1137
+ }
1138
+ // =====================================================================
1139
+ // 40. line · 折线趋势(obsidian,多序列)
1140
+ // =====================================================================
1141
+ function renderLine(slide, p) {
1142
+ const pal = P('theme10_line_v1', p);
1143
+ frame(slide, pal);
1144
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1145
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1146
+ const cats = t10NormCats(p.categories);
1147
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({
1148
+ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color,
1149
+ }));
1150
+ const n = Math.max(cats.length, 1), m = Math.max(sers.length, 1);
1151
+ const all = sers.flatMap((s) => s.values);
1152
+ const max = all.length ? Math.max(...all) : 1;
1153
+ const min = all.length ? Math.min(...all, 0) : 0;
1154
+ const sc = t10Nice(min, max, 4);
1155
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1156
+ const band = (x1 - x0) / n;
1157
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1158
+ const xs = cats.map((_, i) => (n === 1 ? (x0 + x1) / 2 : x0 + (i / (n - 1)) * (x1 - x0)));
1159
+ const colors = series(pal);
1160
+ sers.forEach((s, si) => {
1161
+ const col = s.color ?? colors[si];
1162
+ const ys = s.values.map((v) => yOf(v));
1163
+ for (let i = 0; i < ys.length - 1; i++)
1164
+ t10Seg(slide, xs[i], ys[i], xs[i + 1], ys[i + 1], col, 2.2);
1165
+ ys.forEach((yy, i) => slide.addShape('oval', { x: xs[i] - 0.05, y: yy - 0.05, w: 0.1, h: 0.1, fill: { color: col } }));
1166
+ });
1167
+ cats.forEach((c, i) => txt(slide, c, { x: xs[i] - band / 2, y: plotBot + 0.05, w: band, h: 0.26, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body }));
1168
+ if (m > 1)
1169
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), x0, y0 - 0.34);
1170
+ footer(slide, p.folioLeft, p.folioRight, pal);
1171
+ }
1172
+ // =====================================================================
1173
+ // 41. area · 面积图(aurora,单序列渐变填充近似为折线+端点)
1174
+ // =====================================================================
1175
+ function renderArea(slide, p) {
1176
+ const pal = P('theme10_area_v1', p);
1177
+ frame(slide, pal);
1178
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1179
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1180
+ const cats = t10NormCats(p.categories);
1181
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({
1182
+ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color,
1183
+ }));
1184
+ const vals = sers[0]?.values ?? [];
1185
+ const n = Math.max(cats.length, 1);
1186
+ const max = vals.length ? Math.max(...vals) : 1;
1187
+ const min = vals.length ? Math.min(...vals, 0) : 0;
1188
+ const sc = t10Nice(min, max, 4);
1189
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1190
+ if (vals.length) {
1191
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1192
+ const xs = vals.map((_, i) => (n === 1 ? (x0 + x1) / 2 : x0 + (i / (n - 1)) * (x1 - x0)));
1193
+ const col = sers[0]?.color ?? pal.accent;
1194
+ const ys = vals.map((v) => yOf(v));
1195
+ for (let i = 0; i < ys.length - 1; i++)
1196
+ t10Seg(slide, xs[i], ys[i], xs[i + 1], ys[i + 1], col, 2.4);
1197
+ ys.forEach((yy, i) => slide.addShape('oval', { x: xs[i] - 0.05, y: yy - 0.05, w: 0.1, h: 0.1, fill: { color: col } }));
1198
+ cats.forEach((c, i) => txt(slide, c, { x: xs[i] - (x1 - x0) / n / 2, y: plotBot + 0.05, w: (x1 - x0) / n, h: 0.26, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body }));
1199
+ }
1200
+ footer(slide, p.folioLeft, p.folioRight, pal);
1201
+ }
1202
+ // =====================================================================
1203
+ // 42. kpis · 指标卡(ember,2×2 大数字)
1204
+ // =====================================================================
1205
+ function renderKpis(slide, p) {
1206
+ const pal = P('theme10_kpis_v1', p);
1207
+ frame(slide, pal);
1208
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1209
+ const areaH = BOT - y0 - 0.45, gx = 0.3, gy = 0.3;
1210
+ const cw = (CW - gx) / 2, ch = (areaH - gy) / 2;
1211
+ const items = (Array.isArray(p.items) ? p.items : []).slice(0, 4);
1212
+ for (let i = 0; i < 4; i++) {
1213
+ const c = i % 2, r = Math.floor(i / 2);
1214
+ const x = MX + c * (cw + gx), y = y0 + r * (ch + gy);
1215
+ const it = items[i] ?? {};
1216
+ card(slide, x, y, cw, ch, pal);
1217
+ txt(slide, it.label ?? '', { x: x + 0.25, y: y + 0.2, w: cw - 0.5, h: 0.3, fontSize: 12, color: pal.ink2, fontFace: S.fonts.body });
1218
+ const up = it.delta != null && !/^-/.test(String(it.delta).trim());
1219
+ const valStr = `${it.value ?? ''}${it.unit ? ' ' + it.unit : ''}`;
1220
+ txt(slide, valStr, { x: x + 0.25, y: y + 0.52, w: cw - 0.5, h: 0.9, fontSize: 40, bold: true, color: pal.ink, fontFace: S.fonts.mono });
1221
+ slide.addShape('line', { x: x + 0.25, y: y + 1.5, w: cw - 0.5, h: 0, line: { color: pal.gold, width: 1 } });
1222
+ txt(slide, it.delta ?? '', { x: x + 0.25, y: y + 1.6, w: cw - 0.5, h: 0.3, fontSize: 13, bold: true, color: up ? pal.green : pal.red, fontFace: S.fonts.mono });
1223
+ }
1224
+ footer(slide, p.folioLeft, p.folioRight, pal);
1225
+ }
1226
+ // =====================================================================
1227
+ // 43. grouped · 分组柱状图(aurora,柱顶数值)
1228
+ // =====================================================================
1229
+ function renderGrouped(slide, p) {
1230
+ const pal = P('theme10_grouped_v1', p);
1231
+ frame(slide, pal);
1232
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1233
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1234
+ const cats = t10NormCats(p.categories);
1235
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({
1236
+ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color,
1237
+ }));
1238
+ const n = Math.max(cats.length, 1), m = Math.max(sers.length, 1);
1239
+ const all = sers.flatMap((s) => s.values);
1240
+ const max = all.length ? Math.max(...all) : 1;
1241
+ const min = all.length ? Math.min(...all, 0) : 0;
1242
+ const sc = t10Nice(min, max, 4);
1243
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1244
+ const band = (x1 - x0) / n, inner = band * 0.58, barW = inner / m;
1245
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1246
+ const colors = series(pal);
1247
+ for (let i = 0; i < n; i++) {
1248
+ const bx = x0 + i * band + (band - inner) / 2;
1249
+ for (let si = 0; si < m; si++) {
1250
+ const s = sers[si], v = s.values[i] ?? 0;
1251
+ const top = yOf(v), base = yOf(0), h = Math.abs(top - base);
1252
+ const col = s.color ?? colors[si];
1253
+ slide.addShape('rect', { x: bx + si * barW + 0.02, y: Math.min(top, base), w: Math.max(barW - 0.04, 0.04), h: Math.max(h, 0.02), fill: { color: col } });
1254
+ txt(slide, t10Fmt(v), { x: bx + si * barW, y: Math.min(top, base) - 0.28, w: barW, h: 0.24, fontSize: 8, color: pal.ink2, align: 'center', fontFace: S.fonts.mono });
1255
+ }
1256
+ if (cats[i])
1257
+ txt(slide, cats[i], { x: x0 + i * band, y: plotBot + 0.05, w: band, h: 0.26, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1258
+ }
1259
+ if (m > 1)
1260
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), x0, y0 - 0.34);
1261
+ footer(slide, p.folioLeft, p.folioRight, pal);
1262
+ }
1263
+ // =====================================================================
1264
+ // 44. stack · 堆叠柱状图(aurora,柱顶合计)
1265
+ // =====================================================================
1266
+ function renderStack(slide, p) {
1267
+ const pal = P('theme10_stack_v1', p);
1268
+ frame(slide, pal);
1269
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1270
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1271
+ const cats = t10NormCats(p.categories);
1272
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({
1273
+ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color,
1274
+ }));
1275
+ const n = Math.max(cats.length, 1), m = Math.max(sers.length, 1);
1276
+ const totals = cats.map((_, i) => sers.reduce((a, s) => a + (s.values[i] ?? 0), 0));
1277
+ const max = totals.length ? Math.max(...totals, 1) : 1;
1278
+ const sc = t10Nice(0, max, 4);
1279
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1280
+ const band = (x1 - x0) / n, barW = band * 0.5;
1281
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1282
+ const colors = series(pal);
1283
+ for (let i = 0; i < n; i++) {
1284
+ const bx = x0 + i * band + (band - barW) / 2;
1285
+ let cum = 0;
1286
+ for (let si = 0; si < m; si++) {
1287
+ const s = sers[si], v = s.values[i] ?? 0, col = s.color ?? colors[si];
1288
+ const yTop = yOf(cum + v), yBot = yOf(cum);
1289
+ slide.addShape('rect', { x: bx, y: yTop, w: barW, h: Math.max(yBot - yTop, 0.02), fill: { color: col } });
1290
+ cum += v;
1291
+ }
1292
+ txt(slide, t10Fmt(cum), { x: bx, y: yOf(cum) - 0.28, w: barW, h: 0.24, fontSize: 8, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
1293
+ if (cats[i])
1294
+ txt(slide, cats[i], { x: x0 + i * band, y: plotBot + 0.05, w: band, h: 0.26, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1295
+ }
1296
+ if (m > 1)
1297
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), x0, y0 - 0.34);
1298
+ footer(slide, p.folioLeft, p.folioRight, pal);
1299
+ }
1300
+ // =====================================================================
1301
+ // 45. donut · 环形图(ember,圆心合计 + 右侧图例)
1302
+ // angleRange 约定:0°=正东(3 点钟),顺时针为正(与 SVG 一致)
1303
+ // =====================================================================
1304
+ function renderDonut(slide, p) {
1305
+ const pal = P('theme10_donut_v1', p);
1306
+ frame(slide, pal);
1307
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1308
+ const items = (Array.isArray(p.items) ? p.items : []).map((it, i) => ({ name: it?.name ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) }));
1309
+ const total = items.reduce((a, it) => a + Math.max(it.value, 0), 0) || 1;
1310
+ const cx = 3.4, cy = y0 + (BOT - y0 - 0.4) / 2, R = 1.3, rIn = 0.82;
1311
+ const colors = series(pal);
1312
+ let acc = 0;
1313
+ items.forEach((it, i) => {
1314
+ const f0 = acc / total;
1315
+ acc += Math.max(it.value, 0);
1316
+ const f1 = acc / total;
1317
+ const a0 = ((270 + f0 * 360) % 360 + 360) % 360;
1318
+ const sweep = (f1 - f0) * 360;
1319
+ slide.addShape('pie', { x: cx - R, y: cy - R, w: 2 * R, h: 2 * R, fill: { color: colors[i % 6] }, angleRange: [a0, sweep] });
1320
+ });
1321
+ slide.addShape('oval', { x: cx - rIn, y: cy - rIn, w: 2 * rIn, h: 2 * rIn, fill: { color: pal.bg } });
1322
+ txt(slide, t10Fmt(total), { x: cx - 1.0, y: cy - 0.16, w: 2.0, h: 0.5, fontSize: 26, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
1323
+ txt(slide, p.centerLabel ?? '总计', { x: cx - 1.0, y: cy + 0.36, w: 2.0, h: 0.3, fontSize: 11, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
1324
+ const lx = cx + R + 0.38, ly0 = y0 + 0.35, lrow = 0.62;
1325
+ items.forEach((it, i) => {
1326
+ const ly = ly0 + i * lrow, pct = Math.round((Math.max(it.value, 0) / total) * 100);
1327
+ slide.addShape('rect', { x: lx, y: ly + 0.02, w: 0.18, h: 0.18, fill: { color: colors[i % 6] } });
1328
+ txt(slide, it.name, { x: lx + 0.26, y: ly - 0.04, w: 3.0, h: 0.28, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
1329
+ txt(slide, pct + '%', { x: lx + 0.26, y: ly + 0.26, w: 3.0, h: 0.26, fontSize: 11, bold: true, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
1330
+ });
1331
+ footer(slide, p.folioLeft, p.folioRight, pal);
1332
+ }
1333
+ // =====================================================================
1334
+ // 46. pie · 饼图(ember,全扇区 + 右侧图例)
1335
+ // =====================================================================
1336
+ function renderPie(slide, p) {
1337
+ const pal = P('theme10_pie_v1', p);
1338
+ frame(slide, pal);
1339
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1340
+ const items = (Array.isArray(p.items) ? p.items : []).map((it, i) => ({ name: it?.name ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) }));
1341
+ const total = items.reduce((a, it) => a + Math.max(it.value, 0), 0) || 1;
1342
+ const cx = 3.4, cy = y0 + (BOT - y0 - 0.4) / 2, R = 1.5;
1343
+ const colors = series(pal);
1344
+ let acc = 0;
1345
+ items.forEach((it, i) => {
1346
+ const f0 = acc / total;
1347
+ acc += Math.max(it.value, 0);
1348
+ const f1 = acc / total;
1349
+ const a0 = ((270 + f0 * 360) % 360 + 360) % 360;
1350
+ const sweep = (f1 - f0) * 360;
1351
+ slide.addShape('pie', { x: cx - R, y: cy - R, w: 2 * R, h: 2 * R, fill: { color: colors[i % 6] }, angleRange: [a0, sweep] });
1352
+ });
1353
+ const lx = cx + R + 0.38, ly0 = y0 + 0.35, lrow = 0.62;
1354
+ items.forEach((it, i) => {
1355
+ const ly = ly0 + i * lrow, pct = Math.round((Math.max(it.value, 0) / total) * 100);
1356
+ slide.addShape('rect', { x: lx, y: ly + 0.02, w: 0.18, h: 0.18, fill: { color: colors[i % 6] } });
1357
+ txt(slide, it.name, { x: lx + 0.26, y: ly - 0.04, w: 3.0, h: 0.28, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
1358
+ txt(slide, pct + '%', { x: lx + 0.26, y: ly + 0.26, w: 3.0, h: 0.26, fontSize: 11, bold: true, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
1359
+ });
1360
+ footer(slide, p.folioLeft, p.folioRight, pal);
1361
+ }
1362
+ // =====================================================================
1363
+ // 47. waterfall · 瀑布图(obsidian,浮动柱 + 连接线)
1364
+ // =====================================================================
1365
+ function renderWaterfall(slide, p) {
1366
+ const pal = P('theme10_waterfall_v1', p);
1367
+ frame(slide, pal);
1368
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1369
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1370
+ const items = (Array.isArray(p.items) ? p.items : []).map((it, i) => ({ name: it?.name ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) }));
1371
+ const n = Math.max(items.length, 1);
1372
+ const run = [];
1373
+ let cur = 0;
1374
+ for (const it of items) {
1375
+ run.push(cur);
1376
+ cur += it.value;
1377
+ }
1378
+ const all = [0, ...run.map((_, i) => run[i] + items[i].value), cur];
1379
+ const minV = Math.min(...all, 0), maxV = Math.max(...all, 1);
1380
+ const sc = t10Nice(minV, maxV, 4);
1381
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1382
+ const band = (x1 - x0) / n, inner = band * 0.56;
1383
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1384
+ for (let i = 1; i < n; i++) {
1385
+ const afterPrev = run[i - 1] + items[i - 1].value, ya = yOf(afterPrev);
1386
+ const xS = x0 + (i - 1) * band + (band - inner) / 2 + inner;
1387
+ const xE = x0 + i * band + (band - inner) / 2;
1388
+ t10Seg(slide, xS, ya, xE, ya, pal.borderStrong, 0.75);
1389
+ }
1390
+ items.forEach((it, i) => {
1391
+ const before = run[i], after = before + it.value;
1392
+ const yTop = yOf(Math.max(before, after)), yBot = yOf(Math.min(before, after));
1393
+ const bx = x0 + i * band + (band - inner) / 2, up = it.value >= 0;
1394
+ const col = up ? pal.green : pal.red;
1395
+ slide.addShape('rect', { x: bx, y: yTop, w: inner, h: Math.max(yBot - yTop, 0.02), fill: { color: col } });
1396
+ txt(slide, t10Fmt(it.value), { x: bx, y: up ? yTop - 0.28 : yBot + 0.02, w: inner, h: 0.24, fontSize: 8, bold: true, color: col, align: 'center', fontFace: S.fonts.mono });
1397
+ txt(slide, it.name, { x: x0 + i * band, y: plotBot + 0.05, w: band, h: 0.26, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1398
+ });
1399
+ footer(slide, p.folioLeft, p.folioRight, pal);
1400
+ }
1401
+ // =====================================================================
1402
+ // 48. scatter · 散点图(obsidian,双数值轴)
1403
+ // =====================================================================
1404
+ function renderScatter(slide, p) {
1405
+ const pal = P('theme10_scatter_v1', p);
1406
+ frame(slide, pal);
1407
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1408
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1409
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({ name: sr?.name ?? `序列 ${i + 1}`, pts: t10ParseXY(sr?.points), color: sr?.color }));
1410
+ const flat = sers.flatMap((s) => s.pts);
1411
+ const allX = flat.map((q) => q[0]);
1412
+ const allY = flat.map((q) => q[1]);
1413
+ const scx = t10Nice(allX.length ? Math.min(...allX) : 0, allX.length ? Math.max(...allX, 1) : 1, 5);
1414
+ const scy = t10Nice(allY.length ? Math.min(...allY, 0) : 0, allY.length ? Math.max(...allY, 1) : 1, 4);
1415
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, scy);
1416
+ const xOf = (v) => x0 + (v - scx.min) / (scx.max - scx.min || 1) * (x1 - x0);
1417
+ const yOf = (v) => plotBot - (v - scy.min) / (scy.max - scy.min || 1) * (plotBot - plotTop);
1418
+ const colors = series(pal);
1419
+ for (const t of scx.ticks) {
1420
+ const x = xOf(t);
1421
+ const zero = Math.abs(t) < 1e-9;
1422
+ slide.addShape('line', { x, y: plotTop, w: 0, h: plotBot - plotTop, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1 : 0.5 } });
1423
+ txt(slide, t10Fmt(t), { x: x - 0.4, y: plotBot + 0.04, w: 0.8, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1424
+ }
1425
+ sers.forEach((s, si) => {
1426
+ const col = s.color ?? colors[si];
1427
+ s.pts.forEach((q) => {
1428
+ const cx = xOf(q[0]), cy = yOf(q[1]);
1429
+ slide.addShape('oval', { x: cx - 0.07, y: cy - 0.07, w: 0.14, h: 0.14, fill: { color: col } });
1430
+ });
1431
+ });
1432
+ if (sers.length > 1)
1433
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), x0, y0 - 0.34);
1434
+ if (p.xLabel)
1435
+ txt(slide, p.xLabel, { x: (x0 + x1) / 2 - 0.5, y: plotBot + 0.32, w: 1.0, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
1436
+ if (p.yLabel)
1437
+ txt(slide, p.yLabel, { x: MX - 0.05, y: plotTop - 0.02, w: 1.0, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
1438
+ footer(slide, p.folioLeft, p.folioRight, pal);
1439
+ }
1440
+ // =====================================================================
1441
+ // 49. bubble · 气泡图(aurora,x/y/尺寸)
1442
+ // =====================================================================
1443
+ function renderBubble(slide, p) {
1444
+ const pal = P('theme10_bubble_v1', p);
1445
+ frame(slide, pal);
1446
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1447
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1448
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({ name: sr?.name ?? `序列 ${i + 1}`, pts: t10ParseXYS(sr?.points), color: sr?.color }));
1449
+ const flat = sers.flatMap((s) => s.pts);
1450
+ const allX = flat.map((q) => q[0]);
1451
+ const allY = flat.map((q) => q[1]);
1452
+ const allS = flat.map((q) => q[2]);
1453
+ const scx = t10Nice(allX.length ? Math.min(...allX) : 0, allX.length ? Math.max(...allX, 1) : 1, 5);
1454
+ const scy = t10Nice(allY.length ? Math.min(...allY, 0) : 0, allY.length ? Math.max(...allY, 1) : 1, 4);
1455
+ const minS = allS.length ? Math.min(...allS) : 0;
1456
+ const maxS = allS.length ? Math.max(...allS, 1) : 1;
1457
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, scy);
1458
+ const xOf = (v) => x0 + (v - scx.min) / (scx.max - scx.min || 1) * (x1 - x0);
1459
+ const yOf = (v) => plotBot - (v - scy.min) / (scy.max - scy.min || 1) * (plotBot - plotTop);
1460
+ const rOf = (sz) => 0.08 + ((sz - minS) / (maxS - minS || 1)) * 0.34;
1461
+ const colors = series(pal);
1462
+ for (const t of scx.ticks) {
1463
+ const x = xOf(t);
1464
+ const zero = Math.abs(t) < 1e-9;
1465
+ slide.addShape('line', { x, y: plotTop, w: 0, h: plotBot - plotTop, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1 : 0.5 } });
1466
+ txt(slide, t10Fmt(t), { x: x - 0.4, y: plotBot + 0.04, w: 0.8, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1467
+ }
1468
+ sers.forEach((s, si) => {
1469
+ const col = s.color ?? colors[si];
1470
+ s.pts.forEach((q) => {
1471
+ const cx = xOf(q[0]), cy = yOf(q[1]), r = rOf(q[2]);
1472
+ slide.addShape('oval', { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r, fill: { color: col }, line: { color: col, width: 0.75 } });
1473
+ });
1474
+ });
1475
+ if (sers.length > 1)
1476
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), x0, y0 - 0.34);
1477
+ footer(slide, p.folioLeft, p.folioRight, pal);
1478
+ }
1479
+ // =====================================================================
1480
+ // 50. radar · 雷达图(obsidian,多维多边形)
1481
+ // =====================================================================
1482
+ function renderRadar(slide, p) {
1483
+ const pal = P('theme10_radar_v1', p);
1484
+ frame(slide, pal);
1485
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1486
+ const axes = t10NormCats(p.axes);
1487
+ const n = Math.max(axes.length, 3);
1488
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color }));
1489
+ const allV = sers.flatMap((s) => s.values);
1490
+ const sc = t10Nice(0, allV.length ? Math.max(...allV, 1) : 100, 4);
1491
+ const xL = MX + 0.5, xR = MX + CW - 0.5, yT = y0 + 0.15, yB = BOT - 0.35;
1492
+ const cx = (xL + xR) / 2, cy = (yT + yB) / 2, R = (Math.min(xR - xL, yB - yT) / 2) * 0.82, rings = 4;
1493
+ const ang = (i) => ((-90 + (i * 360) / n) * Math.PI) / 180;
1494
+ const ptAt = (v, i) => { const r = (v / (sc.max || 1)) * R; return [cx + r * Math.cos(ang(i)), cy + r * Math.sin(ang(i))]; };
1495
+ for (let k = 1; k <= rings; k++) {
1496
+ const f = (k / rings) * sc.max;
1497
+ for (let i = 0; i < n; i++) {
1498
+ const a = ptAt(f, i), b = ptAt(f, (i + 1) % n);
1499
+ t10Seg(slide, a[0], a[1], b[0], b[1], pal.border, 0.5);
1500
+ }
1501
+ }
1502
+ for (let i = 0; i < n; i++) {
1503
+ const o = ptAt(0, i), e = ptAt(sc.max, i);
1504
+ t10Seg(slide, o[0], o[1], e[0], e[1], pal.border, 0.5);
1505
+ if (axes[i]) {
1506
+ const lx = cx + (R + 0.3) * Math.cos(ang(i)), ly = cy + (R + 0.3) * Math.sin(ang(i));
1507
+ txt(slide, axes[i], { x: lx - 0.7, y: ly - 0.13, w: 1.4, h: 0.26, fontSize: 10, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1508
+ }
1509
+ }
1510
+ const colors = series(pal);
1511
+ sers.forEach((s, si) => {
1512
+ const col = s.color ?? colors[si];
1513
+ for (let i = 0; i < n; i++) {
1514
+ const a = ptAt(s.values[i] ?? 0, i), b = ptAt(s.values[(i + 1) % n] ?? 0, (i + 1) % n);
1515
+ t10Seg(slide, a[0], a[1], b[0], b[1], col, 1.5);
1516
+ }
1517
+ });
1518
+ if (sers.length > 1)
1519
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), MX + 0.5, y0 - 0.34);
1520
+ footer(slide, p.folioLeft, p.folioRight, pal);
1521
+ }
1522
+ // =====================================================================
1523
+ // 51. radial · 径向柱图(ember,玫瑰扇区)
1524
+ // angleRange 约定:0°=正东(3 点钟),顺时针为正(与 SVG 一致)
1525
+ // =====================================================================
1526
+ function renderRadial(slide, p) {
1527
+ const pal = P('theme10_radial_v1', p);
1528
+ frame(slide, pal);
1529
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1530
+ const items = (Array.isArray(p.items) ? p.items : []).map((it, i) => ({ name: it?.name ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) })).slice(0, 8);
1531
+ const n = Math.max(items.length, 1);
1532
+ const maxV = items.length ? Math.max(...items.map((it) => it.value), 1) : 1;
1533
+ const xL = MX + 0.5, xR = MX + CW - 0.5, yT = y0 + 0.2, yB = BOT - 0.35;
1534
+ const cx = (xL + xR) / 2, cy = (yT + yB) / 2, Rmax = (Math.min(xR - xL, yB - yT) / 2) * 0.9, rIn = Rmax * 0.2;
1535
+ const step = 360 / n, gap = Math.min(6, step * 0.18);
1536
+ const colors = series(pal);
1537
+ items.forEach((it, i) => {
1538
+ const f = it.value / (maxV || 1);
1539
+ const rOut = rIn + Math.max(f, 0.04) * (Rmax - rIn);
1540
+ const a0 = 270 + i * step + gap / 2;
1541
+ const sweep = step - gap;
1542
+ slide.addShape('pie', { x: cx - rOut, y: cy - rOut, w: 2 * rOut, h: 2 * rOut, fill: { color: colors[i % 6] }, angleRange: [a0, sweep] });
1543
+ });
1544
+ slide.addShape('oval', { x: cx - rIn, y: cy - rIn, w: 2 * rIn, h: 2 * rIn, fill: { color: pal.bg } });
1545
+ items.forEach((it, i) => {
1546
+ const mid = ((270 + i * step + gap / 2 + (step - gap) / 2) * Math.PI) / 180;
1547
+ const lx = cx + (Rmax + 0.28) * Math.cos(mid), ly = cy + (Rmax + 0.28) * Math.sin(mid);
1548
+ txt(slide, it.name, { x: lx - 0.6, y: ly - 0.13, w: 1.2, h: 0.26, fontSize: 10, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1549
+ txt(slide, t10Fmt(it.value), { x: lx - 0.6, y: ly + 0.14, w: 1.2, h: 0.24, fontSize: 10, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
1550
+ });
1551
+ footer(slide, p.folioLeft, p.folioRight, pal);
1552
+ }
1553
+ // =====================================================================
1554
+ // 52. heat · 热力图(aurora,行×列矩阵)
1555
+ // =====================================================================
1556
+ function renderHeat(slide, p) {
1557
+ const pal = P('theme10_heat_v1', p);
1558
+ frame(slide, pal);
1559
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1560
+ const rows = t10NormCats(p.rows);
1561
+ const cols = t10NormCats(p.cols);
1562
+ const matrix = (p.values ?? '').split(';').map((r) => r.split(',').map((x) => Number(x.trim())).filter((nn) => isFinite(nn)));
1563
+ const nR = Math.max(rows.length, matrix.length, 1);
1564
+ const nC = Math.max(cols.length, matrix[0]?.length ?? 1, 1);
1565
+ const flat = matrix.flat();
1566
+ const minV = flat.length ? Math.min(...flat) : 0;
1567
+ const maxV = flat.length ? Math.max(...flat, 1) : 1;
1568
+ const xL = MX + 1.3, xR = MX + CW - 0.2, yT = y0 + 0.5, yB = BOT - 0.25;
1569
+ const cellW = (xR - xL) / nC, cellH = (yB - yT) / nR;
1570
+ cols.forEach((c, j) => { if (c)
1571
+ txt(slide, c, { x: xL + j * cellW, y: yT - 0.2, w: cellW, h: 0.24, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body }); });
1572
+ for (let i = 0; i < nR; i++) {
1573
+ if (rows[i])
1574
+ txt(slide, rows[i], { x: xL - 0.1, y: yT + i * cellH + cellH / 2 - 0.12, w: 1.05, h: 0.24, fontSize: 9, color: pal.ink2, align: 'right', fontFace: S.fonts.body });
1575
+ for (let j = 0; j < nC; j++) {
1576
+ const v = (matrix[i] ?? [])[j] ?? 0;
1577
+ const t = (v - minV) / (maxV - minV || 1);
1578
+ const hex = t10MixHex('#4a7fd4', '#d9b977', t);
1579
+ const dark = t10Lum(hex) > 0.6;
1580
+ slide.addShape('rect', { x: xL + j * cellW + 0.02, y: yT + i * cellH + 0.02, w: cellW - 0.04, h: cellH - 0.04, fill: { color: hex } });
1581
+ txt(slide, t10Fmt(v), { x: xL + j * cellW, y: yT + i * cellH + cellH / 2 - 0.12, w: cellW, h: 0.24, fontSize: 9, bold: true, color: dark ? '#05070b' : pal.ink, align: 'center', fontFace: S.fonts.mono });
1582
+ }
1583
+ }
1584
+ footer(slide, p.folioLeft, p.folioRight, pal);
1585
+ }
1586
+ // =====================================================================
1587
+ // 53. trend · 趋势图(aurora,面积+折线,PPTX 近似:折线+端点+末值)
1588
+ // =====================================================================
1589
+ function renderTrend(slide, p) {
1590
+ const pal = P('theme10_trend_v1', p);
1591
+ frame(slide, pal);
1592
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1593
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1594
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color }));
1595
+ const n = Math.max(...sers.map((s) => s.values.length), 1);
1596
+ const all = sers.flatMap((s) => s.values);
1597
+ const sc = t10Nice(all.length ? Math.min(...all, 0) : 0, all.length ? Math.max(...all, 1) : 1, 4);
1598
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1599
+ const xOf = (i) => x0 + (i / (n - 1 || 1)) * (x1 - x0);
1600
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1601
+ const colors = series(pal);
1602
+ sers.forEach((s, si) => {
1603
+ const col = s.color ?? colors[si];
1604
+ const pts = s.values.map((v, i) => [xOf(i), yOf(v)]);
1605
+ for (let i = 0; i < pts.length - 1; i++)
1606
+ t10Seg(slide, pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], col, 2.4);
1607
+ s.values.forEach((_v, i) => { const [cx, cy] = pts[i]; slide.addShape('oval', { x: cx - 0.06, y: cy - 0.06, w: 0.12, h: 0.12, fill: { color: col } }); });
1608
+ const last = pts[pts.length - 1];
1609
+ txt(slide, t10Fmt(s.values[s.values.length - 1] ?? 0), { x: last[0] + 0.06, y: last[1] - 0.22, w: 0.9, h: 0.24, fontSize: 9, bold: true, color: col, align: 'left', fontFace: S.fonts.mono });
1610
+ });
1611
+ t10NormCats(p.categories).forEach((c, i) => { if (c)
1612
+ txt(slide, c, { x: xOf(i) - 0.5, y: plotBot + 0.04, w: 1.0, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body }); });
1613
+ if (sers.length > 1)
1614
+ t10Legend(slide, pal, sers.map((s, i) => ({ name: s.name, color: s.color ?? colors[i] })), x0, y0 - 0.34);
1615
+ footer(slide, p.folioLeft, p.folioRight, pal);
1616
+ }
1617
+ // =====================================================================
1618
+ // 54. range · 区间图(aurora,浮动带 + 中枢线)
1619
+ // =====================================================================
1620
+ function renderRange(slide, p) {
1621
+ const pal = P('theme10_range_v1', p);
1622
+ frame(slide, pal);
1623
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1624
+ const x0 = MX + 1.0, x1 = MX + CW - 0.2, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1625
+ const items = (Array.isArray(p.items) ? p.items : []).map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, low: Number(it?.low ?? 0), high: Number(it?.high ?? 0), mid: it?.mid === undefined ? undefined : Number(it?.mid) }));
1626
+ const n = Math.max(items.length, 1);
1627
+ const lo = Math.min(0, ...items.map((x) => x.low), ...items.map((x) => x.mid ?? 0));
1628
+ const hi = Math.max(1, ...items.map((x) => x.high), ...items.map((x) => x.mid ?? 0));
1629
+ const sc = t10Nice(lo, hi, 4);
1630
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1631
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1632
+ const band = (x1 - x0) / n;
1633
+ const inner = Math.min(band * 0.5, 0.9);
1634
+ const colors = series(pal);
1635
+ items.forEach((it, i) => {
1636
+ const col = colors[i % 6];
1637
+ const cx = x0 + (i + 0.5) * band;
1638
+ const yH = yOf(it.high), yL = yOf(it.low), yM = yOf(it.mid ?? (it.low + it.high) / 2);
1639
+ slide.addShape('rect', { x: cx - inner / 2, y: yH, w: inner, h: Math.max(yL - yH, 0.02), fill: { color: col, transparency: 82 }, line: { color: col, width: 0.75, transparency: 50 } });
1640
+ slide.addShape('line', { x: cx - inner / 2 - 0.12, y: yM, w: inner + 0.24, h: 0, line: { color: pal.ink, width: 1.5 } });
1641
+ slide.addShape('oval', { x: cx - 0.05, y: yM - 0.05, w: 0.1, h: 0.1, fill: { color: pal.ink } });
1642
+ txt(slide, t10Fmt(it.high), { x: cx + inner / 2 + 0.05, y: yH - 0.12, w: 0.8, h: 0.24, fontSize: 9, bold: true, color: col, align: 'left', fontFace: S.fonts.mono });
1643
+ txt(slide, t10Fmt(it.low), { x: cx + inner / 2 + 0.05, y: yL - 0.12, w: 0.8, h: 0.24, fontSize: 9, bold: true, color: col, align: 'left', fontFace: S.fonts.mono });
1644
+ txt(slide, it.label, { x: cx - 0.6, y: plotBot + 0.04, w: 1.2, h: 0.24, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1645
+ });
1646
+ footer(slide, p.folioLeft, p.folioRight, pal);
1647
+ }
1648
+ // =====================================================================
1649
+ // 55. candlestick · K线图(obsidian,OHLC;红涨绿跌 A股惯例)
1650
+ // =====================================================================
1651
+ function renderCandlestick(slide, p) {
1652
+ const pal = P('theme10_candlestick_v1', p);
1653
+ frame(slide, pal);
1654
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1655
+ const x0 = MX + 0.95, x1 = MX + CW - 0.15, plotTop = y0 + 0.05, plotBot = BOT - 0.4;
1656
+ const items = (Array.isArray(p.items) ? p.items : []).map((it) => ({ open: Number(it?.open ?? 0), high: Number(it?.high ?? 0), low: Number(it?.low ?? 0), close: Number(it?.close ?? 0) }));
1657
+ const n = Math.max(items.length, 1);
1658
+ const all = items.flatMap((x) => [x.open, x.high, x.low, x.close]);
1659
+ const sc = t10Nice(all.length ? Math.min(...all) : 0, all.length ? Math.max(...all, 1) : 1, 4);
1660
+ t10Grid(slide, pal, x0, x1, plotTop, plotBot, sc);
1661
+ const yOf = (v) => plotBot - (v - sc.min) / (sc.max - sc.min || 1) * (plotBot - plotTop);
1662
+ const band = (x1 - x0) / n;
1663
+ const inner = Math.min(band * 0.56, 0.5);
1664
+ const cats = t10NormCats(p.categories);
1665
+ items.forEach((it, i) => {
1666
+ const cx = x0 + (i + 0.5) * band;
1667
+ const up = it.close >= it.open;
1668
+ const col = up ? pal.green : pal.red;
1669
+ const yO = yOf(it.open), yC = yOf(it.close), yTop = Math.min(yO, yC), bodyH = Math.max(Math.abs(yC - yO), 0.03);
1670
+ slide.addShape('line', { x: cx, y: yOf(it.high), w: 0, h: yOf(it.low) - yOf(it.high), line: { color: col, width: 1.4 } });
1671
+ slide.addShape('rect', { x: cx - inner / 2, y: yTop, w: inner, h: bodyH, fill: { color: col } });
1672
+ if (cats[i])
1673
+ txt(slide, cats[i], { x: cx - 0.6, y: plotBot + 0.04, w: 1.2, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
1674
+ });
1675
+ footer(slide, p.folioLeft, p.folioRight, pal);
1676
+ }
1677
+ // =====================================================================
1678
+ // 56. ridgeline · 山脊线(aurora,叠加密度曲线,PPTX 近似:折线)
1679
+ // =====================================================================
1680
+ function renderRidgeline(slide, p) {
1681
+ const pal = P('theme10_ridgeline_v1', p);
1682
+ frame(slide, pal);
1683
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1684
+ const x0 = MX + 1.1, x1 = MX + CW - 0.6, plotTop = y0 + 0.1, plotBot = BOT - 0.35;
1685
+ const sers = (Array.isArray(p.series) ? p.series : []).map((sr, i) => ({ name: sr?.name ?? `序列 ${i + 1}`, values: t10ParseVals(sr?.values), color: sr?.color }));
1686
+ const m = Math.max(...sers.map((s) => s.values.length), 1);
1687
+ const k = Math.max(sers.length, 1);
1688
+ const dx = (x1 - x0) / (m - 1 || 1);
1689
+ const stepY = (plotBot - plotTop) / k;
1690
+ const amp = stepY * 0.62;
1691
+ const colors = series(pal);
1692
+ sers.forEach((s, si) => {
1693
+ const col = s.color ?? colors[si];
1694
+ const baseY = plotTop + (si + 0.5) * stepY;
1695
+ const maxV = Math.max(1, ...s.values);
1696
+ const xs = s.values.map((_v, j) => x0 + j * dx);
1697
+ const ys = s.values.map((v) => baseY - (v / maxV) * amp);
1698
+ for (let i = 0; i < ys.length - 1; i++)
1699
+ t10Seg(slide, xs[i], ys[i], xs[i + 1], ys[i + 1], col, 2.2);
1700
+ txt(slide, s.name, { x: x1 + 0.08, y: baseY - 0.12, w: 0.9, h: 0.24, fontSize: 9, bold: true, color: col, align: 'left', fontFace: S.fonts.body });
1701
+ });
1702
+ t10NormCats(p.axis).forEach((a, j) => { if (a)
1703
+ txt(slide, a, { x: x0 + j * dx - 0.5, y: plotBot + 0.04, w: 1.0, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body }); });
1704
+ footer(slide, p.folioLeft, p.folioRight, pal);
1705
+ }
1706
+ // =====================================================================
1707
+ // 57. calendar · 日历热力(ember,GitHub 式网格)
1708
+ // =====================================================================
1709
+ function renderCalendar(slide, p) {
1710
+ const pal = P('theme10_calendar_v1', p);
1711
+ frame(slide, pal);
1712
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1713
+ const weeks = Math.max(1, (Number(p.weeks ?? 18) | 0));
1714
+ const flat = t10ParseVals(p.values);
1715
+ const cells = Array.from({ length: weeks * 7 }, (_u, i) => flat[i] ?? 0);
1716
+ const minV = cells.length ? Math.min(...cells) : 0;
1717
+ const maxV = cells.length ? Math.max(...cells, 1) : 1;
1718
+ const xL = MX + 1.3, xR = MX + CW - 0.2, yT = y0 + 0.5, yB = BOT - 0.45;
1719
+ const cellW = (xR - xL) / weeks, cellH = (yB - yT) / 7;
1720
+ for (let w = 0; w < weeks; w++) {
1721
+ for (let d = 0; d < 7; d++) {
1722
+ const v = cells[w * 7 + d] ?? 0;
1723
+ const t = (v - minV) / (maxV - minV || 1);
1724
+ const hex = t10MixHex('#c97a52', '#d9b977', t);
1725
+ slide.addShape('rect', { x: xL + w * cellW + 0.02, y: yT + d * cellH + 0.02, w: cellW - 0.04, h: cellH - 0.04, fill: { color: hex } });
1726
+ }
1727
+ }
1728
+ footer(slide, p.folioLeft, p.folioRight, pal);
1729
+ }
1730
+ // =====================================================================
1731
+ // 41. funnel · 漏斗图(aurora)
1732
+ // =====================================================================
1733
+ function renderFunnel(slide, p) {
1734
+ const pal = P('theme10_funnel_v1', p);
1735
+ frame(slide, pal);
1736
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1737
+ const items = (Array.isArray(p.items) ? p.items : [])
1738
+ .map((it, i) => ({ label: it?.label ?? `层 ${i + 1}`, value: Number(it?.value ?? 0) }));
1739
+ const n = Math.max(items.length, 1);
1740
+ const maxV = items.length ? Math.max(...items.map((it) => it.value), 1) : 1;
1741
+ const sumV = items.reduce((a, it) => a + it.value, 0) || 1;
1742
+ const cx = 5.0, xL = MX + 0.2, xR = MX + CW - 0.2, top = y0 + 0.12, bot = BOT - 0.3;
1743
+ const layerH = (bot - top) / n;
1744
+ const wOf = (v) => (v / maxV) * (xR - xL);
1745
+ const colors = series(pal);
1746
+ for (let i = 0; i < n; i++) {
1747
+ const it = items[i];
1748
+ const yTop = top + i * layerH, yBot = yTop + layerH - 0.06;
1749
+ const wTop = wOf(it.value), wBot = wOf(i < n - 1 ? items[i + 1].value : it.value);
1750
+ const wMid = Math.max((wTop + wBot) / 2, 0.1);
1751
+ const x = cx - wMid / 2;
1752
+ slide.addShape('rect', { x, y: yTop, w: wMid, h: yBot - yTop, fill: { color: colors[i % colors.length] } });
1753
+ const pct = ((it.value / sumV) * 100).toFixed(1);
1754
+ txt(slide, it.label, { x: cx - wMid / 2, y: yTop + (yBot - yTop) / 2 - 0.16, w: wMid, h: 0.26, fontSize: 11, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.body });
1755
+ txt(slide, String(it.value), { x: cx - wMid / 2, y: yTop + (yBot - yTop) / 2 + 0.04, w: wMid, h: 0.26, fontSize: 10, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
1756
+ txt(slide, pct + '%', { x: x + wMid + 0.1, y: yTop + (yBot - yTop) / 2 - 0.05, w: 0.9, h: 0.26, fontSize: 10, color: pal.ink2, align: 'left', fontFace: S.fonts.mono });
1757
+ }
1758
+ footer(slide, p.folioLeft, p.folioRight, pal);
1759
+ }
1760
+ // =====================================================================
1761
+ // 42. gauge · 仪表盘(ember)
1762
+ // =====================================================================
1763
+ function renderGauge(slide, p) {
1764
+ const pal = P('theme10_gauge_v1', p);
1765
+ frame(slide, pal);
1766
+ t10Head(slide, p, pal, { size: 26 });
1767
+ const v = Math.max(0, Math.min(100, Number(p.value) || 0));
1768
+ const boxW = 3.4, cx = 5.0, boxY = 1.6, boxX = cx - boxW / 2;
1769
+ const ccx = cx, ccy = boxY + boxW / 2, R = boxW / 2;
1770
+ const ang = (a) => ((180 + 1.8 * a) * Math.PI) / 180;
1771
+ const ex = (a) => ccx + (R - 0.18) * Math.cos(ang(a));
1772
+ const ey = (a) => ccy + (R - 0.18) * Math.sin(ang(a));
1773
+ slide.addShape('pie', { x: boxX, y: boxY, w: boxW, h: boxW, fill: { color: pal.border }, angleRange: [180, 180] });
1774
+ slide.addShape('pie', { x: boxX, y: boxY, w: boxW, h: boxW, fill: { color: pal.red }, angleRange: [180, 72] });
1775
+ slide.addShape('pie', { x: boxX, y: boxY, w: boxW, h: boxW, fill: { color: pal.goldAmber }, angleRange: [252, 54] });
1776
+ slide.addShape('pie', { x: boxX, y: boxY, w: boxW, h: boxW, fill: { color: pal.green }, angleRange: [306, 54] });
1777
+ t10Seg(slide, ccx, ccy, ex(v), ey(v), pal.ink, 2.5);
1778
+ slide.addShape('oval', { x: ccx - 0.09, y: ccy - 0.09, w: 0.18, h: 0.18, fill: { color: pal.ink }, line: { color: pal.gold, width: 1.5 } });
1779
+ txt(slide, `${v}${p.unit || '%'}`, { x: cx - 1.0, y: ccy - 0.72, w: 2.0, h: 0.6, fontSize: 30, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
1780
+ if (p.gaugeLabel)
1781
+ txt(slide, p.gaugeLabel, { x: cx - 1.2, y: ccy + 0.36, w: 2.4, h: 0.3, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
1782
+ footer(slide, p.folioLeft, p.folioRight, pal);
1783
+ }
1784
+ // =====================================================================
1785
+ // 43. bullet · 子弹图(aurora)
1786
+ // =====================================================================
1787
+ function renderBullet(slide, p) {
1788
+ const pal = P('theme10_bullet_v1', p);
1789
+ frame(slide, pal);
1790
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1791
+ const items = (Array.isArray(p.items) ? p.items : [])
1792
+ .map((it, i) => ({ label: it?.label ?? `指标 ${i + 1}`, value: Number(it?.value ?? 0), target: Number(it?.target ?? 0), range: Number(it?.range ?? 0) }));
1793
+ const n = Math.max(items.length, 1);
1794
+ const maxV = items.length ? Math.max(...items.flatMap((it) => [it.value, it.target, it.range].filter((x) => x > 0)), 1) : 1;
1795
+ const xL = MX + 1.6, xR = MX + CW - 0.2, top = y0 + 0.15, bot = BOT - 0.3;
1796
+ const rowH = (bot - top) / n;
1797
+ const xOf = (v) => xL + (v / maxV) * (xR - xL);
1798
+ for (let i = 0; i < n; i++) {
1799
+ const it = items[i], yc = top + i * rowH + rowH / 2;
1800
+ const trackH = rowH * 0.62, barH = rowH * 0.4;
1801
+ if (it.range > 0)
1802
+ slide.addShape('rect', { x: xL, y: yc - trackH / 2, w: Math.max(xOf(it.range) - xL, 0.02), h: trackH, fill: { color: pal.surfaceElevated } });
1803
+ slide.addShape('rect', { x: xL, y: yc - barH / 2, w: Math.max(xOf(it.value) - xL, 0.02), h: barH, fill: { color: pal.accent } });
1804
+ const tx = xOf(it.target);
1805
+ slide.addShape('line', { x: tx, y: yc - trackH / 2 - 0.02, w: 0, h: trackH + 0.04, line: { color: pal.gold, width: 2.5 } });
1806
+ txt(slide, it.label, { x: MX, y: yc - 0.13, w: 1.5, h: 0.26, fontSize: 10, color: pal.ink2, align: 'right', fontFace: S.fonts.body });
1807
+ txt(slide, String(it.value), { x: Math.min(xOf(it.value), xR) + 0.06, y: yc - 0.13, w: 1.0, h: 0.26, fontSize: 10, bold: true, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
1808
+ }
1809
+ footer(slide, p.folioLeft, p.folioRight, pal);
1810
+ }
1811
+ // =====================================================================
1812
+ // 44. box · 箱线图(obsidian)
1813
+ // =====================================================================
1814
+ function renderBox(slide, p) {
1815
+ const pal = P('theme10_box_v1', p);
1816
+ frame(slide, pal);
1817
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1818
+ const items = (Array.isArray(p.items) ? p.items : [])
1819
+ .map((it, i) => ({ label: it?.label ?? `组 ${i + 1}`, min: Number(it?.min ?? 0), q1: Number(it?.q1 ?? 0), median: Number(it?.median ?? 0), q3: Number(it?.q3 ?? 0), max: Number(it?.max ?? 0) }));
1820
+ const n = Math.max(items.length, 1);
1821
+ const allMin = items.length ? Math.min(...items.map((it) => it.min)) : 0;
1822
+ const allMax = items.length ? Math.max(...items.map((it) => it.max), 1) : 1;
1823
+ const sc = t10Nice(allMin, allMax, 4);
1824
+ const xL = MX + 1.6, xR = MX + CW - 0.2, top = y0 + 0.15, bot = BOT - 0.4;
1825
+ const xOf = (v) => xL + (v - sc.min) / (sc.max - sc.min || 1) * (xR - xL);
1826
+ for (const t of sc.ticks) {
1827
+ const x = xOf(t), zero = Math.abs(t) < 1e-9;
1828
+ slide.addShape('line', { x, y: top, w: 0, h: bot - top, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1 : 0.75 } });
1829
+ txt(slide, t10Fmt(t), { x: x - 0.4, y: bot + 0.04, w: 0.8, h: 0.24, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1830
+ }
1831
+ const rowH = (bot - top) / n;
1832
+ for (let i = 0; i < n; i++) {
1833
+ const it = items[i], yc = top + i * rowH + rowH / 2, bandH = rowH * 0.42;
1834
+ const xMin = xOf(it.min), xMax = xOf(it.max), xQ1 = xOf(it.q1), xQ3 = xOf(it.q3), xMed = xOf(it.median);
1835
+ slide.addShape('line', { x: xMin, y: yc, w: xMax - xMin, h: 0, line: { color: pal.ink2, width: 1.5 } });
1836
+ slide.addShape('line', { x: xMin, y: yc - 0.06, w: 0, h: 0.12, line: { color: pal.ink2, width: 1.5 } });
1837
+ slide.addShape('line', { x: xMax, y: yc - 0.06, w: 0, h: 0.12, line: { color: pal.ink2, width: 1.5 } });
1838
+ slide.addShape('rect', { x: Math.min(xQ1, xQ3), y: yc - bandH / 2, w: Math.max(Math.abs(xQ3 - xQ1), 0.02), h: bandH, fill: { color: pal.accent, transparency: 78 }, line: { color: pal.accent, width: 1.2 } });
1839
+ slide.addShape('line', { x: xMed, y: yc - bandH / 2, w: 0, h: bandH, line: { color: pal.gold, width: 2.5 } });
1840
+ txt(slide, it.label, { x: MX, y: yc - 0.13, w: 1.5, h: 0.26, fontSize: 10, color: pal.ink2, align: 'right', fontFace: S.fonts.body });
1841
+ }
1842
+ footer(slide, p.folioLeft, p.folioRight, pal);
1843
+ }
1844
+ function t10Squarify(items, X, Y, W, H) {
1845
+ const total = items.reduce((s, it) => s + Math.max(it.value, 0), 0) || 1;
1846
+ const totalArea = W * H;
1847
+ const data = items.map((it, idx) => ({ idx, area: Math.max(it.value, 0) / total * totalArea })).sort((a, b) => b.area - a.area);
1848
+ const out = new Array(items.length);
1849
+ let x = X, y = Y, w = W, h = H, i = 0, row = [];
1850
+ const sumA = (r) => r.reduce((s, a) => s + a.area, 0);
1851
+ const worst = (r, side) => {
1852
+ let mx = 0, mn = Infinity;
1853
+ for (const a of r) {
1854
+ mx = Math.max(mx, a.area);
1855
+ mn = Math.min(mn, a.area);
1856
+ }
1857
+ const s = sumA(r), s2 = s * s, side2 = side * side;
1858
+ return Math.max((side2 * mx) / s2, s2 / (side2 * mn));
1859
+ };
1860
+ const placeRow = (r) => {
1861
+ const s = sumA(r);
1862
+ if (w >= h) {
1863
+ const colW = s / h;
1864
+ let cy = y;
1865
+ for (const a of r) {
1866
+ const ih = a.area / colW;
1867
+ out[a.idx] = { x, y: cy, w: colW, h: ih };
1868
+ cy += ih;
1869
+ }
1870
+ x += colW;
1871
+ w -= colW;
1872
+ }
1873
+ else {
1874
+ const bandH = s / w;
1875
+ let cx = x;
1876
+ for (const a of r) {
1877
+ const iw = a.area / bandH;
1878
+ out[a.idx] = { x: cx, y, w: iw, h: bandH };
1879
+ cx += iw;
1880
+ }
1881
+ y += bandH;
1882
+ h -= bandH;
1883
+ }
1884
+ };
1885
+ while (i < data.length) {
1886
+ const next = [...row, data[i]];
1887
+ const side = Math.min(w, h);
1888
+ if (row.length === 0 || worst(next, side) <= worst(row, side)) {
1889
+ row = next;
1890
+ i++;
1891
+ continue;
1892
+ }
1893
+ placeRow(row);
1894
+ row = [];
1895
+ }
1896
+ if (row.length)
1897
+ placeRow(row);
1898
+ return out;
1899
+ }
1900
+ function renderTreemap(slide, p) {
1901
+ const pal = P('theme10_treemap_v1', p);
1902
+ frame(slide, pal);
1903
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1904
+ const items = (Array.isArray(p.items) ? p.items : [])
1905
+ .map((it, i) => ({ label: it?.label ?? `区块 ${i + 1}`, value: Number(it?.value ?? 0) }));
1906
+ const xL = MX + 0.1, xR = MX + CW - 0.1, top = y0 + 0.12, bot = BOT - 0.3;
1907
+ const rects = t10Squarify(items, xL, top, xR - xL, bot - top);
1908
+ const colors = series(pal);
1909
+ items.forEach((it, i) => {
1910
+ const r = rects[i];
1911
+ if (!r || r.w < 0.02 || r.h < 0.02)
1912
+ return;
1913
+ slide.addShape('rect', { x: r.x + 0.02, y: r.y + 0.02, w: Math.max(r.w - 0.04, 0.02), h: Math.max(r.h - 0.04, 0.02), fill: { color: colors[i % colors.length] }, line: { color: pal.ink, width: 0.75 } });
1914
+ if (r.w > 0.7 && r.h > 0.34) {
1915
+ txt(slide, it.label, { x: r.x + 0.1, y: r.y + 0.1, w: r.w - 0.2, h: 0.26, fontSize: 11, bold: true, color: pal.ink, align: 'left', fontFace: S.fonts.body });
1916
+ txt(slide, String(it.value), { x: r.x + 0.1, y: r.y + 0.34, w: r.w - 0.2, h: 0.26, fontSize: 10, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
1917
+ }
1918
+ });
1919
+ footer(slide, p.folioLeft, p.folioRight, pal);
1920
+ }
1921
+ // ────────────────────────────────────────────────────────────────
1922
+ // P2 数据图表 · 批次K(关系与构成进阶 5 版式)
1923
+ // sankey / dumbbell / histogram / slope / waffle
1924
+ // ────────────────────────────────────────────────────────────────
1925
+ function renderSankey(slide, p) {
1926
+ const pal = P('theme10_sankey_v1', p);
1927
+ frame(slide, pal);
1928
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1929
+ const links = (Array.isArray(p.links) ? p.links : [])
1930
+ .map((l) => ({ from: String(l?.from ?? ''), to: String(l?.to ?? ''), value: Number(l?.value ?? 0) }));
1931
+ const sources = [...new Set(links.map((l) => l.from))].filter(Boolean);
1932
+ const targets = [...new Set(links.map((l) => l.to))].filter(Boolean);
1933
+ const srcOut = {}, tgtIn = {};
1934
+ links.forEach((l) => { srcOut[l.from] = (srcOut[l.from] ?? 0) + l.value; tgtIn[l.to] = (tgtIn[l.to] ?? 0) + l.value; });
1935
+ const colors = series(pal);
1936
+ const srcColor = (k) => colors[sources.indexOf(k) % colors.length];
1937
+ const tgtColor = (k) => colors[(targets.indexOf(k) + 3) % colors.length];
1938
+ const leftX = MX + 0.1, rightX = MX + CW - 0.1;
1939
+ const top = y0 + 0.18, bot = BOT - 0.3;
1940
+ const H = bot - top, nodeW = 0.18;
1941
+ const srcScale = (H * 0.92) / (sources.reduce((a, k) => a + srcOut[k], 0) || 1);
1942
+ const tgtScale = (H * 0.92) / (targets.reduce((a, k) => a + tgtIn[k], 0) || 1);
1943
+ const srcY = {}, srcNodeH = {};
1944
+ let cy = top;
1945
+ const sgap = sources.length > 1 ? (H * 0.08) / (sources.length - 1) : 0;
1946
+ sources.forEach((k) => { const h = srcOut[k] * srcScale; srcNodeH[k] = h; srcY[k] = cy; cy += h + sgap; });
1947
+ const tgtY = {}, tgtNodeH = {};
1948
+ cy = top;
1949
+ const tgap = targets.length > 1 ? (H * 0.08) / (targets.length - 1) : 0;
1950
+ targets.forEach((k) => { const h = tgtIn[k] * tgtScale; tgtNodeH[k] = h; tgtY[k] = cy; cy += h + tgap; });
1951
+ const bandX0 = leftX + nodeW, bandX1 = rightX - nodeW;
1952
+ links.forEach((l) => {
1953
+ const ys = srcY[l.from], ye = tgtY[l.to];
1954
+ const my = (ys + ye) / 2;
1955
+ const thk = Math.max(0.05, ((l.value * srcScale) + (l.value * tgtScale)) * 0.5);
1956
+ slide.addShape('rect', { x: bandX0, y: my - thk / 2, w: bandX1 - bandX0, h: thk, fill: { color: srcColor(l.from), transparency: 58 } });
1957
+ });
1958
+ sources.forEach((k) => {
1959
+ slide.addShape('rect', { x: leftX, y: srcY[k], w: nodeW, h: Math.max(srcNodeH[k], 0.03), fill: { color: srcColor(k) } });
1960
+ txt(slide, k, { x: leftX - 1.45, y: srcY[k] + srcNodeH[k] / 2 - 0.13, w: 1.35, h: 0.26, fontSize: 10, color: pal.ink2, align: 'right', fontFace: S.fonts.body });
1961
+ });
1962
+ targets.forEach((k) => {
1963
+ slide.addShape('rect', { x: rightX - nodeW, y: tgtY[k], w: nodeW, h: Math.max(tgtNodeH[k], 0.03), fill: { color: tgtColor(k) } });
1964
+ txt(slide, k, { x: rightX + 0.05, y: tgtY[k] + tgtNodeH[k] / 2 - 0.13, w: 1.45, h: 0.26, fontSize: 10, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
1965
+ });
1966
+ footer(slide, p.folioLeft, p.folioRight, pal);
1967
+ }
1968
+ function renderDumbbell(slide, p) {
1969
+ const pal = P('theme10_dumbbell_v1', p);
1970
+ frame(slide, pal);
1971
+ const y0 = t10Head(slide, p, pal, { size: 26 });
1972
+ const items = (Array.isArray(p.items) ? p.items : [])
1973
+ .map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, start: Number(it?.start ?? 0), end: Number(it?.end ?? 0) }));
1974
+ const all = items.flatMap((it) => [it.start, it.end]);
1975
+ const min = all.length ? Math.min(...all, 0) : 0;
1976
+ const max = all.length ? Math.max(...all, 1) : 1;
1977
+ const sc = t10Nice(min, max, 5);
1978
+ const labelW = 1.5, x0 = MX + labelW, x1 = MX + CW;
1979
+ const top = y0 + 0.22, bot = BOT - 0.3;
1980
+ const xOf = (v) => x0 + (v - sc.min) / (sc.max - sc.min || 1) * (x1 - x0);
1981
+ const colors = series(pal), blue = colors[0], amber = colors[2];
1982
+ sc.ticks.forEach((t) => {
1983
+ const x = xOf(t), zero = Math.abs(t) < 1e-9;
1984
+ slide.addShape('line', { x, y: top - 0.1, w: 0, h: bot - top + 0.2, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1.4 : 0.75 } });
1985
+ txt(slide, t10Fmt(t), { x: x - 0.45, y: bot + 0.06, w: 0.9, h: 0.26, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1986
+ });
1987
+ const n = Math.max(items.length, 1), rowH = (bot - top) / n;
1988
+ items.forEach((it, i) => {
1989
+ const y = top + rowH * (i + 0.5);
1990
+ const xs = xOf(it.start), xe = xOf(it.end), up = it.end >= it.start;
1991
+ const col = up ? blue : amber;
1992
+ t10Seg(slide, xs, y, xe, y, col, 3);
1993
+ slide.addShape('oval', { x: xs - 0.09, y: y - 0.09, w: 0.18, h: 0.18, fill: { color: amber } });
1994
+ slide.addShape('oval', { x: xe - 0.09, y: y - 0.09, w: 0.18, h: 0.18, fill: { color: blue } });
1995
+ txt(slide, it.label, { x: MX, y: y - 0.13, w: labelW - 0.12, h: 0.26, fontSize: 11, color: pal.ink2, align: 'right', fontFace: S.fonts.body });
1996
+ txt(slide, t10Fmt(it.start), { x: xs - 0.6, y: y - 0.4, w: 0.6, h: 0.26, fontSize: 10, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
1997
+ txt(slide, t10Fmt(it.end), { x: xe - 0.6, y: y - 0.4, w: 0.6, h: 0.26, fontSize: 10, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
1998
+ });
1999
+ if (p.startLabel)
2000
+ txt(slide, '● ' + p.startLabel, { x: x0, y: top - 0.34, w: 2, h: 0.26, fontSize: 11, color: amber, align: 'left', fontFace: S.fonts.body });
2001
+ if (p.endLabel)
2002
+ txt(slide, '● ' + p.endLabel, { x: x1, y: top - 0.34, w: 2, h: 0.26, fontSize: 11, color: blue, align: 'right', fontFace: S.fonts.body });
2003
+ footer(slide, p.folioLeft, p.folioRight, pal);
2004
+ }
2005
+ function renderHistogram(slide, p) {
2006
+ const pal = P('theme10_histogram_v1', p);
2007
+ frame(slide, pal);
2008
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2009
+ const items = (Array.isArray(p.items) ? p.items : [])
2010
+ .map((it, i) => ({ label: it?.label ?? `箱 ${i + 1}`, value: Number(it?.value ?? 0) }));
2011
+ const max = items.length ? Math.max(...items.map((it) => it.value), 1) : 1;
2012
+ const sc = t10Nice(0, max, 4);
2013
+ const x0 = MX + 0.1, x1 = MX + CW - 0.1;
2014
+ const top = y0 + 0.2, bot = BOT - 0.4;
2015
+ t10Grid(slide, pal, x0, x1, top, bot, sc);
2016
+ const n = Math.max(items.length, 1), band = (x1 - x0) / n, bw = band * 0.86;
2017
+ const yOf = (v) => bot - (v - sc.min) / (sc.max - sc.min || 1) * (bot - top);
2018
+ const colors = series(pal);
2019
+ items.forEach((it, i) => {
2020
+ const bx = x0 + i * band + (band - bw) / 2;
2021
+ const topY = yOf(it.value), h = Math.max(bot - topY, 0.02);
2022
+ slide.addShape('rect', { x: bx, y: topY, w: bw, h, fill: { color: colors[i % colors.length], transparency: 12 } });
2023
+ txt(slide, t10Fmt(it.value), { x: bx, y: topY - 0.3, w: bw, h: 0.26, fontSize: 9, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
2024
+ txt(slide, it.label, { x: bx - band * 0.25, y: bot + 0.06, w: band * 1.5, h: 0.26, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
2025
+ });
2026
+ if (p.xLabel)
2027
+ txt(slide, p.xLabel, { x: (x0 + x1) / 2 - 1, y: bot + 0.34, w: 2, h: 0.26, fontSize: 10, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
2028
+ footer(slide, p.folioLeft, p.folioRight, pal);
2029
+ }
2030
+ function renderSlope(slide, p) {
2031
+ const pal = P('theme10_slope_v1', p);
2032
+ frame(slide, pal);
2033
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2034
+ const items = (Array.isArray(p.items) ? p.items : [])
2035
+ .map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, before: Number(it?.before ?? 0), after: Number(it?.after ?? 0) }));
2036
+ const all = items.flatMap((it) => [it.before, it.after]);
2037
+ const min = all.length ? Math.min(...all, 0) : 0;
2038
+ const max = all.length ? Math.max(...all, 1) : 1;
2039
+ const sc = t10Nice(min, max, 4);
2040
+ const leftX = MX + 1.7, rightX = MX + CW - 1.7;
2041
+ const top = y0 + 0.32, bot = BOT - 0.3;
2042
+ const yOf = (v) => bot - (v - sc.min) / (sc.max - sc.min || 1) * (bot - top);
2043
+ const colors = series(pal), blue = colors[0], amber = colors[2];
2044
+ for (const t of sc.ticks) {
2045
+ const y = yOf(t), zero = Math.abs(t) < 1e-9;
2046
+ slide.addShape('line', { x: leftX - 0.4, y, w: rightX - leftX + 0.8, h: 0, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1 : 0.75 } });
2047
+ txt(slide, t10Fmt(t), { x: leftX - 1.5, y: y - 0.13, w: 1.0, h: 0.26, fontSize: 9, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
2048
+ }
2049
+ slide.addShape('line', { x: leftX, y: top - 0.1, w: 0, h: bot - top + 0.2, line: { color: pal.borderStrong, width: 1.2 } });
2050
+ slide.addShape('line', { x: rightX, y: top - 0.1, w: 0, h: bot - top + 0.2, line: { color: pal.borderStrong, width: 1.2 } });
2051
+ const n = Math.max(items.length, 1), rowH = (bot - top) / n;
2052
+ items.forEach((it, i) => {
2053
+ const yb = yOf(it.before), ya = yOf(it.after), up = it.after >= it.before, col = up ? blue : amber;
2054
+ const my = top + rowH * (i + 0.5);
2055
+ t10Seg(slide, leftX, yb, rightX, ya, col, 2.6);
2056
+ slide.addShape('oval', { x: leftX - 0.08, y: yb - 0.08, w: 0.16, h: 0.16, fill: { color: col } });
2057
+ slide.addShape('oval', { x: rightX - 0.08, y: ya - 0.08, w: 0.16, h: 0.16, fill: { color: col } });
2058
+ txt(slide, it.label, { x: (leftX + rightX) / 2 - 1, y: my - 0.13, w: 2, h: 0.26, fontSize: 10, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
2059
+ txt(slide, t10Fmt(it.before), { x: leftX - 0.7, y: yb - 0.13, w: 0.6, h: 0.26, fontSize: 9, color: pal.ink, align: 'right', fontFace: S.fonts.mono });
2060
+ txt(slide, t10Fmt(it.after), { x: rightX + 0.1, y: ya - 0.13, w: 0.6, h: 0.26, fontSize: 9, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
2061
+ });
2062
+ if (p.beforeLabel)
2063
+ txt(slide, p.beforeLabel, { x: leftX - 1, y: top - 0.36, w: 2, h: 0.26, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
2064
+ if (p.afterLabel)
2065
+ txt(slide, p.afterLabel, { x: rightX - 1, y: top - 0.36, w: 2, h: 0.26, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
2066
+ footer(slide, p.folioLeft, p.folioRight, pal);
2067
+ }
2068
+ function renderWaffle(slide, p) {
2069
+ const pal = P('theme10_waffle_v1', p);
2070
+ frame(slide, pal);
2071
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2072
+ const items = (Array.isArray(p.items) ? p.items : [])
2073
+ .map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) }));
2074
+ const total = items.reduce((a, it) => a + it.value, 0) || 1;
2075
+ let counts = items.map((it) => Math.round((it.value / total) * 100));
2076
+ const diff = 100 - counts.reduce((a, b) => a + b, 0);
2077
+ if (diff !== 0 && counts.length) {
2078
+ const mi = counts.indexOf(Math.max(...counts));
2079
+ counts[mi] += diff;
2080
+ }
2081
+ const colors = series(pal);
2082
+ const cols = 10, rows = 10, cell = 0.22, gap = 0.03;
2083
+ const ox = MX + 0.2, oy = y0 + 0.25;
2084
+ let filled = 0, ci = 0;
2085
+ for (let r = 0; r < rows; r++) {
2086
+ for (let c = 0; c < cols; c++) {
2087
+ let color, empty = false;
2088
+ if (ci < items.length && filled < counts[ci]) {
2089
+ color = colors[ci % colors.length];
2090
+ filled++;
2091
+ if (filled >= counts[ci]) {
2092
+ ci++;
2093
+ filled = 0;
2094
+ }
2095
+ }
2096
+ else {
2097
+ color = pal.ink3;
2098
+ empty = true;
2099
+ }
2100
+ slide.addShape('rect', { x: ox + c * (cell + gap), y: oy + r * (cell + gap), w: cell, h: cell, fill: { color, transparency: empty ? 88 : 8 } });
2101
+ }
2102
+ }
2103
+ const lx = ox + cols * cell + (cols - 1) * gap + 0.45;
2104
+ let ly = oy + 0.08;
2105
+ items.forEach((it, i) => {
2106
+ slide.addShape('rect', { x: lx, y: ly + 0.02, w: 0.16, h: 0.16, fill: { color: colors[i % colors.length] } });
2107
+ txt(slide, `${it.label} ${Math.round((it.value / total) * 100)}%`, { x: lx + 0.26, y: ly - 0.04, w: 2.6, h: 0.26, fontSize: 11, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
2108
+ ly += 0.44;
2109
+ });
2110
+ footer(slide, p.folioLeft, p.folioRight, pal);
2111
+ }
2112
+ // ────────────────────────────────────────────────────────────────
2113
+ // P2 数据图表 · 批次L(时序与分布 5 版式)
2114
+ // gantt / bump / rose / dotplot / timeline
2115
+ // ────────────────────────────────────────────────────────────────
2116
+ function renderGantt(slide, p) {
2117
+ const pal = P('theme10_gantt_v1', p);
2118
+ frame(slide, pal);
2119
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2120
+ const tasks = (Array.isArray(p.tasks) ? p.tasks : [])
2121
+ .map((t, i) => ({ name: t?.name ?? `任务 ${i + 1}`, start: Number(t?.start ?? 0), end: Number(t?.end ?? 0) }));
2122
+ const maxWeek = Math.max(1, ...tasks.map((t) => t.end), ...tasks.map((t) => t.start));
2123
+ const labelW = 1.5, x0 = MX + labelW, x1 = MX + CW - 0.1;
2124
+ const top = y0 + 0.2, bot = BOT - 0.3;
2125
+ const xOf = (v) => x0 + (v / (maxWeek || 1)) * (x1 - x0);
2126
+ const colors = series(pal);
2127
+ for (let w = 0; w <= maxWeek; w++) {
2128
+ const x = xOf(w);
2129
+ slide.addShape('line', { x, y: top - 0.1, w: 0, h: bot - top + 0.2, line: { color: w === 0 ? pal.borderStrong : pal.border, width: w === 0 ? 1.4 : 0.75 } });
2130
+ txt(slide, 'W' + w, { x: x - 0.3, y: bot + 0.04, w: 0.6, h: 0.26, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
2131
+ }
2132
+ const n = Math.max(tasks.length, 1), rowH = (bot - top) / n;
2133
+ tasks.forEach((t, i) => {
2134
+ const y = top + rowH * (i + 0.5);
2135
+ const xs = xOf(t.start), xe = xOf(t.end), bw = Math.max(xe - xs, 0.06);
2136
+ slide.addShape('rect', { x: xs, y: y - rowH * 0.3, w: bw, h: rowH * 0.6, fill: { color: colors[i % colors.length] } });
2137
+ txt(slide, t.name, { x: MX, y: y - 0.13, w: labelW - 0.12, h: 0.26, fontSize: 11, color: pal.ink2, align: 'right', fontFace: S.fonts.body });
2138
+ txt(slide, String(t.end - t.start) + 'w', { x: xe + 0.08, y: y - 0.13, w: 0.8, h: 0.26, fontSize: 9, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
2139
+ });
2140
+ footer(slide, p.folioLeft, p.folioRight, pal);
2141
+ }
2142
+ function renderBump(slide, p) {
2143
+ const pal = P('theme10_bump_v1', p);
2144
+ frame(slide, pal);
2145
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2146
+ const labels = (Array.isArray(p.periodLabels) ? p.periodLabels : []).map(String);
2147
+ const items = (Array.isArray(p.items) ? p.items : [])
2148
+ .map((it, i) => ({ name: it?.name ?? `项 ${i + 1}`, ranks: [Number(it?.r1 ?? 0), Number(it?.r2 ?? 0), Number(it?.r3 ?? 0), Number(it?.r4 ?? 0)] }));
2149
+ const cols = 4;
2150
+ const allR = items.flatMap((it) => it.ranks).filter((r) => r > 0);
2151
+ const maxRank = Math.max(1, ...allR);
2152
+ const x0 = MX + 0.7, x1 = MX + CW - 0.7, top = y0 + 0.25, bot = BOT - 0.3;
2153
+ const colX = (c) => x0 + (c / (cols - 1)) * (x1 - x0);
2154
+ const yOf = (r) => top + ((r - 1) / (maxRank - 1 || 1)) * (bot - top);
2155
+ const colors = series(pal);
2156
+ for (let r = 1; r <= maxRank; r++) {
2157
+ const y = yOf(r);
2158
+ slide.addShape('line', { x: x0, y, w: x1 - x0, h: 0, line: { color: r === 1 ? pal.borderStrong : pal.border, width: r === 1 ? 1.3 : 0.75 } });
2159
+ txt(slide, '#' + r, { x: MX - 0.05, y: y - 0.13, w: 0.6, h: 0.26, fontSize: 9, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
2160
+ }
2161
+ labels.forEach((lab, c) => txt(slide, lab, { x: colX(c) - 0.6, y: top - 0.34, w: 1.2, h: 0.26, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.body }));
2162
+ items.forEach((it, i) => {
2163
+ const col = colors[i % colors.length];
2164
+ const pts = it.ranks.map((r, c) => ({ x: colX(c), y: yOf(r) }));
2165
+ for (let c = 0; c < pts.length - 1; c++)
2166
+ t10Seg(slide, pts[c].x, pts[c].y, pts[c + 1].x, pts[c + 1].y, col, 3);
2167
+ pts.forEach((pt) => { slide.addShape('oval', { x: pt.x - 0.09, y: pt.y - 0.09, w: 0.18, h: 0.18, fill: { color: col }, line: { color: pal.ink, width: 1 } }); });
2168
+ txt(slide, it.name, { x: x1 + 0.1, y: pts[pts.length - 1].y - 0.13, w: 1.4, h: 0.26, fontSize: 10, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
2169
+ });
2170
+ footer(slide, p.folioLeft, p.folioRight, pal);
2171
+ }
2172
+ function renderRose(slide, p) {
2173
+ const pal = P('theme10_rose_v1', p);
2174
+ frame(slide, pal);
2175
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2176
+ const items = (Array.isArray(p.items) ? p.items : [])
2177
+ .map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) }));
2178
+ const n = Math.max(items.length, 1);
2179
+ const maxV = Math.max(1, ...items.map((it) => it.value));
2180
+ const cx = 5, cy = (y0 + BOT) / 2 + 0.2, rMax = 1.75;
2181
+ const sweep = 360 / n;
2182
+ const colors = series(pal);
2183
+ items.forEach((it, i) => {
2184
+ const r = Math.max(rMax * Math.sqrt(it.value / maxV), 0.18);
2185
+ const a0 = i * sweep;
2186
+ slide.addShape('pie', { x: cx - r, y: cy - r, w: r * 2, h: r * 2, fill: { color: colors[i % colors.length] }, angleRange: [a0, sweep], line: { color: pal.bg, width: 1.5 } });
2187
+ });
2188
+ items.forEach((it, i) => {
2189
+ const a = i * sweep + sweep / 2;
2190
+ const lx = cx + (rMax + 0.25) * Math.sin((a * Math.PI) / 180);
2191
+ const ly = cy - (rMax + 0.25) * Math.cos((a * Math.PI) / 180);
2192
+ txt(slide, `${it.label} ${t10Fmt(it.value)}`, { x: lx - 1, y: ly - 0.13, w: 2, h: 0.26, fontSize: 10, color: pal.ink2, align: (a > 90 && a < 270) ? 'right' : 'left', fontFace: S.fonts.body });
2193
+ });
2194
+ footer(slide, p.folioLeft, p.folioRight, pal);
2195
+ }
2196
+ function renderDotplot(slide, p) {
2197
+ const pal = P('theme10_dotplot_v1', p);
2198
+ frame(slide, pal);
2199
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2200
+ const items = (Array.isArray(p.items) ? p.items : [])
2201
+ .map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) }));
2202
+ const all = items.map((it) => it.value);
2203
+ const min = all.length ? Math.min(...all, 0) : 0;
2204
+ const max = all.length ? Math.max(...all, 1) : 1;
2205
+ const sc = t10Nice(min, max, 5);
2206
+ const labelW = 1.3, x0 = MX + labelW, x1 = MX + CW - 0.1;
2207
+ const top = y0 + 0.2, bot = BOT - 0.3;
2208
+ const xOf = (v) => x0 + (v - sc.min) / (sc.max - sc.min || 1) * (x1 - x0);
2209
+ const colors = series(pal);
2210
+ for (const t of sc.ticks) {
2211
+ const x = xOf(t), zero = Math.abs(t) < 1e-9;
2212
+ slide.addShape('line', { x, y: top - 0.1, w: 0, h: bot - top + 0.2, line: { color: zero ? pal.borderStrong : pal.border, width: zero ? 1.4 : 0.75 } });
2213
+ txt(slide, t10Fmt(t), { x: x - 0.45, y: bot + 0.04, w: 0.9, h: 0.26, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
2214
+ }
2215
+ const n = Math.max(items.length, 1), rowH = (bot - top) / n;
2216
+ items.forEach((it, i) => {
2217
+ const y = top + rowH * (i + 0.5), x = xOf(it.value);
2218
+ slide.addShape('line', { x: x0, y, w: x - x0, h: 0, line: { color: pal.border, width: 0.75, dashType: 'dash' } });
2219
+ slide.addShape('oval', { x: x - 0.11, y: y - 0.11, w: 0.22, h: 0.22, fill: { color: colors[i % colors.length] }, line: { color: pal.ink, width: 1 } });
2220
+ txt(slide, it.label, { x: MX, y: y - 0.13, w: labelW - 0.12, h: 0.26, fontSize: 11, color: pal.ink2, align: 'right', fontFace: S.fonts.body });
2221
+ txt(slide, t10Fmt(it.value), { x: x + 0.16, y: y - 0.13, w: 0.9, h: 0.26, fontSize: 10, color: pal.ink, align: 'left', fontFace: S.fonts.mono });
2222
+ });
2223
+ footer(slide, p.folioLeft, p.folioRight, pal);
2224
+ }
2225
+ function renderTimeline(slide, p) {
2226
+ const pal = P('theme10_timeline_v1', p);
2227
+ frame(slide, pal);
2228
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2229
+ const events = (Array.isArray(p.events) ? p.events : [])
2230
+ .map((ev) => ({ year: String(ev?.year ?? ''), text: String(ev?.text ?? '') }));
2231
+ const n = Math.max(events.length, 1);
2232
+ const x0 = MX + 0.7, x1 = MX + CW - 0.7;
2233
+ const axisY = (y0 + BOT) / 2 + 0.25;
2234
+ const xOf = (i) => (n === 1 ? (x0 + x1) / 2 : x0 + (i / (n - 1)) * (x1 - x0));
2235
+ slide.addShape('line', { x: x0, y: axisY, w: x1 - x0, h: 0, line: { color: pal.gold, width: 2.4 } });
2236
+ events.forEach((ev, i) => {
2237
+ const x = xOf(i), up = i % 2 === 0;
2238
+ const ty = up ? axisY - 1.0 : axisY + 0.55;
2239
+ const my = up ? axisY - 0.32 : axisY + 0.32;
2240
+ slide.addShape('line', { x, y: up ? axisY - 0.32 : axisY, w: 0, h: 0.32, line: { color: pal.gold, width: 1.2 } });
2241
+ slide.addShape('oval', { x: x - 0.11, y: axisY - 0.11, w: 0.22, h: 0.22, fill: { color: pal.gold }, line: { color: pal.ink, width: 1.4 } });
2242
+ txt(slide, ev.year, { x: x - 0.8, y: ty, w: 1.6, h: 0.3, fontSize: 13, bold: true, color: pal.gold, align: 'center', fontFace: S.fonts.mono });
2243
+ txt(slide, ev.text, { x: x - 1.2, y: my + (up ? -0.28 : 0.05), w: 2.4, h: 0.5, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
2244
+ });
2245
+ footer(slide, p.folioLeft, p.folioRight, pal);
2246
+ }
2247
+ // ────────────────────────────────────────────────────────────────
2248
+ // P2 数据图表 · 批次M(结构与关系 5 版式)
2249
+ // orgchart / parallel / circlepack / cscatter / marimekko
2250
+ // ────────────────────────────────────────────────────────────────
2251
+ function renderOrgChart(slide, p) {
2252
+ const pal = P('theme10_orgchart_v1', p);
2253
+ frame(slide, pal);
2254
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2255
+ const kids = (Array.isArray(p.children) ? p.children : [])
2256
+ .map((c) => ({ name: c?.name ?? '', role: String(c?.role ?? '') }));
2257
+ const n = Math.max(kids.length, 1);
2258
+ const rw = 2.3, rh = 0.78, rx = 5 - rw / 2, ry = y0 + 0.2;
2259
+ const cw = 1.78, ch = 0.84, gap = 0.28;
2260
+ const totW = n * cw + (n - 1) * gap;
2261
+ const startX = 5 - totW / 2;
2262
+ const cy = y0 + 2.0;
2263
+ const rootBottom = ry + rh, cx = 5, midY = rootBottom + (cy - rootBottom) / 2;
2264
+ const colors = series(pal);
2265
+ kids.forEach((k, i) => {
2266
+ const kx = startX + i * (cw + gap), kMidX = kx + cw / 2, kTop = cy;
2267
+ slide.addShape('line', { x: cx, y: rootBottom, w: 0, h: midY - rootBottom, line: { color: pal.borderStrong, width: 1.4 } });
2268
+ slide.addShape('line', { x: Math.min(cx, kMidX), y: midY, w: Math.abs(kMidX - cx), h: 0, line: { color: pal.borderStrong, width: 1.4 } });
2269
+ slide.addShape('line', { x: kMidX, y: midY, w: 0, h: kTop - midY, line: { color: pal.borderStrong, width: 1.4 } });
2270
+ slide.addShape('rect', { x: kx, y: kTop, w: cw, h: ch, fill: { color: colors[i % colors.length], transparency: 84 }, line: { color: colors[i % colors.length], width: 1.4 } });
2271
+ txt(slide, k.name, { x: kx + 0.05, y: kTop + 0.16, w: cw - 0.1, h: 0.3, fontSize: 12, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.body });
2272
+ if (k.role)
2273
+ txt(slide, k.role, { x: kx + 0.05, y: kTop + 0.46, w: cw - 0.1, h: 0.3, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
2274
+ });
2275
+ slide.addShape('rect', { x: rx, y: ry, w: rw, h: rh, fill: { color: pal.accent, transparency: 78 }, line: { color: pal.accent, width: 2 } });
2276
+ txt(slide, String(p.rootName ?? '根'), { x: rx + 0.05, y: ry + 0.18, w: rw - 0.1, h: 0.3, fontSize: 13, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.body });
2277
+ if (p.rootRole)
2278
+ txt(slide, String(p.rootRole), { x: rx + 0.05, y: ry + 0.46, w: rw - 0.1, h: 0.3, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
2279
+ footer(slide, p.folioLeft, p.folioRight, pal);
2280
+ }
2281
+ function renderParallel(slide, p) {
2282
+ const pal = P('theme10_parallel_v1', p);
2283
+ frame(slide, pal);
2284
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2285
+ const ax = (Array.isArray(p.axes) ? p.axes : []).map(String);
2286
+ const m = Math.max(ax.length, 1);
2287
+ const items = (Array.isArray(p.items) ? p.items : [])
2288
+ .map((it, i) => ({ name: it?.name ?? `项 ${i + 1}`, vals: [Number(it?.v1 ?? 0), Number(it?.v2 ?? 0), Number(it?.v3 ?? 0)].slice(0, m) }));
2289
+ const x0 = MX + 0.8, x1 = MX + CW - 0.8, top = y0 + 0.3, bot = BOT - 0.3;
2290
+ const axisX = (c) => (m === 1 ? (x0 + x1) / 2 : x0 + (c / (m - 1)) * (x1 - x0));
2291
+ const colors = series(pal);
2292
+ for (let c = 0; c < m; c++) {
2293
+ slide.addShape('line', { x: axisX(c), y: top, w: 0, h: bot - top, line: { color: pal.borderStrong, width: 1.4 } });
2294
+ txt(slide, ax[c] ?? `轴${c + 1}`, { x: axisX(c) - 0.8, y: top - 0.34, w: 1.6, h: 0.26, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
2295
+ }
2296
+ items.forEach((it, i) => {
2297
+ const col = colors[i % colors.length];
2298
+ const pts = it.vals.map((v, c) => {
2299
+ const col2 = items.map((q) => q.vals[c]);
2300
+ const min = Math.min(...col2, v), max = Math.max(...col2, v), span = max - min || 1;
2301
+ return { x: axisX(c), y: bot - ((v - min) / span) * (bot - top) };
2302
+ });
2303
+ for (let c = 0; c < pts.length - 1; c++)
2304
+ t10Seg(slide, pts[c].x, pts[c].y, pts[c + 1].x, pts[c + 1].y, col, 2.6);
2305
+ pts.forEach((pt) => { slide.addShape('oval', { x: pt.x - 0.08, y: pt.y - 0.08, w: 0.16, h: 0.16, fill: { color: col }, line: { color: pal.ink, width: 1 } }); });
2306
+ txt(slide, it.name, { x: pts[pts.length - 1].x + 0.12, y: pts[pts.length - 1].y - 0.13, w: 1.4, h: 0.26, fontSize: 10, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
2307
+ });
2308
+ footer(slide, p.folioLeft, p.folioRight, pal);
2309
+ }
2310
+ function renderCirclepack(slide, p) {
2311
+ const pal = P('theme10_circlepack_v1', p);
2312
+ frame(slide, pal);
2313
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2314
+ const items = (Array.isArray(p.items) ? p.items : [])
2315
+ .map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, value: Number(it?.value ?? 0) }))
2316
+ .sort((a, b) => b.value - a.value);
2317
+ const cx = 5, cy = (y0 + BOT) / 2 + 0.2, rMax = 1.55;
2318
+ const total = items.reduce((a, it) => a + Math.sqrt(it.value), 0) || 1;
2319
+ const colors = series(pal);
2320
+ items.forEach((it, i) => {
2321
+ const rr = Math.max((Math.sqrt(it.value) / total) * rMax, 0.18);
2322
+ const ang = (i / Math.max(items.length, 1)) * Math.PI * 2;
2323
+ const ring = items.length > 1 ? rMax * 0.6 + rr : 0;
2324
+ const px = i === 0 ? cx : cx + ring * Math.cos(ang);
2325
+ const py = i === 0 ? cy : cy + ring * Math.sin(ang);
2326
+ slide.addShape('oval', { x: px - rr, y: py - rr, w: rr * 2, h: rr * 2, fill: { color: colors[i % colors.length], transparency: 8 }, line: { color: pal.ink, width: 1.2 } });
2327
+ if (rr > 0.28) {
2328
+ txt(slide, it.label, { x: px - rr, y: py - 0.14, w: rr * 2, h: 0.28, fontSize: 10, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.body });
2329
+ txt(slide, t10Fmt(it.value), { x: px - rr, y: py + 0.16, w: rr * 2, h: 0.28, fontSize: 9, color: pal.ink2, align: 'center', fontFace: S.fonts.mono });
2330
+ }
2331
+ });
2332
+ footer(slide, p.folioLeft, p.folioRight, pal);
2333
+ }
2334
+ function renderCscatter(slide, p) {
2335
+ const pal = P('theme10_cscatter_v1', p);
2336
+ frame(slide, pal);
2337
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2338
+ const pts = (Array.isArray(p.points) ? p.points : [])
2339
+ .map((q, i) => ({ x: Number(q?.x ?? 0), y: Number(q?.y ?? 0), label: String(q?.label ?? `点 ${i + 1}`) }));
2340
+ const xs = pts.map((q) => q.x), ys = pts.map((q) => q.y);
2341
+ const scx = t10Nice(Math.min(...xs, 0), Math.max(...xs, 1), 4);
2342
+ const scy = t10Nice(Math.min(...ys, 0), Math.max(...ys, 1), 4);
2343
+ const x0 = MX + 0.8, x1 = MX + CW - 0.3, top = y0 + 0.2, bot = BOT - 0.4;
2344
+ const xOf = (v) => x0 + (v - scx.min) / (scx.max - scx.min || 1) * (x1 - x0);
2345
+ const yOf = (v) => bot - (v - scy.min) / (scy.max - scy.min || 1) * (bot - top);
2346
+ const colors = series(pal);
2347
+ for (const t of scx.ticks) {
2348
+ const x = xOf(t);
2349
+ slide.addShape('line', { x, y: top, w: 0, h: bot - top, line: { color: t === 0 ? pal.borderStrong : pal.border, width: t === 0 ? 1.4 : 0.75 } });
2350
+ txt(slide, t10Fmt(t), { x: x - 0.4, y: bot + 0.04, w: 0.8, h: 0.26, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
2351
+ }
2352
+ for (const t of scy.ticks) {
2353
+ const y = yOf(t);
2354
+ slide.addShape('line', { x: x0, y, w: x1 - x0, h: 0, line: { color: t === 0 ? pal.borderStrong : pal.border, width: t === 0 ? 1.4 : 0.75 } });
2355
+ txt(slide, t10Fmt(t), { x: x0 - 0.45, y: y - 0.13, w: 0.4, h: 0.26, fontSize: 9, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
2356
+ }
2357
+ for (let i = 0; i < pts.length - 1; i++)
2358
+ t10Seg(slide, xOf(pts[i].x), yOf(pts[i].y), xOf(pts[i + 1].x), yOf(pts[i + 1].y), pal.accent, 2.4);
2359
+ pts.forEach((q, i) => {
2360
+ const x = xOf(q.x), y = yOf(q.y);
2361
+ slide.addShape('oval', { x: x - 0.11, y: y - 0.11, w: 0.22, h: 0.22, fill: { color: colors[i % colors.length] }, line: { color: pal.ink, width: 1.4 } });
2362
+ if (q.label)
2363
+ txt(slide, q.label, { x: x + 0.16, y: y - 0.13, w: 1.6, h: 0.26, fontSize: 10, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
2364
+ });
2365
+ footer(slide, p.folioLeft, p.folioRight, pal);
2366
+ }
2367
+ function renderMarimekko(slide, p) {
2368
+ const pal = P('theme10_marimekko_v1', p);
2369
+ frame(slide, pal);
2370
+ const y0 = t10Head(slide, p, pal, { size: 26 });
2371
+ const items = (Array.isArray(p.items) ? p.items : [])
2372
+ .map((it, i) => ({ label: it?.label ?? `项 ${i + 1}`, share: Number(it?.share ?? 0), value: Number(it?.value ?? 0) }));
2373
+ const totalShare = items.reduce((a, it) => a + it.share, 0) || 1;
2374
+ const maxV = Math.max(1, ...items.map((it) => it.value));
2375
+ const x0 = MX + 0.1, x1 = MX + CW - 0.1, top = y0 + 0.2, bot = BOT - 0.3;
2376
+ const plotH = bot - top;
2377
+ const colors = series(pal);
2378
+ let cx = x0;
2379
+ items.forEach((it, i) => {
2380
+ const w = (it.share / totalShare) * (x1 - x0);
2381
+ const h = (it.value / maxV) * plotH;
2382
+ const by = bot - h;
2383
+ slide.addShape('rect', { x: cx + 0.015, y: by, w: Math.max(w - 0.03, 0.02), h, fill: { color: colors[i % colors.length], transparency: 8 }, line: { color: pal.ink, width: 1 } });
2384
+ if (w > 0.7 && h > 0.4) {
2385
+ txt(slide, it.label, { x: cx + 0.05, y: by + 0.06, w: w - 0.1, h: 0.3, fontSize: 11, bold: true, color: pal.ink, align: 'left', fontFace: S.fonts.body });
2386
+ txt(slide, t10Fmt(it.value), { x: cx + 0.05, y: by + 0.36, w: w - 0.1, h: 0.3, fontSize: 10, color: pal.ink2, align: 'left', fontFace: S.fonts.mono });
2387
+ }
2388
+ txt(slide, `${it.label} ${Math.round((it.share / totalShare) * 100)}%`, { x: cx, y: bot + 0.04, w: w, h: 0.26, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
2389
+ cx += w;
2390
+ });
2391
+ footer(slide, p.folioLeft, p.folioRight, pal);
2392
+ }
2393
+ // ════════════════════════════════════════════════════════════════
2394
+ // P3 结构 / 流程 / 长尾(11 版式)
2395
+ // ════════════════════════════════════════════════════════════════
2396
+ function t10Arr(v) {
2397
+ return Array.isArray(v) ? v : [];
2398
+ }
2399
+ function renderSteps(slide, p) {
2400
+ const pal = P('theme10_steps_v1', p);
2401
+ frame(slide, pal);
2402
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2403
+ const items = t10Arr(p.items).slice(0, 5);
2404
+ const n = Math.max(1, items.length);
2405
+ const gx = 0.35;
2406
+ const cw = (CW - (n - 1) * gx) / n;
2407
+ items.forEach((it, i) => {
2408
+ const x = MX + i * (cw + gx);
2409
+ slide.addShape('rect', { x, y: y0, w: cw, h: 0.04, fill: { color: pal.gold } });
2410
+ txt(slide, it.no ?? String(i + 1).padStart(2, '0'), { x, y: y0 + 0.12, w: cw, h: 0.4, fontSize: 24, color: pal.accent, bold: true, fontFace: S.fonts.mono });
2411
+ txt(slide, it.name ?? '', { x, y: y0 + 0.58, w: cw, h: 0.32, fontSize: 16, color: pal.ink, bold: true, fontFace: S.fonts.heading });
2412
+ txt(slide, it.desc ?? '', { x, y: y0 + 0.94, w: cw, h: 1.1, fontSize: 11, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
2413
+ if (i < n - 1)
2414
+ txt(slide, '→', { x: x + cw + 0.02, y: y0 + 0.2, w: gx, h: 0.4, fontSize: 18, color: pal.gold, align: 'center', valign: 'middle' });
2415
+ });
2416
+ }
2417
+ function renderCycle(slide, p) {
2418
+ const pal = P('theme10_cycle_v1', p);
2419
+ frame(slide, pal);
2420
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2421
+ const items = t10Arr(p.items).slice(0, 6);
2422
+ const n = Math.max(1, items.length);
2423
+ const cols = n <= 3 ? n : 3;
2424
+ const rows = Math.ceil(n / cols);
2425
+ const gx = 0.4, gy = 0.5;
2426
+ const cw = (CW - (cols - 1) * gx) / cols;
2427
+ const ch = (BOT - y0 - 0.3 - (rows - 1) * gy) / rows;
2428
+ items.forEach((it, i) => {
2429
+ const c = i % cols, r = Math.floor(i / cols);
2430
+ const x = MX + c * (cw + gx), y = y0 + 0.3 + r * (ch + gy);
2431
+ card(slide, x, y, cw, ch, pal);
2432
+ slide.addShape('oval', { x: x + 0.18, y: y + 0.18, w: 0.5, h: 0.5, fill: { color: pal.surface }, line: { color: pal.gold, width: 1 } });
2433
+ txt(slide, String(i + 1).padStart(2, '0'), { x: x + 0.18, y: y + 0.18, w: 0.5, h: 0.5, align: 'center', valign: 'middle', fontSize: 14, bold: true, color: pal.accent, fontFace: S.fonts.mono });
2434
+ txt(slide, it.name ?? '', { x: x + 0.8, y: y + 0.2, w: cw - 0.95, h: 0.4, fontSize: 15, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2435
+ txt(slide, it.desc ?? '', { x: x + 0.2, y: y + 0.78, w: cw - 0.4, h: ch - 0.9, fontSize: 10, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
2436
+ });
2437
+ }
2438
+ function renderSwimlane(slide, p) {
2439
+ const pal = P('theme10_swimlane_v1', p);
2440
+ frame(slide, pal);
2441
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2442
+ const items = t10Arr(p.items).slice(0, 4);
2443
+ const n = Math.max(1, items.length);
2444
+ const rh = (BOT - y0 - 0.2) / n;
2445
+ items.forEach((ln, i) => {
2446
+ const y = y0 + 0.2 + i * rh;
2447
+ slide.addShape('line', { x: MX, y, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
2448
+ txt(slide, ln.role ?? '', { x: MX, y: y + 0.12, w: 1.6, h: 0.4, fontSize: 15, bold: true, color: pal.gold, fontFace: S.fonts.heading });
2449
+ const tasks = normStrings(ln.tasks).slice(0, 4).join(' · ');
2450
+ txt(slide, tasks, { x: MX + 1.8, y: y + 0.12, w: CW - 1.8, h: 0.4, fontSize: 13, color: pal.ink, fontFace: S.fonts.body });
2451
+ });
2452
+ }
2453
+ function renderChecklist(slide, p) {
2454
+ const pal = P('theme10_checklist_v1', p);
2455
+ frame(slide, pal);
2456
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2457
+ const items = t10Arr(p.items).slice(0, 8);
2458
+ const half = Math.ceil(items.length / 2);
2459
+ const colW = (CW - 0.6) / 2;
2460
+ items.forEach((it, i) => {
2461
+ const c = i < half ? 0 : 1;
2462
+ const row = c === 0 ? i : i - half;
2463
+ const x = MX + c * (colW + 0.6);
2464
+ const y = y0 + 0.1 + row * ((BOT - y0 - 0.2) / half);
2465
+ txt(slide, it.done ? '✓' : '○', { x, y, w: 0.4, h: 0.34, fontSize: 16, color: it.done ? pal.green : pal.ink3, bold: true, fontFace: S.fonts.mono });
2466
+ txt(slide, it.text ?? '', { x: x + 0.45, y, w: colW - 0.45, h: 0.34, fontSize: 14, color: pal.ink, fontFace: S.fonts.body, valign: 'top' });
2467
+ });
2468
+ }
2469
+ function renderPlans(slide, p) {
2470
+ const pal = P('theme10_plans_v1', p);
2471
+ frame(slide, pal);
2472
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2473
+ const items = t10Arr(p.items).slice(0, 3);
2474
+ const n = Math.max(1, items.length);
2475
+ const gx = 0.4;
2476
+ const cw = (CW - (n - 1) * gx) / n;
2477
+ items.forEach((pl, i) => {
2478
+ const x = MX + i * (cw + gx);
2479
+ card(slide, x, y0 + 0.1, cw, BOT - y0 - 0.2, pal);
2480
+ slide.addShape('rect', { x, y: y0 + 0.1, w: cw, h: 0.06, fill: { color: pal.gold } });
2481
+ txt(slide, pl.tag ?? '', { x: x + 0.22, y: y0 + 0.28, w: cw - 0.44, h: 0.28, fontSize: 11, color: pal.gold, fontFace: S.fonts.mono, charSpacing: 1 });
2482
+ txt(slide, pl.name ?? '', { x: x + 0.22, y: y0 + 0.56, w: cw - 0.44, h: 0.4, fontSize: 20, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2483
+ const pts = normStrings(pl.points).slice(0, 5);
2484
+ pts.forEach((t, j) => {
2485
+ const ty = y0 + 1.1 + j * 0.42;
2486
+ txt(slide, '▸', { x: x + 0.22, y: ty, w: 0.3, h: 0.3, fontSize: 12, color: pal.accent });
2487
+ txt(slide, t, { x: x + 0.52, y: ty, w: cw - 0.74, h: 0.3, fontSize: 13, color: pal.ink2, fontFace: S.fonts.body });
2488
+ });
2489
+ });
2490
+ }
2491
+ function renderJourney(slide, p) {
2492
+ const pal = P('theme10_journey_v1', p);
2493
+ frame(slide, pal);
2494
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2495
+ const items = t10Arr(p.items).slice(0, 6);
2496
+ const n = Math.max(1, items.length);
2497
+ const gx = 0.5;
2498
+ const cw = (CW - (n - 1) * gx) / n;
2499
+ items.forEach((it, i) => {
2500
+ const x = MX + i * (cw + gx);
2501
+ const lv = Math.max(0, Math.min(100, Number(it.level ?? 50)));
2502
+ const bh = (lv / 100) * (BOT - y0 - 0.9);
2503
+ slide.addShape('rect', { x: x + cw / 2 - 0.18, y: BOT - 0.7 - bh, w: 0.36, h: bh, fill: { color: i % 2 ? pal.gold : pal.accent } });
2504
+ txt(slide, String(it.level ?? ''), { x, y: BOT - 0.7 - bh - 0.32, w: cw, h: 0.3, fontSize: 12, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
2505
+ txt(slide, it.stage ?? '', { x, y: BOT - 0.62, w: cw, h: 0.3, fontSize: 14, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.heading });
2506
+ txt(slide, it.action ?? '', { x: x - 0.1, y: BOT - 0.32, w: cw + 0.2, h: 0.3, fontSize: 10, color: pal.ink3, align: 'center', fontFace: S.fonts.body });
2507
+ });
2508
+ }
2509
+ function renderGoals(slide, p) {
2510
+ const pal = P('theme10_goals_v1', p);
2511
+ frame(slide, pal);
2512
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2513
+ const items = t10Arr(p.items).slice(0, 4);
2514
+ const n = Math.max(1, items.length);
2515
+ const gx = 0.4, gy = 0.4;
2516
+ const cols = n <= 2 ? n : 2;
2517
+ const rows = Math.ceil(n / cols);
2518
+ const cw = (CW - (cols - 1) * gx) / cols;
2519
+ const ch = (BOT - y0 - 0.2 - (rows - 1) * gy) / rows;
2520
+ items.forEach((it, i) => {
2521
+ const c = i % cols, r = Math.floor(i / cols);
2522
+ const x = MX + c * (cw + gx), y = y0 + 0.2 + r * (ch + gy);
2523
+ card(slide, x, y, cw, ch, pal);
2524
+ txt(slide, it.name ?? '', { x: x + 0.22, y: y + 0.18, w: cw * 0.6, h: 0.34, fontSize: 16, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2525
+ txt(slide, it.target ?? '', { x: x + cw - 1.6, y: y + 0.18, w: 1.4, h: 0.34, fontSize: 13, color: pal.ink2, align: 'right', fontFace: S.fonts.mono });
2526
+ const pct = Math.max(0, Math.min(100, Number(it.progress ?? 0)));
2527
+ slide.addShape('rect', { x: x + 0.22, y: y + ch / 2, w: cw - 0.44, h: 0.12, fill: { color: pal.surfaceElevated } });
2528
+ slide.addShape('rect', { x: x + 0.22, y: y + ch / 2, w: (cw - 0.44) * pct / 100, h: 0.12, fill: { color: pal.gold } });
2529
+ txt(slide, `${pct}%`, { x: x + 0.22, y: y + ch / 2 + 0.2, w: cw - 0.44, h: 0.3, fontSize: 13, bold: true, color: pal.gold, fontFace: S.fonts.mono });
2530
+ });
2531
+ }
2532
+ function renderGlossary(slide, p) {
2533
+ const pal = P('theme10_glossary_v1', p);
2534
+ frame(slide, pal);
2535
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2536
+ const items = t10Arr(p.items).slice(0, 8);
2537
+ const half = Math.ceil(items.length / 2);
2538
+ const colW = (CW - 0.6) / 2;
2539
+ items.forEach((it, i) => {
2540
+ const c = i < half ? 0 : 1;
2541
+ const row = c === 0 ? i : i - half;
2542
+ const x = MX + c * (colW + 0.6);
2543
+ const y = y0 + 0.1 + row * ((BOT - y0 - 0.2) / half);
2544
+ slide.addShape('line', { x, y, w: colW, h: 0, line: { color: pal.border, width: 0.75 } });
2545
+ txt(slide, it.term ?? '', { x, y: y + 0.06, w: colW, h: 0.32, fontSize: 15, bold: true, color: pal.gold, fontFace: S.fonts.heading });
2546
+ txt(slide, it.def ?? '', { x, y: y + 0.4, w: colW, h: 0.6, fontSize: 12, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
2547
+ });
2548
+ }
2549
+ function renderFaq(slide, p) {
2550
+ const pal = P('theme10_faq_v1', p);
2551
+ frame(slide, pal);
2552
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2553
+ const items = t10Arr(p.items).slice(0, 5);
2554
+ const n = Math.max(1, items.length);
2555
+ const rh = (BOT - y0 - 0.2) / n;
2556
+ items.forEach((it, i) => {
2557
+ const y = y0 + 0.2 + i * rh;
2558
+ slide.addShape('line', { x: MX, y, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
2559
+ txt(slide, `Q${String(i + 1).padStart(2, '0')}`, { x: MX, y: y + 0.08, w: 0.7, h: 0.3, fontSize: 12, bold: true, color: pal.gold, fontFace: S.fonts.mono });
2560
+ txt(slide, it.q ?? '', { x: MX + 0.8, y: y + 0.06, w: CW - 0.8, h: 0.32, fontSize: 15, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2561
+ txt(slide, it.a ?? '', { x: MX + 0.8, y: y + 0.4, w: CW - 0.8, h: rh - 0.5, fontSize: 11, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
2562
+ });
2563
+ }
2564
+ function renderIsotype(slide, p) {
2565
+ const pal = P('theme10_isotype_v1', p);
2566
+ frame(slide, pal);
2567
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2568
+ const items = t10Arr(p.items).slice(0, 5);
2569
+ const maxV = Math.max(1, ...items.map((it) => Number(it.value ?? 0)));
2570
+ const n = Math.max(1, items.length);
2571
+ const rh = (BOT - y0 - 0.2) / n;
2572
+ items.forEach((it, i) => {
2573
+ const y = y0 + 0.2 + i * rh;
2574
+ txt(slide, it.label ?? '', { x: MX, y: y + 0.04, w: 1.4, h: 0.32, fontSize: 14, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2575
+ const v = Math.round(Number(it.value ?? 0));
2576
+ const gn = Math.max(1, Math.round((v / maxV) * 16));
2577
+ const gw = 0.16, gg = 0.06;
2578
+ for (let j = 0; j < gn; j++) {
2579
+ slide.addShape('rect', { x: MX + 1.5 + j * (gw + gg), y: y + 0.06, w: gw, h: gw, fill: { color: i % 2 ? pal.gold : pal.accent } });
2580
+ }
2581
+ txt(slide, String(v), { x: 9.0, y: y + 0.02, w: 0.3, h: 0.32, fontSize: 16, bold: true, color: pal.ink, align: 'right', fontFace: S.fonts.mono });
2582
+ });
2583
+ }
2584
+ function renderVenn(slide, p) {
2585
+ const pal = P('theme10_venn_v1', p);
2586
+ frame(slide, pal);
2587
+ const y0 = t10Head(slide, p, pal, { size: 30 });
2588
+ const items = t10Arr(p.items).slice(0, 3);
2589
+ const names = [items[0]?.name ?? 'A', items[1]?.name ?? 'B', items[2]?.name ?? 'C'];
2590
+ const cols = [pal.accent, pal.gold, pal.copper];
2591
+ const R = 1.15;
2592
+ const cx = [3.7, 6.3, 5.0];
2593
+ const cy = [y0 + 1.7, y0 + 1.7, y0 + 2.9];
2594
+ cx.forEach((x, i) => {
2595
+ slide.addShape('oval', { x: x - R, y: cy[i] - R, w: R * 2, h: R * 2, fill: { color: cols[i], transparency: 80 }, line: { color: cols[i], width: 1.25 } });
2596
+ });
2597
+ txt(slide, names[0], { x: cx[0] - R, y: cy[0] - R - 0.3, w: R * 2, h: 0.3, align: 'center', fontSize: 14, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2598
+ txt(slide, names[1], { x: cx[1] - R, y: cy[1] - R - 0.3, w: R * 2, h: 0.3, align: 'center', fontSize: 14, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2599
+ txt(slide, names[2], { x: cx[2] - R, y: cy[2] + R + 0.05, w: R * 2, h: 0.3, align: 'center', fontSize: 14, bold: true, color: pal.ink, fontFace: S.fonts.heading });
2600
+ txt(slide, '交集 = 共同诉求', { x: 3.3, y: y0 + 2.2, w: 3.4, h: 0.3, align: 'center', fontSize: 12, color: pal.ink2, fontFace: S.fonts.body });
2601
+ }
2602
+ // ────────────────────────────────────────────────────────────────
2603
+ // W4 扩展版式(7 个)· PPTX 渲染器
2604
+ // ────────────────────────────────────────────────────────────────
2605
+ /** 涨跌色(红涨绿跌:A股惯例;与 index_board 网页端一致)。 */
2606
+ function t10DeltaColor(pal, delta, redUp = false) {
2607
+ const up = delta != null && !/^-/.test(String(delta).trim());
2608
+ if (redUp)
2609
+ return up ? pal.red : pal.green;
2610
+ return up ? pal.green : pal.red;
2611
+ }
2612
+ /** 内容页页眉(栏标=section + 标题),返回正文起始 y。 */
2613
+ function t10HeadX(slide, p, pal, size = 30) {
2614
+ kicker(slide, p.section, pal, { y: 0.78 });
2615
+ title(slide, p.title, pal, { x: MX, y: 1.14, w: CW, size });
2616
+ return 1.14 + size / 72 * 1.18 + 0.45;
2617
+ }
2618
+ // 76. metric-hero · 英雄指标
2619
+ function renderMetricHero(slide, p) {
2620
+ const pal = P('theme10_metric_hero_v1', p);
2621
+ frame(slide, pal);
2622
+ kicker(slide, p.section, pal, { y: 0.78 });
2623
+ title(slide, p.title, pal, { x: MX, y: 1.2, w: CW * 0.82, size: 24 });
2624
+ const y0 = 2.5;
2625
+ txt(slide, p.label, { x: MX, y: y0, w: CW, h: 0.3, fontSize: 14, color: pal.ink3, fontFace: S.fonts.mono, charSpacing: 1 });
2626
+ txt(slide, `${p.value ?? ''}${p.unit ? ' ' + p.unit : ''}`, { x: MX, y: y0 + 0.32, w: CW, h: 1.4, fontSize: 84, bold: true, color: pal.ink, fontFace: S.fonts.mono });
2627
+ if (p.delta)
2628
+ txt(slide, p.delta, { x: MX, y: y0 + 1.78, w: CW, h: 0.36, fontSize: 16, bold: true, color: t10DeltaColor(pal, p.delta), fontFace: S.fonts.mono });
2629
+ if (p.context)
2630
+ txt(slide, p.context, { x: MX, y: y0 + 2.2, w: CW * 0.82, h: 0.9, fontSize: 14, color: pal.ink2, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.25 });
2631
+ footer(slide, p.folioLeft, p.folioRight, pal);
2632
+ }
2633
+ // 77. scorecard · 记分卡
2634
+ function renderScorecard(slide, p) {
2635
+ const pal = P('theme10_scorecard_v1', p);
2636
+ frame(slide, pal);
2637
+ const y0 = t10HeadX(slide, p, pal, 26);
2638
+ const items = (Array.isArray(p.items) ? p.items : []).slice(0, 6);
2639
+ const gx = 0.28, gy = 0.28;
2640
+ const cols = Math.min(Math.max(items.length, 1), 3);
2641
+ const rows = Math.ceil(items.length / cols);
2642
+ const areaH = BOT - y0 - 0.4;
2643
+ const cw = (CW - gx * (cols - 1)) / cols, ch = (areaH - gy * (rows - 1)) / rows;
2644
+ items.forEach((it, i) => {
2645
+ const c = i % cols, r = Math.floor(i / cols);
2646
+ const x = MX + c * (cw + gx), y = y0 + r * (ch + gy);
2647
+ card(slide, x, y, cw, ch, pal);
2648
+ txt(slide, it.label ?? '', { x: x + 0.24, y: y + 0.2, w: cw - 0.48, h: 0.3, fontSize: 12, color: pal.ink2, fontFace: S.fonts.body });
2649
+ txt(slide, `${it.score ?? ''}${it.total ? ' / ' + it.total : ''}`, { x: x + 0.24, y: y + 0.56, w: cw - 0.48, h: ch - 1.2, fontSize: 36, bold: true, color: pal.ink, fontFace: S.fonts.mono });
2650
+ if (it.note)
2651
+ txt(slide, it.note, { x: x + 0.24, y: y + ch - 0.6, w: cw - 0.48, h: 0.5, fontSize: 11, color: pal.ink3, fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
2652
+ });
2653
+ footer(slide, p.folioLeft, p.folioRight, pal);
2654
+ }
2655
+ // 78. comparison-stat · 对比指标
2656
+ function renderComparisonStat(slide, p) {
2657
+ const pal = P('theme10_comparison_stat_v1', p);
2658
+ frame(slide, pal);
2659
+ const y0 = t10HeadX(slide, p, pal, 26);
2660
+ const left = p.left ?? {}, right = p.right ?? {};
2661
+ const midY = (y0 + BOT) / 2 - 0.3;
2662
+ const sideW = 3.0, gapC = 0.6;
2663
+ const lx = MX + (CW - 2 * sideW - gapC) / 2;
2664
+ const rx = lx + sideW + gapC;
2665
+ const drawSide = (it, x) => {
2666
+ txt(slide, it.label ?? '', { x, y: midY - 0.9, w: sideW, h: 0.3, fontSize: 13, color: pal.ink2, align: 'center', fontFace: S.fonts.body });
2667
+ txt(slide, `${it.value ?? ''}${it.unit ? ' ' + it.unit : ''}`, { x, y: midY - 0.55, w: sideW, h: 1.0, fontSize: 52, bold: true, color: pal.ink, align: 'center', fontFace: S.fonts.mono });
2668
+ if (it.delta)
2669
+ txt(slide, it.delta, { x, y: midY + 0.55, w: sideW, h: 0.34, fontSize: 15, bold: true, color: t10DeltaColor(pal, it.delta), align: 'center', fontFace: S.fonts.mono });
2670
+ };
2671
+ drawSide(left, lx);
2672
+ drawSide(right, rx);
2673
+ slide.addShape('oval', { x: MX + CW / 2 - 0.32, y: midY - 0.32, w: 0.64, h: 0.64, fill: { color: pal.bg }, line: { color: pal.gold, width: 1.25 } });
2674
+ txt(slide, p.versus ?? 'VS', { x: MX + CW / 2 - 0.32, y: midY - 0.12, w: 0.64, h: 0.3, fontSize: 14, bold: true, color: pal.gold, align: 'center', fontFace: S.fonts.mono });
2675
+ footer(slide, p.folioLeft, p.folioRight, pal);
2676
+ }
2677
+ // 79. small-multiples · 小多图(PPTX 近似:每格迷你柱+标题)
2678
+ function renderSmallMultiples(slide, p) {
2679
+ const pal = P('theme10_small_multiples_v1', p);
2680
+ frame(slide, pal);
2681
+ const y0 = t10HeadX(slide, p, pal, 26);
2682
+ const panels = (Array.isArray(p.panels) ? p.panels : []).slice(0, 6);
2683
+ const gx = 0.26, gy = 0.26;
2684
+ const cols = panels.length > 1 ? 2 : 1;
2685
+ const rows = Math.ceil(panels.length / cols);
2686
+ const areaH = BOT - y0 - 0.4;
2687
+ const cw = (CW - gx * (cols - 1)) / cols, ch = (areaH - gy * (rows - 1)) / rows;
2688
+ const colors = series(pal);
2689
+ panels.forEach((pn, i) => {
2690
+ const c = i % cols, r = Math.floor(i / cols);
2691
+ const x = MX + c * (cw + gx), y = y0 + r * (ch + gy);
2692
+ card(slide, x, y, cw, ch, pal, pal.surface, pal.border);
2693
+ const vals = t10ParseVals(pn.values);
2694
+ txt(slide, pn.title ?? '', { x: x + 0.22, y: y + 0.16, w: cw - 0.44, h: 0.3, fontSize: 13, bold: true, color: pal.ink2, fontFace: S.fonts.body });
2695
+ if (vals.length) {
2696
+ const plotX = x + 0.3, plotW = cw - 0.6, plotY = y + 0.6, plotH = ch - 1.0;
2697
+ const mx = Math.max(...vals, 1), mn = Math.min(...vals, 0);
2698
+ const n = vals.length, bw = plotW / n * 0.7, gap = plotW / n * 0.3;
2699
+ vals.forEach((v, k) => {
2700
+ const bh = (v - mn) / (mx - mn || 1) * plotH;
2701
+ const bx = plotX + k * (bw + gap) + gap / 2;
2702
+ slide.addShape('rect', { x: bx, y: plotY + plotH - bh, w: Math.max(bw, 0.04), h: Math.max(bh, 0.02), fill: { color: colors[k % colors.length] } });
2703
+ });
2704
+ }
2705
+ });
2706
+ footer(slide, p.folioLeft, p.folioRight, pal);
2707
+ }
2708
+ // 80. stat-strip · 指标长条
2709
+ function renderStatStrip(slide, p) {
2710
+ const pal = P('theme10_stat_strip_v1', p);
2711
+ frame(slide, pal);
2712
+ const y0 = t10HeadX(slide, p, pal, 26);
2713
+ const items = (Array.isArray(p.items) ? p.items : []).slice(0, 6);
2714
+ const n = Math.max(items.length, 1);
2715
+ const cw = CW / n;
2716
+ const y = (y0 + BOT) / 2 - 0.7;
2717
+ items.forEach((it, i) => {
2718
+ const x = MX + i * cw;
2719
+ if (i > 0)
2720
+ slide.addShape('line', { x, y: y + 0.1, w: 0, h: 1.2, line: { color: pal.border, width: 0.75 } });
2721
+ const px = x + 0.22;
2722
+ txt(slide, it.label ?? '', { x: px, y, w: cw - 0.44, h: 0.3, fontSize: 12, color: pal.ink2, fontFace: S.fonts.body });
2723
+ txt(slide, `${it.value ?? ''}${it.unit ? ' ' + it.unit : ''}`, { x: px, y: y + 0.34, w: cw - 0.44, h: 0.7, fontSize: 34, bold: true, color: pal.ink, fontFace: S.fonts.mono });
2724
+ if (it.delta)
2725
+ txt(slide, it.delta, { x: px, y: y + 1.06, w: cw - 0.44, h: 0.3, fontSize: 13, bold: true, color: t10DeltaColor(pal, it.delta), fontFace: S.fonts.mono });
2726
+ });
2727
+ footer(slide, p.folioLeft, p.folioRight, pal);
2728
+ }
2729
+ // 81. quote-stat · 金句指标
2730
+ function renderQuoteStat(slide, p) {
2731
+ const pal = P('theme10_quote_stat_v1', p);
2732
+ frame(slide, pal);
2733
+ kicker(slide, p.section, pal, { y: 0.78 });
2734
+ const qy = 1.4;
2735
+ txt(slide, p.quote, { x: MX, y: qy, w: CW * 0.82, h: 2.0, fontSize: 34, bold: true, color: pal.ink, fontFace: S.fonts.heading, valign: 'top', lineSpacingMultiple: 1.25 });
2736
+ if (p.sign)
2737
+ txt(slide, p.sign, { x: MX, y: qy + 2.0, w: CW * 0.82, h: 0.3, fontSize: 13, color: pal.gold, fontFace: S.fonts.mono });
2738
+ const cy = 2.9;
2739
+ slide.addShape('line', { x: MX, y: cy, w: 0, h: 1.4, line: { color: pal.gold, width: 2.5 } });
2740
+ txt(slide, p.statLabel, { x: MX + 0.22, y: cy, w: 3.4, h: 0.3, fontSize: 13, color: pal.ink3, fontFace: S.fonts.mono, charSpacing: 1 });
2741
+ txt(slide, `${p.statValue ?? ''}${p.statUnit ? ' ' + p.statUnit : ''}`, { x: MX + 0.22, y: cy + 0.32, w: 3.4, h: 0.8, fontSize: 40, bold: true, color: pal.ink, fontFace: S.fonts.mono });
2742
+ if (p.statDelta)
2743
+ txt(slide, p.statDelta, { x: MX + 0.22, y: cy + 1.12, w: 3.4, h: 0.3, fontSize: 14, bold: true, color: t10DeltaColor(pal, p.statDelta), fontFace: S.fonts.mono });
2744
+ footer(slide, p.folioLeft, p.folioRight, pal);
2745
+ }
2746
+ // 82. index-board · 指数看板(红涨绿跌)
2747
+ function renderIndexBoard(slide, p) {
2748
+ const pal = P('theme10_index_board_v1', p);
2749
+ frame(slide, pal);
2750
+ const y0 = t10HeadX(slide, p, pal, 26);
2751
+ const rows = (Array.isArray(p.rows) ? p.rows : []).slice(0, 8);
2752
+ const hy = y0 + 0.05;
2753
+ const cols = [0, 2.2, 4.0, 6.6].map((x) => MX + x);
2754
+ txt(slide, '代码', { x: cols[0], y: hy, w: 2.0, h: 0.26, fontSize: 10, color: pal.ink3, charSpacing: 1, fontFace: S.fonts.mono });
2755
+ txt(slide, '名称', { x: cols[1], y: hy, w: 1.8, h: 0.26, fontSize: 10, color: pal.ink3, charSpacing: 1, fontFace: S.fonts.mono });
2756
+ txt(slide, '最新价', { x: cols[2], y: hy, w: 2.6, h: 0.26, fontSize: 10, color: pal.ink3, align: 'right', charSpacing: 1, fontFace: S.fonts.mono });
2757
+ txt(slide, '涨跌', { x: cols[3], y: hy, w: 2.6, h: 0.26, fontSize: 10, color: pal.ink3, align: 'right', charSpacing: 1, fontFace: S.fonts.mono });
2758
+ slide.addShape('line', { x: MX, y: hy + 0.34, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
2759
+ const rh = (BOT - hy - 0.5) / Math.max(rows.length, 1);
2760
+ rows.forEach((r, i) => {
2761
+ const y = hy + 0.42 + i * rh;
2762
+ txt(slide, r.code ?? '', { x: cols[0], y, w: 2.0, h: rh - 0.1, fontSize: 15, bold: true, color: pal.ink, fontFace: S.fonts.mono, valign: 'middle' });
2763
+ txt(slide, r.name ?? '', { x: cols[1], y, w: 1.8, h: rh - 0.1, fontSize: 15, color: pal.ink2, fontFace: S.fonts.body, valign: 'middle' });
2764
+ txt(slide, r.value ?? '', { x: cols[2], y, w: 2.6, h: rh - 0.1, fontSize: 18, bold: true, color: pal.ink, align: 'right', fontFace: S.fonts.mono, valign: 'middle' });
2765
+ txt(slide, r.delta ?? '', { x: cols[3], y, w: 2.6, h: rh - 0.1, fontSize: 15, bold: true, color: t10DeltaColor(pal, r.delta, true), align: 'right', fontFace: S.fonts.mono, valign: 'middle' });
2766
+ if (i < rows.length - 1)
2767
+ slide.addShape('line', { x: MX, y: y + rh, w: CW, h: 0, line: { color: pal.border, width: 0.5 } });
2768
+ });
2769
+ footer(slide, p.folioLeft, p.folioRight, pal);
2770
+ }
2771
+ const RENDERERS = {
2772
+ theme10_cover_dusk_v1: renderCoverDusk,
2773
+ theme10_cover_field_v1: renderCoverField,
2774
+ theme10_cover_atmos_v1: renderCoverAtmos,
2775
+ theme10_cover_horizon_v1: renderCoverHorizon,
2776
+ theme10_cover_standard_v1: renderCoverStandard,
2777
+ theme10_cover_dawn_v1: renderCoverDawn,
2778
+ theme10_chapter_v1: renderChapter,
2779
+ theme10_divider_v1: renderDivider,
2780
+ theme10_statement_section_v1: renderStatementSection,
2781
+ theme10_statement_v1: renderStatement,
2782
+ theme10_principles_v1: renderPrinciples,
2783
+ theme10_closing_v1: renderClosing,
2784
+ // P1 影像图文 批次A
2785
+ theme10_profile_v1: renderProfile,
2786
+ theme10_team_v1: renderTeam,
2787
+ theme10_quote_v1: renderQuote,
2788
+ theme10_editorial_v1: renderEditorial,
2789
+ theme10_magazine_v1: renderMagazine,
2790
+ // P1 影像图文 批次B
2791
+ theme10_triptych_v1: renderTriptych,
2792
+ theme10_strata_v1: renderStrata,
2793
+ theme10_spark_v1: renderSpark,
2794
+ theme10_testimonials_v1: renderTestimonials,
2795
+ theme10_feature_v1: renderFeature,
2796
+ // P1 影像图文 批次C
2797
+ theme10_compareimg_v1: renderCompareImg,
2798
+ theme10_pinboard_v1: renderPinboard,
2799
+ theme10_filmstrip_v1: renderFilmstrip,
2800
+ theme10_inset_v1: renderInset,
2801
+ theme10_gallery2_v1: renderGallery2,
2802
+ // P1 影像图文 批次D
2803
+ theme10_mosaic_v1: renderMosaic,
2804
+ theme10_collage_v1: renderCollage,
2805
+ theme10_captioned_v1: renderCaptioned,
2806
+ theme10_showcase_v1: renderShowcase,
2807
+ theme10_poster_v1: renderPoster,
2808
+ // P1 影像图文 批次E
2809
+ theme10_annotated_v1: renderAnnotated,
2810
+ theme10_quoteimg_v1: renderQuoteImg,
2811
+ theme10_quilt_v1: renderQuilt,
2812
+ theme10_exhibit_v1: renderExhibit,
2813
+ theme10_medallions_v1: renderMedallions,
2814
+ // P2 数据图表 批次F(基础图表 5 版式)
2815
+ theme10_bar_v1: renderBar,
2816
+ theme10_hbar_v1: renderHBar,
2817
+ theme10_line_v1: renderLine,
2818
+ theme10_area_v1: renderArea,
2819
+ theme10_kpis_v1: renderKpis,
2820
+ // P2 数据图表 批次G(对比与构成 5 版式)
2821
+ theme10_grouped_v1: renderGrouped,
2822
+ theme10_stack_v1: renderStack,
2823
+ theme10_donut_v1: renderDonut,
2824
+ theme10_pie_v1: renderPie,
2825
+ theme10_waterfall_v1: renderWaterfall,
2826
+ // P2 数据图表 批次H(分布与关系 5 版式)
2827
+ theme10_scatter_v1: renderScatter,
2828
+ theme10_bubble_v1: renderBubble,
2829
+ theme10_radar_v1: renderRadar,
2830
+ theme10_radial_v1: renderRadial,
2831
+ theme10_heat_v1: renderHeat,
2832
+ // P2 数据图表 批次I(趋势与时间 5 版式)
2833
+ theme10_trend_v1: renderTrend,
2834
+ theme10_range_v1: renderRange,
2835
+ theme10_candlestick_v1: renderCandlestick,
2836
+ theme10_ridgeline_v1: renderRidgeline,
2837
+ theme10_calendar_v1: renderCalendar,
2838
+ // P2 数据图表 批次J(分布与构成进阶 5 版式)
2839
+ theme10_funnel_v1: renderFunnel,
2840
+ theme10_gauge_v1: renderGauge,
2841
+ theme10_bullet_v1: renderBullet,
2842
+ theme10_box_v1: renderBox,
2843
+ theme10_treemap_v1: renderTreemap,
2844
+ // P2 数据图表 批次K(关系与构成进阶 5 版式)
2845
+ theme10_sankey_v1: renderSankey,
2846
+ theme10_dumbbell_v1: renderDumbbell,
2847
+ theme10_histogram_v1: renderHistogram,
2848
+ theme10_slope_v1: renderSlope,
2849
+ theme10_waffle_v1: renderWaffle,
2850
+ // P2 数据图表 批次L(时序与分布 5 版式)
2851
+ theme10_gantt_v1: renderGantt,
2852
+ theme10_bump_v1: renderBump,
2853
+ theme10_rose_v1: renderRose,
2854
+ theme10_dotplot_v1: renderDotplot,
2855
+ theme10_timeline_v1: renderTimeline,
2856
+ // P2 数据图表 批次M(结构与关系 5 版式)
2857
+ theme10_orgchart_v1: renderOrgChart,
2858
+ theme10_parallel_v1: renderParallel,
2859
+ theme10_circlepack_v1: renderCirclepack,
2860
+ theme10_cscatter_v1: renderCscatter,
2861
+ theme10_marimekko_v1: renderMarimekko,
2862
+ // P3 结构 / 流程 / 长尾(11 版式)
2863
+ theme10_steps_v1: renderSteps,
2864
+ theme10_cycle_v1: renderCycle,
2865
+ theme10_swimlane_v1: renderSwimlane,
2866
+ theme10_checklist_v1: renderChecklist,
2867
+ theme10_plans_v1: renderPlans,
2868
+ theme10_journey_v1: renderJourney,
2869
+ theme10_goals_v1: renderGoals,
2870
+ theme10_glossary_v1: renderGlossary,
2871
+ theme10_faq_v1: renderFaq,
2872
+ theme10_isotype_v1: renderIsotype,
2873
+ theme10_venn_v1: renderVenn,
2874
+ // W4 扩展版式(7 个)
2875
+ theme10_metric_hero_v1: renderMetricHero,
2876
+ theme10_scorecard_v1: renderScorecard,
2877
+ theme10_comparison_stat_v1: renderComparisonStat,
2878
+ theme10_small_multiples_v1: renderSmallMultiples,
2879
+ theme10_stat_strip_v1: renderStatStrip,
2880
+ theme10_quote_stat_v1: renderQuoteStat,
2881
+ theme10_index_board_v1: renderIndexBoard,
2882
+ };
2883
+ /** 将全部 theme10 渲染器注册到导出管线的按 layoutId 索引注册表。 */
2884
+ export function registerTheme10Renderers(register) {
2885
+ for (const [id, fn] of Object.entries(RENDERERS)) {
2886
+ register(id, fn);
2887
+ }
2888
+ }
2889
+ //# sourceMappingURL=theme10-pptx.js.map