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