@lemonppt/renderer 1.0.6 → 1.0.8

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 (92) hide show
  1. package/dist/browser-export-driver.d.ts +28 -0
  2. package/dist/browser-export-driver.d.ts.map +1 -0
  3. package/dist/browser-export-driver.js +115 -0
  4. package/dist/browser-export-driver.js.map +1 -0
  5. package/dist/browser-export.d.ts +66 -0
  6. package/dist/browser-export.d.ts.map +1 -0
  7. package/dist/browser-export.js +176 -0
  8. package/dist/browser-export.js.map +1 -0
  9. package/dist/client/browser-export.js +1 -0
  10. package/dist/client/client-render.js +21 -21
  11. package/dist/client/editor-script.js +1 -2
  12. package/dist/client/theme-echarts.js +27 -27
  13. package/dist/editor-script.d.ts +1 -1
  14. package/dist/editor-script.d.ts.map +1 -1
  15. package/dist/editor-script.js +1 -2
  16. package/dist/editor-script.js.map +1 -1
  17. package/dist/export-pdf.d.ts.map +1 -1
  18. package/dist/export-pdf.js +7 -4
  19. package/dist/export-pdf.js.map +1 -1
  20. package/dist/export-pptx-screenshot.d.ts +36 -0
  21. package/dist/export-pptx-screenshot.d.ts.map +1 -0
  22. package/dist/export-pptx-screenshot.js +74 -0
  23. package/dist/export-pptx-screenshot.js.map +1 -0
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/normalize-goal.d.ts +6 -0
  29. package/dist/normalize-goal.d.ts.map +1 -0
  30. package/dist/normalize-goal.js +12 -0
  31. package/dist/normalize-goal.js.map +1 -0
  32. package/dist/preview-server.d.ts +9 -0
  33. package/dist/preview-server.d.ts.map +1 -0
  34. package/dist/preview-server.js +81 -0
  35. package/dist/preview-server.js.map +1 -0
  36. package/dist/render-editor-data.d.ts.map +1 -1
  37. package/dist/render-editor-data.js +29 -2
  38. package/dist/render-editor-data.js.map +1 -1
  39. package/dist/render.d.ts.map +1 -1
  40. package/dist/render.js +25 -5
  41. package/dist/render.js.map +1 -1
  42. package/package.json +8 -5
  43. package/templates/editor.html +26 -0
  44. package/templates/editor.js +1 -0
  45. package/dist/export-pptx.d.ts +0 -1167
  46. package/dist/export-pptx.d.ts.map +0 -1
  47. package/dist/export-pptx.js +0 -3007
  48. package/dist/export-pptx.js.map +0 -1
  49. package/dist/theme01-pptx.d.ts +0 -12
  50. package/dist/theme01-pptx.d.ts.map +0 -1
  51. package/dist/theme01-pptx.js +0 -2957
  52. package/dist/theme01-pptx.js.map +0 -1
  53. package/dist/theme02-pptx.d.ts +0 -5
  54. package/dist/theme02-pptx.d.ts.map +0 -1
  55. package/dist/theme02-pptx.js +0 -1063
  56. package/dist/theme02-pptx.js.map +0 -1
  57. package/dist/theme03-pptx.d.ts +0 -5
  58. package/dist/theme03-pptx.d.ts.map +0 -1
  59. package/dist/theme03-pptx.js +0 -3279
  60. package/dist/theme03-pptx.js.map +0 -1
  61. package/dist/theme04-pptx.d.ts +0 -6
  62. package/dist/theme04-pptx.d.ts.map +0 -1
  63. package/dist/theme04-pptx.js +0 -5496
  64. package/dist/theme04-pptx.js.map +0 -1
  65. package/dist/theme05-pptx.d.ts +0 -5
  66. package/dist/theme05-pptx.d.ts.map +0 -1
  67. package/dist/theme05-pptx.js +0 -3152
  68. package/dist/theme05-pptx.js.map +0 -1
  69. package/dist/theme06-pptx.d.ts +0 -124
  70. package/dist/theme06-pptx.d.ts.map +0 -1
  71. package/dist/theme06-pptx.js +0 -8154
  72. package/dist/theme06-pptx.js.map +0 -1
  73. package/dist/theme07-pptx.d.ts +0 -49
  74. package/dist/theme07-pptx.d.ts.map +0 -1
  75. package/dist/theme07-pptx.js +0 -2579
  76. package/dist/theme07-pptx.js.map +0 -1
  77. package/dist/theme08-pptx.d.ts +0 -19
  78. package/dist/theme08-pptx.d.ts.map +0 -1
  79. package/dist/theme08-pptx.js +0 -1411
  80. package/dist/theme08-pptx.js.map +0 -1
  81. package/dist/theme09-pptx.d.ts +0 -18
  82. package/dist/theme09-pptx.d.ts.map +0 -1
  83. package/dist/theme09-pptx.js +0 -1513
  84. package/dist/theme09-pptx.js.map +0 -1
  85. package/dist/theme10-pptx.d.ts +0 -18
  86. package/dist/theme10-pptx.d.ts.map +0 -1
  87. package/dist/theme10-pptx.js +0 -2889
  88. package/dist/theme10-pptx.js.map +0 -1
  89. package/dist/theme11-pptx.d.ts +0 -17
  90. package/dist/theme11-pptx.d.ts.map +0 -1
  91. package/dist/theme11-pptx.js +0 -908
  92. package/dist/theme11-pptx.js.map +0 -1
@@ -1,2889 +0,0 @@
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