@topmindspace/tms-skills 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +139 -134
  2. package/bin/tms-skills.js +160 -160
  3. package/package.json +48 -48
  4. package/top-ppt-html/README.md +254 -254
  5. package/top-ppt-html/SKILL.md +111 -111
  6. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3873 -3926
  7. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3873 -3926
  9. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +167 -167
  10. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4272 -4325
  11. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +320 -320
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4272 -4325
  13. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +320 -320
  14. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4272 -4325
  15. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
  16. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5474 -5527
  17. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +913 -913
  18. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5474 -5527
  19. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +913 -913
  20. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5474 -5527
  21. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
  22. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5474 -5527
  23. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +913 -913
  24. package/top-ppt-html/assets/pptx-export.js +1944 -1944
  25. package/top-ppt-html/assets/style-gallery.html +559 -589
  26. package/top-ppt-html/assets/templates/architecture.html +3675 -3728
  27. package/top-ppt-html/assets/templates/engine.css +787 -840
  28. package/top-ppt-html/assets/templates/presentation.html +3685 -3738
  29. package/top-ppt-html/assets/templates/research.html +3964 -4017
  30. package/top-ppt-html/assets/templates/ui.js +520 -520
  31. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  32. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  33. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  34. package/top-ppt-html/assets/theme-overview.png +0 -0
  35. package/top-ppt-html/evals/prompts.csv +15 -15
  36. package/top-ppt-html/evals/rubric.schema.json +25 -25
  37. package/top-ppt-html/evals/run_evals.py +220 -220
  38. package/top-ppt-html/evals/trace.example.json +16 -16
  39. package/top-ppt-html/package-lock.json +175 -0
  40. package/top-ppt-html/package.json +30 -35
  41. package/top-ppt-html/references/charts-basic.md +624 -624
  42. package/top-ppt-html/references/charts-discipline.md +108 -108
  43. package/top-ppt-html/references/charts-extended.md +482 -482
  44. package/top-ppt-html/references/charts.md +28 -28
  45. package/top-ppt-html/references/components-atoms.md +624 -624
  46. package/top-ppt-html/references/components.md +30 -30
  47. package/top-ppt-html/references/content-rules.md +490 -490
  48. package/top-ppt-html/references/design-system-engine.md +235 -235
  49. package/top-ppt-html/references/design-system.md +471 -478
  50. package/top-ppt-html/references/failure-modes.md +214 -214
  51. package/top-ppt-html/references/high-fidelity.md +127 -127
  52. package/top-ppt-html/references/icons.md +397 -397
  53. package/top-ppt-html/references/industry-benchmark.md +105 -105
  54. package/top-ppt-html/references/infographics-stats.md +308 -308
  55. package/top-ppt-html/references/infographics-structure.md +226 -226
  56. package/top-ppt-html/references/infographics.md +43 -43
  57. package/top-ppt-html/references/layout-grammar.md +315 -315
  58. package/top-ppt-html/references/layouts-architecture.md +108 -108
  59. package/top-ppt-html/references/layouts-combo.md +600 -600
  60. package/top-ppt-html/references/layouts-research.md +160 -160
  61. package/top-ppt-html/references/modes.md +254 -254
  62. package/top-ppt-html/references/outline-design.md +275 -275
  63. package/top-ppt-html/references/playbook.md +266 -266
  64. package/top-ppt-html/references/pptx-export.md +209 -209
  65. package/top-ppt-html/references/reform-plan.md +252 -252
  66. package/top-ppt-html/references/styles.md +336 -370
  67. package/top-ppt-html/references/tech-design.md +138 -138
  68. package/top-ppt-html/scripts/audit_css.py +109 -109
  69. package/top-ppt-html/scripts/audit_docs.py +176 -176
  70. package/top-ppt-html/scripts/audit_skill.py +220 -220
  71. package/top-ppt-html/scripts/audit_styles.py +293 -351
  72. package/top-ppt-html/scripts/build_examples.py +2276 -2276
  73. package/top-ppt-html/scripts/build_pptx.js +2380 -2380
  74. package/top-ppt-html/scripts/capture_theme_overview.js +78 -78
  75. package/top-ppt-html/scripts/checks_html.py +127 -127
  76. package/top-ppt-html/scripts/cross_verify.py +294 -294
  77. package/top-ppt-html/scripts/env_probe.py +158 -158
  78. package/top-ppt-html/scripts/extract_model.py +210 -210
  79. package/top-ppt-html/scripts/extract_snippet.py +374 -374
  80. package/top-ppt-html/scripts/gen_channel_a.js +214 -214
  81. package/top-ppt-html/scripts/layout-constants.json +3309 -3377
  82. package/top-ppt-html/scripts/layout_slots.json +830 -830
  83. package/top-ppt-html/scripts/lib_layout_regions.js +412 -412
  84. package/top-ppt-html/scripts/measure_height.py +178 -178
  85. package/top-ppt-html/scripts/model-schema.json +547 -547
  86. package/top-ppt-html/scripts/negative_tests.py +307 -307
  87. package/top-ppt-html/scripts/package_skill.py +291 -291
  88. package/top-ppt-html/scripts/prepare_images.py +341 -341
  89. package/top-ppt-html/scripts/probe_image_export.py +188 -188
  90. package/top-ppt-html/scripts/quality_gate.py +301 -301
  91. package/top-ppt-html/scripts/regression.py +307 -308
  92. package/top-ppt-html/scripts/render_compare.py +275 -275
  93. package/top-ppt-html/scripts/render_from_model.py +698 -698
  94. package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
  95. package/top-ppt-html/scripts/section-file-map.json +104 -104
  96. package/top-ppt-html/scripts/sync_runtime.py +662 -662
  97. package/top-ppt-html/scripts/validate_pptx.py +1510 -1510
  98. package/top-ppt-html/scripts/validate_report.py +1456 -1456
@@ -1,1944 +1,1944 @@
1
- /* ═══════════════════════════════════════════════════════════════════════════
2
- * TopPPT HTML· 浏览器端 PPTX 预览运行时
3
- * 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
4
- *
5
- * 定位:HTML 是阅读与演示界面,PPTX 是协作与修改格式——两者来自同一内容模型
6
- * (window.REPORT_MODEL)。页面只提供「预览 PPTX」(WYSIWYG 模态,与智能体精导通道
7
- * 同一序列化引擎)与可复制 AI 提示词;正式 PPTX 一律由本地智能体走精导通道
8
- * (build_pptx.js)生成并过 strict 硬门禁,浏览器端导出达不到该质量标准。
9
- *
10
- * API:
11
- * TopPptHtml.slidesXml(model) → string[](每页 slide XML,预览模态渲染用)
12
- * TopPptHtml.validateModel(model) → {ok, missing[], warnings[], pages}(schema 单源驱动)
13
- * TopPptHtml.slidesOf / slideXml / relsXml / NS / PRESETS
14
- * → 供 scripts/gen_channel_a.js 在 Node 侧组装完整
15
- * PPTX 包(回归双裁判内部质量路径,页面不使用)
16
- *
17
- * model = window.REPORT_MODEL(JSON,与页面正文同源):
18
- * { mode, style, theme(light|dark:PPTX 按此选亮/暗 token,默认 light),
19
- * title, subtitle, meta, agenda:[[num,标题,说明]…](architecture 可省),
20
- * sections:[{type: points|metrics|kpi|table|timeline|steps|bar|donut|heatmap|bullet|pyramid|
21
- * image|cards|split|comparison|quote|diagram|exhibit|twocol|threecol|halftable|
22
- * matrix|lane, soWhat?, footnote?, flags?, image?, …}],
23
- * closing:{title,points} }
24
- * 通用可选字段:soWhat(结论条)/ footnote(来源行)/ flags(待核实清单 → accent 强调色条)
25
- * / image(素材图片或配图占位,{src|items|placeholder, layout, fit, caption, alt, hint},
26
- * src 仅 data: 或相对路径;layout ∈ full/half/bleed/grid/compare/wall,见 layout-constants.json imageSpec)。
27
- * 页型字段定义(29 种)与 chart.type 白名单(30 类)的唯一事实源:scripts/model-schema.json。
28
- *
29
- * ★ 单源机制(scripts/sync_runtime.py 注入,标记块内禁止手改):
30
- * - 版式常量:scripts/layout-constants.json → __TOPPPT_CONSTANTS__ 块;
31
- * 通道 B(build_pptx.js)require 同一份 JSON,两通道产出一致。
32
- * - DSL schema:scripts/model-schema.json → __TOPPPT_SCHEMA__ 块;
33
- * scripts/extract_model.py 读同一份文件,双端校验同源。
34
- * ★ 三模式独立排版比例尺(MTS)——research 咨询密排 / architecture 全幅图页型;
35
- * modeSize() 把调用点的 presentation 基准 pt 映射到当前模式比例尺。
36
- * ★ 本运行时只做「预览与回归裁判」:图表为形状近似渲染(交付通道 B 用原生数据图表,
37
- * 可在 PowerPoint 中"编辑数据");文本与版式几何两通道同源,cross_verify 逐页比对。
38
- * ═══════════════════════════════════════════════════════════════════════════ */
39
- (function (g) {
40
- 'use strict';
41
- /* 双形态载荷判定:[[t,d]…] 数组 vs [{t,d,accent}…] 记录。typeof [] === 'object'。 */
42
- function isRec(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
43
-
44
- /* __TOPPPT_CONSTANTS_START__ */
45
- /* ── 由 scripts/sync_runtime.py 从 scripts/layout-constants.json 注入 · 禁止手改 ── */
46
- /* MTS = 三模式独立排版比例尺(按模式取 pt;基准 TS_BASE = presentation 档) */
47
- /* PRESETS_DARK = 风格 dark token(REPORT_MODEL.theme="dark" 时导出深色版)*/
48
- /* GRID = 12 列网格;TYPO = 语义字阶 C0–T14;CONT = 容器内边距;REG = 图表登记四元组;DEEP = 深度模式配置 */
49
- /* IMG = 素材图片规格与配图占位约定(版式/裁切/建议尺寸/占位标签,双引擎同源) */
50
- /* STYLE_DATA_COLORS(_DARK) = 9 套风格各自的编码色板 c1–c5(数据系列;结构色纪律不变) */
51
- /* LAYOUT_SLOTS + regionOf = 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */
52
- var PW = 13.333, PH = 7.5, MX = 0.6, CW = PW - 2 * MX, EMU = 914400;
53
- var TS_BASE = {"coverTitle":44,"coverSub":18,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10};
54
- var MTS = {"presentation":{"coverTitle":44,"coverSub":18,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10},"research":{"coverTitle":36,"coverSub":14,"h1":22,"h2":13.5,"lead":11.5,"body":10.5,"caption":9,"micro":8.5},"architecture":{"coverTitle":44,"coverSub":19,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10}};
55
- var PT = {"layout":{"contentTop":2.35,"contentBottom":6.9,"contentBottomWithNote":6.4,"pageNumY":7.0},"common":{"headEyebrowY":0.48,"headTitleY":0.82,"headRuleY":1.72,"leadY":1.88,"bodyY":2.35,"bodyYWithLead":2.55,"pagerY":7.0},"cover":{"metaY":0.7,"titleY":2.4,"titleH":2.0,"subtitleY":4.5,"subtitleH":0.8},"closing":{"eyebrowY":0.7,"titleY":1.2,"titleH":1.2,"pointsY":3.0,"pointTitleH":0.6,"pointBodyH":1.2},"points":{"listY":2.5,"rowH":0.72,"rowHMin":0.5,"markSize":0.14,"metricY":4.7,"metricW":2.6,"metricValH":0.9,"metricCapY":5.6},"metrics":{"bandY":3.1,"cardH":2.3,"valY":3.45,"valH":1.1,"capY":4.55,"capH":0.7,"gap":0.3,"maxW":2.9},"table":{"y":2.5,"rowH":0.55,"rowHMin":0.32,"headRowH":0.5},"bar":{"hbarY0":2.6,"hbarY1":6.25,"chartX":0.6,"chartY":2.95,"chartW":1.2,"chartH":3.35,"noteY":6.55},"note":{"y":6.55,"h":0.4},"exhibit":{"badgeY":2.35,"chartY":2.75,"chartH":3.25,"soWhatY":6.05,"footnoteY":6.72},"twocol":{"colGap":0.5,"bodyH":3.6,"rowHMin":0.5},"hbar":{"labelW":1.9,"valW":1.0,"rowH":0.55,"barRatio":0.5},"diagram":{"layerBarW":1.35,"layerGap":0.15,"nodeGap":0.15,"bodyStartY":2.9,"maxLayerH":1.15,"nodeMaxW":2.2},"split":{"leftW":0.55,"rightW":0.415,"rightX":0.585,"pointsY":2.7,"rowH":0.78,"capY":2.55,"chartY":3.0,"chartH":3.7,"tableY":2.7,"tableRowH":0.5},"cards":{"startY":3.0,"gap":0.25,"maxH":2.0,"titleH":0.4},"timeline":{"labelY":2.95,"axisY":3.58,"dotY":3.51,"dotR":0.18,"nameY":3.9,"descY":4.45,"descH":1.2},"research":{"col3Gap":0.38,"matrixLabelW":1.5,"matrixCellH":0.85,"matrixY":2.2,"denseTableY":2.2,"denseRowH":0.42,"halfTableW":0.52,"halfChartX":0.57,"halfChartW":0.43},"arch":{"fullStartY":2.1,"fullEndY":6.9,"laneH":0.72,"laneHMin":0.5,"laneGap":0.14,"laneHeadW":1.3,"stepGap":0.12,"stepMaxW":1.9},"kpi":{"heroY":2.9,"heroW":0.52,"heroH":1.6,"dividerX":0.56,"metricY0":2.95,"metricRowH":0.85},"comparison":{"panelGap":0.4,"panelY":2.55,"panelH":3.4,"titleH":0.45,"rowH":0.62},"quote":{"markY":1.75,"textY":2.7,"textH":2.3,"authorY":5.25,"contextY":5.8},"donut":{"centerX":3.05,"centerY":4.6,"r":1.6,"holeR":0.95,"legendX":5.6,"legendRowH":0.55,"legendLabelW":3.4,"legendValW":1.3,"legendPctX":5.3,"legendPctW":1.3},"steps":{"y":3.05,"barH":1.35,"gap":0.12,"arrowW":0.3,"numH":0.3,"titleH":0.34,"pad":0.08,"maxPerRow":6},"heatmap":{"y":2.6,"labelW":1.7,"headH":0.42,"cellH":0.6,"cellGap":0.06,"legendW":1.6},"pyramid":{"y":2.35,"rowH":0.72,"gap":0.08,"minW":0.34,"labelW":3.2},"bullet":{"y":2.6,"rowH":0.8,"labelW":2.4,"valW":1.7,"barH":0.26},"image":{"y":2.3,"h":4.2,"halfW":0.56,"halfGap":0.06,"capY":6.6,"capH":0.3,"radius":0.09,"noteRowH":0.62,"gridGap":0.16,"gridCols":{"2":2,"3":3,"4":4,"6":3},"gridCapH":0.28,"compareGap":0.3,"compareTagH":0.26,"wallCellH":0.92,"wallGap":0.18,"wallMaxRows":2},"flagbar":{"hdH":0.26,"rowH":0.24,"maxRows":3,"gap":0.12},"sankey":{"y":2.45,"h":4.1,"nodeW":0.32,"nodeGap":0.14,"labelW":1.55,"minBandH":0.22,"maxNodes":12,"maxFlows":24},"treemap":{"y":2.45,"h":4.1,"gap":0.05,"labelMinH":0.34,"maxLeaves":16,"maxDepth":3},"boxplot":{"y":2.7,"h":3.7,"labelW":1.5,"groupGap":0.3,"boxMaxW":0.9,"whiskerCapW":0.34,"maxGroups":8},"network":{"y":2.4,"h":4.2,"nodeR":0.28,"nodeRMin":0.18,"labelMaxW":1.4,"maxNodes":18,"maxEdges":30},"marimekko":{"y":2.6,"h":3.75,"labelW":1.55,"colGap":0.06,"legendH":0.42,"maxCols":6,"maxSegs":4},"streamgraph":{"y":2.6,"h":3.75,"labelW":1.4,"bandMinH":0.12,"maxSeries":6}};
56
- var PRESETS = {
57
- 'business-blue': { accent:'1A73E8', ink:'202124', body:'5F6368', faint:'80868B', bg:'FFFFFF', surface:'F8F9FA', line:'DADCE0', soft:'E8F0FE', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
58
- 'apple-mono': { accent:'1D1D1F', ink:'1D1D1F', body:'6E6E73', faint:'86868B', bg:'FFFFFF', surface:'F5F5F7', line:'D2D2D7', soft:'F5F5F7', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
59
- 'mckinsey': { accent:'003A70', ink:'101828', body:'475467', faint:'667085', bg:'FFFFFF', surface:'F2F4F7', line:'D0D5DD', soft:'EEF4FB', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
60
- 'brand-red': { accent:'D0021B', ink:'1F1A1B', body:'5D5456', faint:'8A8083', bg:'FFFFFF', surface:'FAF7F7', line:'E2D9DA', soft:'FBECEC', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
61
- 'warm-sand': { accent:'96681F', ink:'2A2521', body:'6B5F52', faint:'93836F', bg:'FBF9F6', surface:'F6F2EC', line:'E0D7C9', soft:'F3ECDD', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
62
- 'deep-teal': { accent:'0F6B5C', ink:'14201E', body:'4A5A57', faint:'73837F', bg:'FFFFFF', surface:'F2F6F5', line:'CFDEDC', soft:'E4F1EE', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
63
- 'graphite-dark': { accent:'0369A1', ink:'1A1D21', body:'4B5158', faint:'767C84', bg:'F4F5F7', surface:'ECEEF1', line:'CED1D6', soft:'E2EFF7', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
64
- 'indigo-violet': { accent:'5B5BD6', ink:'1B1A2E', body:'52506B', faint:'7A779A', bg:'FFFFFF', surface:'F4F4FB', line:'D7D7E8', soft:'ECECF9', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
65
- 'spectrum': { accent:'4563EF', ink:'171A21', body:'4D5566', faint:'7B8494', bg:'FBFCFE', surface:'F4F6FA', line:'D6DCE6', soft:'ECEEFF', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' }
66
- };
67
- var PRESETS_DARK = {
68
- 'business-blue': { accent:'8AB4F8', ink:'E8EAED', body:'ADB4BD', faint:'868D96', bg:'0D0F13', surface:'191D23', line:'2F353D', soft:'16243A', onAccent:'0D1B2E', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
69
- 'apple-mono': { accent:'F5F5F7', ink:'F5F5F7', body:'A1A1A6', faint:'86868B', bg:'000000', surface:'161617', line:'38383D', soft:'1D1D1F', onAccent:'000000', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
70
- 'mckinsey': { accent:'7FB2E5', ink:'E4E7EC', body:'AAB3C2', faint:'8A94A6', bg:'0C1116', surface:'12181F', line:'2A3342', soft:'12233A', onAccent:'0C1A2E', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
71
- 'brand-red': { accent:'FF5A66', ink:'F3ECEC', body:'C4B6B7', faint:'9A8D8E', bg:'130D0E', surface:'1A1213', line:'37272A', soft:'331518', onAccent:'2A0D10', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
72
- 'warm-sand': { accent:'D9B36A', ink:'ECE5DB', body:'C4B8A8', faint:'9B8D7A', bg:'15120E', surface:'1D1913', line:'3A3128', soft:'2E2417', onAccent:'2A1F10', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
73
- 'deep-teal': { accent:'4FC3B0', ink:'E2ECEA', body:'B3C2BF', faint:'86948F', bg:'0D1312', surface:'131B1A', line:'293834', soft:'12312C', onAccent:'0C2420', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
74
- 'graphite-dark': { accent:'4DA3FF', ink:'EEF2F6', body:'B9C2CD', faint:'85909D', bg:'0B0E12', surface:'171D24', line:'2B3540', soft:'132841', onAccent:'07182B', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
75
- 'indigo-violet': { accent:'A5A4F0', ink:'E6E5F5', body:'B6B4D4', faint:'8B89B0', bg:'100F1C', surface:'171629', line:'322F52', soft:'232146', onAccent:'171631', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
76
- 'spectrum': { accent:'8BA0F8', ink:'E8EBF2', body:'A7AFBF', faint:'7E8798', bg:'0E1116', surface:'171C26', line:'2B3342', soft:'1B2242', onAccent:'141A38', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' }
77
- };
78
- var STYLE_DATA_COLORS = {"business-blue":["1a73e8","3526bb","21a199","b77148","99983d"],"apple-mono":["1d1d1f","0071e3","0f7b6c","c2410c","7c3aed"],"mckinsey":["003a70","2826bb","21a18e","b76848","99903d"],"brand-red":["d0021b","bb6326","d634ae","40a182","438aa8"],"warm-sand":["96681f","839324","cf3b3e","4d88b2","4a47a4"],"deep-teal":["0f6b5c","2683bb","22aa3e","b74870","a85c43"],"graphite-dark":["0369a1","2637bb","1f9977","b75948","a18a40"],"indigo-violet":["5b5bd6","7432ae","4197c8","a08f4b","7ca04b"],"spectrum":["4563ef","5b26bb","269ebb","af8746","83993d"]};
79
- var STYLE_DATA_COLORS_DARK = {"business-blue":["8ab4f8","7966e0","88e7e5","d4ad91","cbce83"],"apple-mono":["f5f5f7","2997ff","2dd4bf","ff9f0a","bf5af2"],"mckinsey":["7fb2e5","6f6bdb","8ce3d8","d1a694","cbc586"],"brand-red":["ff5a66","e09e66","e788cb","91d4c2","83b4ce"],"warm-sand":["d9b36a","c2d571","df9391","97b5ce","8e89c7"],"deep-teal":["4fc3b0","7aaecd","97d8a4","c99cac","c29c8f"],"graphite-dark":["4da3ff","6c66e0","88e7dc","d4a691","cec983"],"indigo-violet":["a5a4f0","a967e0","89c3e7","d3c792","aece83"],"spectrum":["8ba0f8","9066e0","88d6e7","d4ba91","bdce83"]};
80
- var GRID = {"columns":12,"gutter":0.16,"colW":0.8644,"x":[0.6,1.6244,2.6488,3.6732,4.6976,5.722,6.7464,7.7708,8.7952,9.8196,10.844,11.8684,12.733],"y":[2.35,3.27,4.19,5.11,6.03,6.9],"safe":{"top":0.4,"bottom":0.4,"left":0.6,"right":0.6}};
81
- var TYPO = {"levels":[{"id":"C0","name":"封面/章节幕标题","role":"coverTitle","usage":"封面主标题、章节幕标题"},{"id":"T1","name":"页码/章节徽章","role":"caption","usage":"页码徽章、章节编号"},{"id":"T2","name":"页面主标题/结论标题","role":"h1","usage":"每页顶部结论句(research 行动标题)"},{"id":"T3","name":"副标题/语境说明","role":"lead","usage":"主标题下方导语"},{"id":"T4","name":"模块标题/图表标题","role":"h2","usage":"卡片标题、图表标题、面板标题"},{"id":"T5","name":"证据编号/轻量标签","role":"micro","usage":"Exhibit 编号、状态标签"},{"id":"T6","name":"证据块标题/小节标题","role":"h2","usage":"分栏小标题、证据块标题"},{"id":"T7","name":"正文解释段落","role":"body","usage":"证据解释、管理解读正文、表格正文"},{"id":"T8","name":"结论条文字","role":"body","usage":"so-what 结论条、核心结论框"},{"id":"T9","name":"SO WHAT 标签","role":"caption","usage":"so-what 标签行"},{"id":"T10","name":"SO WHAT 正文/业务含义","role":"body","usage":"so-what 正文、行动含义、表格行动项"},{"id":"T11","name":"图表轴/图例/刻度/微标签","role":"micro","usage":"坐标轴、图例、刻度、单位(禁用于表格正文与完整短句)"},{"id":"T12","name":"图表数据标签","role":"caption","usage":"折线点值、柱形标签、百分比标注"},{"id":"T13","name":"关键 KPI 大数字","role":"h1","usage":"KPI 大数字、hero 数值"},{"id":"T14","name":"注释/口径/来源/页脚","role":"micro","usage":"footnote、来源行、页脚、小页码"}],"tableSemanticRule":{"body":["T7","T10"],"microAllowed":["T11"],"microForbiddenFor":["表格正文","行动项","风险项","解释句","建议句","长项目符号","完整短句"]}};
82
- var CONT = {"pad":{"card":0.18,"panel":0.2,"cell":0.08,"band":0.16,"soWhat":0.16,"kpi":0.18,"chart":0.1,"head":0.05,"footer":0.05,"node":0.06},"minPad":0.06,"textMetrics":{"lineFactor":1.45,"emAsciiRatio":0.52},"overflowRule":"溢出处置优先级:① 优化内容(列表化/精炼/合并/拆页/换组合版式)→ ② 调整容器尺寸与行宽 → ③ 有限度缩小字号(见 fontShrink)→ ④ 仍装不下则拆页。禁止无限缩字号硬塞,也禁止把「精简」当第一手段砍掉决策必需信息。","fontShrink":{"ladder":[15,14,13.5,13,12.5,12,11.5,11,10.5,10,9.5,9,8.5],"maxShrinkSteps":4,"floorPt":{"presentation":10,"research":9,"architecture":10},"preferContentFirst":true,"warnBelow":9.5},"continuousTextRule":"语义连续的一句话 / so-what 主句 / 结论句 / 表格单元格正文不得拆成多个独立文本框(避免异常空格、断句、基线漂移);局部加粗高亮必须用同一文本框内富文本。"};
83
- var REG = {"bar":{"html":"svg-bars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160}},"hbar":{"html":"svg-hbars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":100}},"stack":{"html":"svg-stack","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160},"nativeOpt":{"barGrouping":"stacked"}},"stackline":{"html":"svg-stackline","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":40},"nativeOpt":{"barGrouping":"percentStacked"}},"line":{"html":"svg-line","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"dualline":{"html":"svg-dualline","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"area":{"html":"svg-area","pptx":"native","nativeType":"area","dataTable":"notes","min":{"vbHeightMin":160}},"donut":{"html":"svg-donut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"multidonut":{"html":"svg-mdonut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"pie":{"html":"svg-pie","pptx":"native","nativeType":"pie","dataTable":"notes","min":{"pxWidthMin":140}},"radar":{"html":"svg-radar","pptx":"native","nativeType":"radar","dataTable":"notes","min":{"pxWidthMin":220}},"scatter":{"html":"svg-scatter","pptx":"native","nativeType":"scatter","dataTable":"notes","min":{"vbHeightMin":180}},"bubble":{"html":"svg-bubble","pptx":"native","nativeType":"bubble","dataTable":"notes","min":{"vbHeightMin":180}},"waterfall":{"html":"svg-wf","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":170},"nativeTrick":"stacked+hiddenBase+connector"},"gauge":{"html":"svg-gauge","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":180},"nativeTrick":"doughnut+firstSliceAng+hiddenRemainder"},"pareto":{"html":"svg-pareto","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":180},"nativeTrick":"multiType+bar+line"},"gantt":{"html":"svg-gantt","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":130},"path":{"forbidPresetShapes":true}},"vsbar":{"html":"svg-vsbar","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"progress":{"html":"svg-progress","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"sparkline":{"html":"svg-spark","pptx":"shape","dataTable":"off","min":{"vbHeightMin":20},"path":{"forbidPresetShapes":true}},"funnel":{"html":"svg-funnel","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"minSamplePoints":8,"forbidPresetShapes":true}},"treemap":{"html":"svg-treemap","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":170},"path":{"forbidPresetShapes":true}},"marimekko":{"html":"svg-marimekko","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"boxplot":{"html":"svg-boxplot","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"network":{"html":"svg-network","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"forbidPresetShapes":true}},"sankey":{"html":"svg-sankey","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"minSamplePoints":16,"trackBothEdges":true,"forbidPresetShapes":true}},"streamgraph":{"html":"svg-stream","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"minSamplePoints":24,"trackBothEdges":true,"forbidPresetShapes":true}},"slope":{"html":"svg-slope","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"dumbbell":{"html":"svg-dumbbell","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"lollipop":{"html":"svg-lollipop","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"dotplot":{"html":"svg-dotplot","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":120},"path":{"forbidPresetShapes":true}},"bulletchart":{"html":"svg-bullet","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"waffle":{"html":"svg-waffle","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"radialbar":{"html":"svg-radialbar","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":200},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"rose":{"html":"svg-rose","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":180},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"candlestick":{"html":"svg-candle","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}}};
84
- var DEEP = {"triggers":["高保真","1:1","一比一","精确还原","按图还原","正式交付","不能偏移","像素级","严格对照","逐页验收"],"complexCharts":["sankey","streamgraph","treemap","marimekko","boxplot","network","rose","radialbar","candlestick","funnel"],"checks":{"anchors":true,"containerOverflow":true,"renderCompare":true,"manifest":true},"renderCompare":{"engine":"soffice","fallback":"powerpoint-com","skipIfMissing":true,"dpi":96}};
85
- var IMG = {"layouts":["full","half","bleed","grid","compare","wall"],"multiLayouts":["grid","compare","wall"],"fitEnum":["cover","contain"],"fitDefault":"cover","formats":["png","jpeg","jpg","webp","svg"],"ratioDefault":{"full":"3:1","half":"4:3","bleed":"21:9","grid":"4:3","compare":"4:3","wall":"1:1"},"ratioCssClass":{"full":"media--r3-1","half":"media--r4-3","bleed":"media--r21-9","grid":"media--r4-3","compare":"media--r4-3","wall":"media--r1-1"},"recommendedSizePx":{"full":"2400×800","half":"1400×1050","bleed":"2560×1100","grid":"900×675","compare":"1400×1050","wall":"480×480"},"maxInlineBytes":1572864,"maxTotalInlineBytes":8388608,"maxPerPage":6,"placeholderLabel":"配图占位","placeholderHintPrefix":"建议 ","placeholderHintSuffix":"px","zeroExternal":true,"qualityRule":"位图优先提供 ≥2× 显示尺寸;截图用 16:9 或 4:3 锁定比例;数据图不要用截图(应用内联 SVG 图表,可随主题变色)。"};
86
- var LAYOUT_SLOTS = {"version":"9.1","pageTypes":{"points":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".grid.g-2 / .card / .ul","pptx":"points columns","minUnits":1},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar / footnoteLine"}]},"metrics":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".metric","pptx":"metric row","minUnits":4},{"id":"annotation","role":"annotation","required":false,"html":"口径行 / .sowhat","pptx":"footnoteLine / soWhatBar"}]},"kpi":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"hero","role":"primary","required":true,"html":".t-metric hero","pptx":"hero number"},{"id":"metrics","role":"secondary","required":false,"html":".metric","pptx":"support metrics"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar"}]},"exhibit":{"mode":["research","presentation"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead / .exhibit__hd","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart] / .exhibit","pptx":"nativeChart | shapeChart","channel":"registry"},{"id":"secondary","role":"secondary","required":false,"html":"要点列表","pptx":"side bullets"},{"id":"annotation","role":"annotation","required":true,"html":".exhibit__src / .sowhat","pptx":"footnoteLine / soWhatBar / notes"}]},"split":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"left","role":"primary","required":true,"html":"左区(points/table/image/chart)","pptx":"left zone"},{"id":"right","role":"secondary","required":true,"html":"右区(points/table/image/chart)","pptx":"right zone"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar"}]},"table":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".tbl-wrap table","pptx":"addTable","maxRows":{"presentation":8,"research":16}},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar / footnoteLine"}]},"bar":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart]","pptx":"nativeChart | shapeChart","channel":"registry"},{"id":"annotation","role":"annotation","required":true,"html":"note / dataTable","pptx":"footnoteLine / notes / inline table"}]},"donut":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=donut]","pptx":"addChart doughnut"},{"id":"annotation","role":"annotation","required":false,"html":"中心合计 / 图例 / .note","pptx":"center label / legend / notes"}]},"twocol":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".cols-2","pptx":"two text columns"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"threecol":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".cols-3","pptx":"three text columns"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"halftable":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"左表","pptx":"addTable left"},{"id":"secondary","role":"secondary","required":true,"html":"右图","pptx":"chart right"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"matrix":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".matrix","pptx":"matrix cells"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"comparison":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"left","role":"primary","required":true,"html":"左面板","pptx":"left panel"},{"id":"right","role":"secondary","required":true,"html":"右面板","pptx":"right panel"},{"id":"annotation","role":"annotation","required":false,"html":"verdict / .sowhat","pptx":"verdict bar"}]},"quote":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"金句正文","pptx":"quote text"}]},"timeline":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".tl","pptx":"timeline axis"}]},"steps":{"mode":["presentation","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".steps","pptx":"step row"}]},"cards":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".card grid","pptx":"card grid"}]},"heatmap":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".heat","pptx":"heat cells"}]},"bullet":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".bul","pptx":"bullet rows"}]},"pyramid":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".pyr","pptx":"pyramid layers"}]},"image":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".media*","pptx":"imageLayoutShapes"}]},"diagram":{"mode":["architecture","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".arch / layers","pptx":"layered nodes + edges","maxNodes":24,"maxLayers":4},{"id":"annotation","role":"annotation","required":false,"html":".arch__legend / .note","pptx":"legend chips / notes"}]},"lane":{"mode":["architecture","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".lane","pptx":"lanes + steps"}]},"sankey":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=sankey]","pptx":"infoSankey","maxNodes":12},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"treemap":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=treemap]","pptx":"infoTreemap"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"boxplot":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=boxplot]","pptx":"infoBoxplot"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"network":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=network]","pptx":"infoNetwork"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"marimekko":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=marimekko]","pptx":"infoMarimekko"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"streamgraph":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=streamgraph]","pptx":"infoStreamgraph"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]}}};
87
-
88
- /* regionOf(pageType, slotId, opts) — 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */
89
- function regionOf(pageType, slotId, opts) {
90
- var type = String(pageType || 'points'), slot = String(slotId || '');
91
- opts = opts || {};
92
- var L = PT.layout || {}, C = PT.common || {};
93
- var topDefault = C.bodyY || 2.5;
94
- var bottomDefault = opts.withNote ? (L.contentBottomWithNote || 6.4) : (L.contentBottom || 6.9);
95
- if (slot === 'head' || slot === 'chrome') {
96
- return { x: MX, y: C.headEyebrowY || 0.55, w: CW,
97
- h: Math.max(0.4, (C.headRuleY || 1.95) - (C.headEyebrowY || 0.55)),
98
- titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY };
99
- }
100
- if (slot === 'annotation' || slot === 'soWhat') {
101
- var sy = (PT.exhibit && PT.exhibit.soWhatY != null) ? PT.exhibit.soWhatY : 6.05;
102
- return { x: MX, y: sy, w: CW, h: 0.62 };
103
- }
104
- if (slot === 'footnote') {
105
- var fy = (PT.exhibit && PT.exhibit.footnoteY != null) ? PT.exhibit.footnoteY : ((PT.note && PT.note.y) || 6.55);
106
- return { x: MX, y: fy, w: CW - 1.2, h: (PT.note && PT.note.h) || 0.32 };
107
- }
108
- var P = PT[type] || {};
109
- if (type === 'exhibit' && slot === 'primary') {
110
- var ey = P.chartY || 2.95;
111
- return { x: MX, y: ey, w: CW, h: Math.max(1.2, (L.contentBottomWithNote || 6.4) - ey), badgeY: P.badgeY };
112
- }
113
- if (type === 'metrics' && slot === 'primary') {
114
- return { x: MX, y: P.bandY || 3.1, w: CW, h: P.cardH || 2.3, gap: P.gap, maxW: P.maxW,
115
- valY: P.valY, valH: P.valH, capY: P.capY, capH: P.capH };
116
- }
117
- if (type === 'table' && slot === 'primary') {
118
- var ty = P.y || topDefault;
119
- return { x: MX, y: ty, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - ty) };
120
- }
121
- if (type === 'bar' && slot === 'primary') {
122
- var by = P.chartY || 2.95;
123
- var bb = opts.bottom != null ? opts.bottom : bottomDefault;
124
- return { x: MX + (P.chartX || 0), y: by, w: CW - (P.chartW || 0), h: Math.max(1.2, bb - by) };
125
- }
126
- if (type === 'bar' && slot === 'hbar') {
127
- var y0 = P.hbarY0 || 2.6;
128
- var y1 = opts.bottom != null ? opts.bottom : (P.hbarY1 || 6.25);
129
- return { x: MX, y: y0, w: CW, h: Math.max(1.2, y1 - y0) };
130
- }
131
- if (type === 'points' && (slot === 'primary' || slot === 'list')) {
132
- var ly = P.listY || topDefault;
133
- return { x: MX, y: ly, w: CW, h: Math.max(0.8, (opts.bottom != null ? opts.bottom : bottomDefault) - ly) };
134
- }
135
- if (type === 'split' && (slot === 'left' || slot === 'right')) {
136
- var st = P.chartY || topDefault;
137
- var sb = opts.bottom != null ? opts.bottom : bottomDefault;
138
- if (slot === 'left') return { x: MX, y: st, w: CW * (P.leftW || 0.55), h: Math.max(1.2, sb - st) };
139
- return { x: MX + CW * (P.rightX != null ? P.rightX : 0.585), y: st, w: CW * (P.rightW || 0.415), h: Math.max(1.2, sb - st) };
140
- }
141
- if (type === 'twocol' && (slot === 'primary' || slot === 'left' || slot === 'right')) {
142
- var gap = P.colGap || 0.5, colW = (CW - gap) / 2;
143
- var tt = opts.top != null ? opts.top : topDefault;
144
- var tb = opts.bottom != null ? opts.bottom : bottomDefault;
145
- if (slot === 'right') return { x: MX + colW + gap, y: tt, w: colW, h: Math.max(1, tb - tt) };
146
- return { x: MX, y: tt, w: colW, h: Math.max(1, tb - tt) };
147
- }
148
- if ((type === 'diagram' || type === 'lane') && slot === 'primary') {
149
- var arch = PT.arch || {};
150
- var dy = (opts.mode === 'architecture' && arch.fullStartY != null) ? arch.fullStartY : (P.bodyStartY || 2.9);
151
- var db = (opts.mode === 'architecture' && arch.fullEndY != null) ? arch.fullEndY
152
- : (opts.bottom != null ? opts.bottom : bottomDefault);
153
- return { x: MX, y: dy, w: CW, h: Math.max(1.2, db - dy) };
154
- }
155
- if (type === 'kpi' && (slot === 'hero' || slot === 'primary')) {
156
- return { x: MX, y: P.heroY || 2.9, w: CW * (P.heroW || 0.52), h: P.heroH || 1.6,
157
- dividerX: P.dividerX, metricY0: P.metricY0, metricRowH: P.metricRowH };
158
- }
159
- if (type === 'comparison' && (slot === 'left' || slot === 'primary' || slot === 'right')) {
160
- var cg = P.panelGap || 0.4, pw = (CW - cg) / 2;
161
- if (slot === 'right') return { x: MX + pw + cg, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
162
- return { x: MX, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
163
- }
164
- if (type === 'quote' && slot === 'primary') {
165
- return { x: MX + 1.05, y: P.textY || 2.7, w: CW - 2.1, h: P.textH || 2.3 };
166
- }
167
- if (type === 'heatmap' && slot === 'primary') {
168
- var hy = P.y || 2.6;
169
- return { x: MX, y: hy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - hy) };
170
- }
171
- if (type === 'bullet' && slot === 'primary') {
172
- var uy = P.y || 2.6;
173
- return { x: MX, y: uy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - uy) };
174
- }
175
- if (type === 'pyramid' && slot === 'primary') {
176
- var py2 = P.y || 2.35;
177
- return { x: MX, y: py2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - py2) };
178
- }
179
- if (type === 'steps' && slot === 'primary') {
180
- return { x: MX, y: P.y || 3.05, w: CW, h: P.barH || 1.35 };
181
- }
182
- if (type === 'cards' && slot === 'primary') {
183
- var cy2 = P.startY || 3.0;
184
- return { x: MX, y: cy2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - cy2) };
185
- }
186
- if (type === 'timeline' && slot === 'primary') {
187
- return { x: MX, y: P.labelY || 2.95, w: CW, h: Math.max(1, bottomDefault - (P.labelY || 2.95)) };
188
- }
189
- if (type === 'image' && slot === 'primary') {
190
- var iy2 = Math.max(P.y || 2.3, opts.top || 0);
191
- return { x: MX, y: iy2, w: CW, h: Math.max(1.2, Math.min(P.h || 4.2, (opts.bottom != null ? opts.bottom : bottomDefault) - iy2)) };
192
- }
193
- if ((type === 'halftable') && (slot === 'left' || slot === 'primary')) {
194
- var rs = PT.research || {};
195
- return { x: MX, y: topDefault, w: CW * (rs.halfTableW || 0.52), h: Math.max(1, bottomDefault - topDefault) };
196
- }
197
- if ((type === 'halftable') && (slot === 'right' || slot === 'secondary')) {
198
- var rs2 = PT.research || {};
199
- return { x: MX + CW * (rs2.halfChartX || 0.57), y: topDefault, w: CW * (rs2.halfChartW || 0.43), h: Math.max(1, bottomDefault - topDefault) };
200
- }
201
- if (type === 'matrix' && slot === 'primary') {
202
- var rm = PT.research || {};
203
- var my = rm.matrixY || 2.3;
204
- return { x: MX, y: my, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - my) };
205
- }
206
- var INFO = { sankey:1, treemap:1, boxplot:1, network:1, marimekko:1, streamgraph:1 };
207
- if (INFO[type] && slot === 'primary') {
208
- var iy3 = P.y || topDefault;
209
- var ih3 = Math.max(1.2, Math.min(P.h || 4.1, (opts.bottom != null ? opts.bottom : bottomDefault) - iy3));
210
- return { x: MX, y: iy3, w: CW, h: ih3 };
211
- }
212
- return { x: MX, y: topDefault, w: CW, h: Math.max(0.8, bottomDefault - topDefault) };
213
- }
214
-
215
- /* __TOPPPT_CONSTANTS_END__ */
216
-
217
- /* __TOPPPT_SCHEMA_START__ */
218
- /* ── 由 scripts/sync_runtime.py 从 scripts/model-schema.json 注入 · 禁止手改 ── */
219
- /* DSL schema 单源:浏览器端 validateModel 与 scripts/extract_model.py 消费同一份定义 */
220
- var MODEL_SCHEMA = {"version":"9.1","modes":["presentation","research","architecture"],"model":{"required":["title","sections:array","closing.title","closing.points:array"],"optional":["mode","style","theme","subtitle","meta","agenda:array"],"agendaMin":{"presentation":3,"research":3,"architecture":0},"sectionsMin":1,"sectionsRecommended":3,"agendaComfortMax":16},"commonSectionFields":["eyebrow","title","lead","soWhat","footnote","flags","image","exhibitNo","layoutPreset"],"flagsHint":"flags = 待核实/待二次修改条目字符串数组(如 '2027 增速 xx%(口径未定)');渲染为 accent 强调色标注行,提示用户核对。","imageHint":"image = 素材图片/配图占位对象。三选一必填:src(用户图,data: 内联或相对路径,零外链铁律禁 http(s))|items(多图版式:grid/compare/wall,元素 {src?, alt?, caption?, placeholder?})|placeholder:true(无图时出配图占位,原生形状渲染、pictures 不增)。其余可选:layout(full/half/bleed/grid/compare/wall,见 layout-constants.json imageSpec.layouts)、fit(cover 默认 / contain)、caption、alt、hint、ratio。","imageLayoutHint":"layout 与 HTML 版式对应:full=版心全宽图|half=左图右注(points 为右栏注解)|bleed=通栏出血|grid=多图网格(items 2/3/4/6 张)|compare=双图 A/B 对比(items 2 张)|wall=Logo 墙(items 3–12 张小图)。占位符标签文本由双引擎按 imageSpec 拼同一串,保证 A/B 通道逐页文本一致。","chartTypes":["bar","hbar","stack","stackline","line","dualline","area","donut","multidonut","pie","radar","scatter","bubble","waterfall","gauge","pareto","funnel","gantt","vsbar","progress","sparkline","slope","dumbbell","lollipop","dotplot","bulletchart","waffle","radialbar","rose","candlestick"],"chartTypesHint":"chart.type 可取值 = 本表(30 类:16 原生 + 14 形状还原)。其余 6 类复杂信息图(sankey/treemap/boxplot/network/marimekko/streamgraph)是**专属页型**而非 chart.type——它们要按列/段/节点等结构化载荷表达,故走 sections[].type。不变量:charts.registry.types == chartTypes ∪ 上述 6 类页型(sync_runtime.py 校验)。","chartDataTable":{"enum":["notes","inline","appendix","off"],"default":"notes","rule":"非原生图表(registry.pptx=shape)的 dataTable 不得为 off;缺失时按 registry 默认值补齐。"},"pageTypes":{"points":{"label":"要点列表页","modes":["presentation","research","architecture"],"required":["points:array"],"optional":["metrics:array"]},"metrics":{"label":"指标带页(4–6 个核心数字)","modes":["presentation","research"],"required":["metrics:array"]},"kpi":{"label":"大数指标页(hero 大数字 + 支撑指标行)","modes":["presentation","research"],"required":["hero:array"],"optional":["metrics:array"]},"table":{"label":"对比表页","modes":["presentation","research","architecture"],"required":["table.head:array","table.rows:array"],"optional":["table.colW:array"]},"timeline":{"label":"时间线 / 路线页","modes":["presentation","research"],"required":["phases:array"]},"steps":{"label":"步骤条页(N 步横排 + 箭头,可分组)","modes":["presentation","research","architecture"],"required":["steps:array"],"optional":["groups:array","note"]},"bar":{"label":"图表页(chart.type 见 chartTypes;native 通道走原生数据图表,shape 通道走高保真形状还原 + 数据表)","modes":["presentation","research"],"required":["chart.labels:array","chart.values:array"],"optional":["chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.start:array","chart.target:array","chart.dataTable","note"]},"donut":{"label":"环形图页(原生 pie 楔形 + 图例)","modes":["presentation","research"],"required":["chart.labels:array","chart.values:array"],"optional":["chart.unit","chart.centerLabel","chart.colors:array","chart.dataTable","note"]},"heatmap":{"label":"热力矩阵页(行 × 列 + 强度色阶)","modes":["presentation","research"],"required":["rowHeads:array","colHeads:array","cells:array"],"optional":["unit","scaleLabel:array","note"]},"bullet":{"label":"达成对比页(实际 vs 目标条)","modes":["presentation","research"],"required":["items:array"],"optional":["unit","max","note"]},"pyramid":{"label":"金字塔页(层级递进,顶层最窄)","modes":["presentation","research","architecture"],"required":["levels:array"],"optional":["note"]},"image":{"label":"素材图片页(full/half/bleed/grid/compare/wall,效果与位置锁定;支持配图占位)","modes":["presentation","research","architecture"],"required":["anyof:image.src:str|image.items:array|image.placeholder"],"optional":["image.caption","image.alt","image.layout","image.fit","image.ratio","image.hint","points:array","note"]},"cards":{"label":"卡片网格页(cards 元素须为 {title, points:[[k,v]…]},不接受元组)","modes":["presentation","research"],"required":["cards:array"],"optional":["columns"]},"split":{"label":"双区组合页(left / right 各可为 要点|图表|表格|图片:type = points(默认)| table | image | 图表类型名;图表/表格字段直接挂在对应侧)","modes":["presentation","research"],"required":["anyof:left.points:array|left.type:str"],"optional":["left.cap","left.image","left.labels:array","left.values:array","left.series:array","left.points:array","left.max","left.unit","left.colors:array","left.dataTable","left.head:array","left.rows:array","left.colW:array","right.type","right.cap","right.image","right.labels:array","right.values:array","right.series:array","right.points:array","right.max","right.unit","right.colors:array","right.dataTable","right.head:array","right.rows:array","right.colW:array"]},"comparison":{"label":"对比页(左右双栏 + 可选结论条)","modes":["presentation","research"],"required":["left.title","left.points:array","right.title","right.points:array"],"optional":["verdict"]},"quote":{"label":"引用 / 金句页(深色全幅)","modes":["presentation","research"],"required":["quote","author"],"optional":["context"]},"diagram":{"label":"分层架构页(architecture 模式走全幅几何)","modes":["presentation","research","architecture"],"required":["layers:array"],"optional":["legend:array"]},"exhibit":{"label":"Exhibit 编号图表页(research R2)","modes":["research"],"required":["chart.labels:array","chart.values:array"],"optional":["exhibitNo","chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.dataTable","note"]},"twocol":{"label":"双栏论证页(research R1)","modes":["research"],"required":["paragraphs:array"]},"threecol":{"label":"三栏证据页(research R6)","modes":["research"],"required":["paragraphs:array"]},"halftable":{"label":"半表半图页(research R7)","modes":["research"],"required":["table.head:array","table.rows:array","chart.labels:array","chart.values:array"],"optional":["table.colW:array","chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.dataTable","note"]},"matrix":{"label":"矩阵图页(research R8)","modes":["research"],"required":["rowHeads:array","colHeads:array","cells:array"]},"lane":{"label":"泳道页(architecture A2)","modes":["architecture"],"required":["lanes:array"]},"sankey":{"label":"桑基图页(节点-流带,流向与流量)","modes":["research","architecture"],"required":["flows:array"],"optional":["unit","chart.dataTable","note"]},"treemap":{"label":"树图页(面积编码的层级构成)","modes":["research","architecture"],"required":["items:array"],"optional":["unit","chart.dataTable","note"]},"boxplot":{"label":"箱线图页(分布对比:min/q1/median/q3/max)","modes":["research","architecture"],"required":["groups:array"],"optional":["unit","chart.dataTable","note"]},"network":{"label":"关系网络页(节点-边拓扑)","modes":["research","architecture"],"required":["nodes:array","edges:array"],"optional":["chart.dataTable","note"]},"marimekko":{"label":"马赛克图页(列宽 × 列高双重编码)","modes":["research","architecture"],"required":["cols:array","cells:array"],"optional":["unit","chart.dataTable","note"]},"streamgraph":{"label":"流带图页(时间上的构成演变)","modes":["research","architecture"],"required":["series:array"],"optional":["labels:array","chart.dataTable","note"]}}};
221
- /* __TOPPPT_SCHEMA_END__ */
222
-
223
- var _mode = 'presentation'; /* 当前模式(slidesOf 里按 model.mode 设置) */
224
- /* 三模式独立排版比例尺:调用点给 presentation 基准 pt → 取最接近的 TS_BASE 角色 →
225
- 查 MTS[mode][role]。research 是咨询密排比例尺(正文 10.5pt),不再是简单 ×0.8。
226
- 未知模式回落 presentation 档(MTS 与 mode 取值同源于 model-schema.json 的 modes)。 */
227
- function modeSize(base) {
228
- var mts = (typeof MTS !== 'undefined' && MTS) ? (MTS[_mode] || MTS.presentation) : null;
229
- if (!mts) return base;
230
- var best = null, bd = 1e9;
231
- for (var k in TS_BASE) {
232
- if (!TS_BASE.hasOwnProperty(k)) continue;
233
- var d = Math.abs(TS_BASE[k] - base);
234
- if (d < bd) { bd = d; best = mts[k]; }
235
- }
236
- /* 钳制与取整必须与 build_pptx.js 的 sz() 逐字一致——同一张表配不同后处理 = 两通道字号漂移 */
237
- return Math.max(8.5, Math.round((best == null ? base : best) * 2) / 2);
238
- }
239
- /* 编码色板:9 套风格**各有自己的** c1–c5(单源 styleDataColors / styleDataColorsDark)。
240
- 与 HTML 侧的 .f-c1~c5 语义类同源;结构色纪律不受影响——本表只用于数据系列。 */
241
- function dataColors(style, theme) {
242
- var dark = (theme === 'dark');
243
- var map = (dark && typeof STYLE_DATA_COLORS_DARK !== 'undefined' && STYLE_DATA_COLORS_DARK)
244
- ? STYLE_DATA_COLORS_DARK
245
- : (typeof STYLE_DATA_COLORS !== 'undefined' ? STYLE_DATA_COLORS : null);
246
- if (!map) return null;
247
- var c = map[style];
248
- return (c && c.length) ? c : null; // null = 用 S 的中性色(在调用处映射)
249
- }
250
-
251
- /* ── 版式安全边界(与 build_pptx.js 同一常量单源) ── */
252
- var LAY = PT.layout || {};
253
- var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
254
- var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
255
-
256
- /* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
257
- function estLines(text, widthIn, fontPt) {
258
- var usable = Math.max(24, widthIn * 72 - 8), w = 0, lines = 1;
259
- var s = String(text == null ? '' : text);
260
- for (var i = 0; i < s.length; i++) {
261
- if (s[i] === '\n') { lines++; w = 0; continue; }
262
- var cw = /[\x00-\xff]/.test(s[i]) ? fontPt * 0.52 : fontPt;
263
- if (w + cw > usable) { lines++; w = cw; } else { w += cw; }
264
- }
265
- return lines;
266
- }
267
- function estTextH(text, widthIn, fontPt, lineFactor) {
268
- return estLines(text, widthIn, fontPt) * fontPt * (lineFactor || 1.45) / 72;
269
- }
270
- var FONT_LADDER = [15, 14, 13.5, 13, 12.5, 12, 11.5, 11, 10.5, 10, 9.5, 9, 8.5];
271
- /* 有限缩字号(v8.2,与 build_pptx.js 同策略):先优化内容/组合/拆页,最后才缩字号。
272
- maxShrinkSteps 限制下探步数;floorPt 按模式绝对下限。 */
273
- function modeFloorPtA(mode) {
274
- var floors = (typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.floorPt) ||
275
- { presentation: 10, research: 9, architecture: 10 };
276
- var m = String(mode || 'presentation');
277
- return (floors[m] != null) ? floors[m] : 10;
278
- }
279
- function fitFont(items, widthIn, availIn, opts) {
280
- opts = opts || {};
281
- var max = (opts.max == null) ? 99 : opts.max, lf = opts.lineFactor || 1.45;
282
- var gapF = (opts.gapFactor == null) ? 0.5 : opts.gapFactor;
283
- var mode = (typeof CONTENT !== 'undefined' && CONTENT && CONTENT.mode) ||
284
- (typeof TOPPPT_MODE !== 'undefined' ? TOPPPT_MODE : 'presentation');
285
- var floor = (opts.floor != null) ? opts.floor : modeFloorPtA(mode);
286
- var maxSteps = (opts.maxShrinkSteps != null) ? opts.maxShrinkSteps
287
- : ((typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.maxShrinkSteps != null)
288
- ? CONT.fontShrink.maxShrinkSteps : 4);
289
- var baseLadder = (typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.ladder) || FONT_LADDER;
290
- var raw = baseLadder.filter(function (v) { return v <= max && v >= floor; });
291
- var needH = function (f) {
292
- var h = 0;
293
- for (var i = 0; i < items.length; i++) h += estTextH(items[i], widthIn, f, lf) + f * gapF / 72;
294
- return h + (opts.pad || 0);
295
- };
296
- for (var li = 0; li < raw.length; li++) {
297
- if (needH(raw[li]) <= availIn) return raw[li];
298
- }
299
- var preferred = raw.length ? raw[0] : floor;
300
- var prefIdx = baseLadder.indexOf(preferred);
301
- var limited = baseLadder.slice(prefIdx, prefIdx + 1 + maxSteps)
302
- .filter(function (v) { return v >= floor && v <= max; });
303
- for (var lj = 0; lj < limited.length; lj++) {
304
- if (needH(limited[lj]) <= availIn) return limited[lj];
305
- }
306
- if (limited.length) return limited[limited.length - 1];
307
- return floor;
308
- }
309
- function cols(total, n, gap) {
310
- var w = (total - (n - 1) * (gap || 0)) / Math.max(1, n);
311
- return { w: w, step: w + (gap || 0) };
312
- }
313
- /* 行高收敛:与 build_pptx.js 同算法(宁可行高更小也不越界) */
314
- function fitRowH(avail, n, pref, floor) {
315
- var lo = (floor == null) ? 0.22 : floor;
316
- return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
317
- }
318
- function chartBottom(hasSoWhat, hasFootnote) {
319
- if (hasFootnote) return PT.exhibit.footnoteY - 0.12;
320
- if (hasSoWhat) return PT.exhibit.soWhatY - 0.12;
321
- return CONTENT_BOTTOM;
322
- }
323
-
324
- /* ── XML 工具 ── */
325
- function esc(s) {
326
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
327
- .replace(/"/g, '&quot;').replace(/'/g, '&apos;');
328
- }
329
- function E(v) { return Math.round(v * EMU); }
330
- function solid(hex) { return '<a:solidFill><a:srgbClr val="' + hex + '"/></a:solidFill>'; }
331
-
332
- var _id = 10;
333
- function nid() { return ++_id; }
334
-
335
- /* 文本框:paras = [[{t,b,sz,col,font,spc}…]…](每个内层数组=一段,可混排多 run)
336
- opts.lineSpacing / opts.spaceBefore 为基准 pt(与 build_pptx.js 的 lineSpacing/paraSpaceBefore 同源),
337
- 序列化时统一过 modeSize 映射,保证两通道行距一致。 */
338
- function txSp(x, y, w, h, paras, opts) {
339
- opts = opts || {};
340
- var pPr = '';
341
- if (opts.lineSpacing) pPr += '<a:lnSpc><a:spcPts val="' + Math.round(modeSize(opts.lineSpacing) * 100) + '"/></a:lnSpc>';
342
- if (opts.spaceBefore) pPr += '<a:spcBef><a:spcPts val="' + Math.round(modeSize(opts.spaceBefore) * 100) + '"/></a:spcBef>';
343
- var pPrXml = (opts.align || pPr)
344
- ? '<a:pPr' + (opts.align ? ' algn="' + opts.align + '"' : '') + '>' + pPr + '</a:pPr>'
345
- : '';
346
- var body = paras.map(function (runs) {
347
- var rs = runs.map(function (r) {
348
- var pr = '<a:rPr lang="zh-CN" sz="' + Math.round(modeSize(r.sz) * 100) + '"' +
349
- (r.b ? ' b="1"' : '') + (r.spc ? ' spc="' + r.spc + '"' : '') + '>' +
350
- solid(r.col) +
351
- '<a:latin typeface="' + esc(r.font) + '"/><a:ea typeface="' + esc(r.font) + '"/></a:rPr>';
352
- return '<a:r>' + pr + '<a:t>' + esc(r.t) + '</a:t></a:r>';
353
- }).join('');
354
- return '<a:p>' + pPrXml + rs + '</a:p>';
355
- }).join('');
356
- var fill = opts.fill ? solid(opts.fill) : '<a:noFill/>';
357
- return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="tx"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>' +
358
- '<p:spPr><a:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(w) + '" cy="' + E(h) + '"/></a:xfrm>' +
359
- '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' + fill + '<a:ln><a:noFill/></a:ln></p:spPr>' +
360
- '<p:txBody><a:bodyPr wrap="square" anchor="' + (opts.anchor || 't') + '"><a:normAutofit/></a:bodyPr><a:lstStyle/>' +
361
- body + '</p:txBody></p:sp>';
362
- }
363
-
364
- /* 形状:rect / roundRect / ellipse / pie(opts.av = avLst 原始内联 XML,供 donut 楔形角度) */
365
- function shape(geom, x, y, w, h, fillHex, opts) {
366
- opts = opts || {};
367
- var av;
368
- if (opts.av) av = '<a:avLst>' + opts.av + '</a:avLst>';
369
- else if (geom === 'roundRect') av = '<a:avLst><a:gd name="adj" fmla="val ' + (opts.adj || 8000) + '"/></a:avLst>';
370
- else av = '<a:avLst/>';
371
- var ln = opts.line
372
- ? '<a:ln w="9525">' + (opts.dash ? '<a:prstDash val="dash"/>' : '') + solid(opts.line) + '</a:ln>'
373
- : '<a:ln><a:noFill/></a:ln>';
374
- return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="' + (opts.name || 'sh') + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
375
- '<p:spPr><a:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(Math.max(w, 0.01)) + '" cy="' + E(Math.max(h, 0.01)) + '"/></a:xfrm>' +
376
- '<a:prstGeom prst="' + geom + '">' + av + '</a:prstGeom>' + solid(fillHex) + ln + '</p:spPr>' +
377
- '<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="zh-CN"/></a:p></p:txBody></p:sp>';
378
- }
379
- /* pie 楔形 avLst:角度(度)× 60000;donut 页型两通道共用同一角度拆分规则 */
380
- function pieAv(a1, a2) {
381
- return '<a:gd name="adj1" fmla="val ' + Math.round(a1 * 60000) + '"/>' +
382
- '<a:gd name="adj2" fmla="val ' + Math.round(a2 * 60000) + '"/>';
383
- }
384
- /* 环形图(donut 页型):原生 pie 楔形 + 背景色圆(挖孔)+ 中心文字 + 右侧图例。
385
- 角度约定:12 点钟(270°)起顺时针;跨 360° 的楔形拆成两段(≤359.9 封顶)。 */
386
- function donutWedges(labels, values) {
387
- var total = 0;
388
- (values || []).forEach(function (v) { total += Math.max(0, v); });
389
- var segs = [], a = 270;
390
- (values || []).forEach(function (v, i) {
391
- var frac = total > 0 ? Math.max(0, v) / total : 0;
392
- var b = a + frac * 360;
393
- var s = a % 360, e = b % 360;
394
- if (frac <= 0) { a = b; return; }
395
- if (b - a >= 359.9) { segs.push([0, 359.9, i]); }
396
- else if (b <= 360) { segs.push([a, Math.min(b, 359.9), i]); }
397
- else { segs.push([a, 359.9, i]); if (e > 0.1) segs.push([0, e, i]); }
398
- a = b;
399
- });
400
- return segs;
401
- }
402
- function donutChart(sh, ec, S, dcols) {
403
- var d = PT.donut, labels = ec.labels || [], values = ec.values || [];
404
- var segs = donutWedges(labels, values);
405
- var bx = d.centerX - d.r, by = d.centerY - d.r, bw = d.r * 2;
406
- segs.forEach(function (sg) {
407
- sh.push(shape('pie', bx, by, bw, bw, dcols[sg[2] % dcols.length], { av: pieAv(sg[0], sg[1]) }));
408
- });
409
- var hb = d.holeR * 2;
410
- sh.push(shape('ellipse', d.centerX - d.holeR, d.centerY - d.holeR, hb, hb, S.bg));
411
- var total = 0; values.forEach(function (v) { total += Math.max(0, v); });
412
- var totalStr = (Math.round(total * 10) / 10) + (ec.unit || '');
413
- sh.push(txSp(d.centerX - d.holeR, d.centerY - 0.42, hb, 0.5,
414
- [[{ t: totalStr, sz: 20, b: 1, col: S.ink, font: S.fontDisplay }]], { align: 'ctr', anchor: 'ctr' }));
415
- sh.push(txSp(d.centerX - d.holeR, d.centerY + 0.08, hb, 0.35,
416
- [[{ t: ec.centerLabel || '合计', sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
417
- var n = Math.max(1, labels.length);
418
- var rowH = Math.min(d.legendRowH, 3.4 / n);
419
- var ly0 = d.centerY - (n * rowH) / 2 + 0.1;
420
- var sum2 = total || 1;
421
- labels.forEach(function (lb, i) {
422
- var v = values[i] || 0;
423
- var y = ly0 + i * rowH;
424
- sh.push(shape('ellipse', d.legendX, y + 0.08, 0.16, 0.16, dcols[i % dcols.length]));
425
- sh.push(txSp(d.legendX + 0.3, y, d.legendLabelW, rowH,
426
- [[{ t: lb, sz: 12, b: 1, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
427
- sh.push(txSp(d.legendX + 3.8, y, d.legendValW, rowH,
428
- [[{ t: String(v) + (ec.unit || ''), sz: 12, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
429
- sh.push(txSp(d.legendPctX, y, d.legendPctW, rowH,
430
- [[{ t: (Math.round(v / sum2 * 1000) / 10) + '%', sz: 10.5, col: S.faint, font: S.font }]], { align: 'r', anchor: 'ctr' }));
431
- });
432
- }
433
- function donutColors(style, S, theme) {
434
- var d = dataColors(style, theme);
435
- return d || [S.accent, S.faint, S.body, S.line];
436
- }
437
-
438
- /* 表格(graphicFrame + a:tbl);rows[0]=表头;行高按可用高度自适应(与 B 通道同规则) */
439
- function table(x, y, w, colW, rows, S, opts) {
440
- opts = opts || {};
441
- var n = rows.length;
442
- var maxRowH = (opts.maxRowH == null) ? PT.table.rowH : opts.maxRowH;
443
- var minRowH = (opts.minRowH == null) ? PT.table.rowHMin : opts.minRowH;
444
- var availH = (opts.availH == null) ? (CONTENT_BOTTOM - y) : opts.availH;
445
- var rowH = Math.max(minRowH, Math.min(maxRowH, availH / Math.max(1, n)));
446
- var dense = rowH < 0.42;
447
- var total = colW.reduce(function (a, b) { return a + b; }, 0);
448
- var grid = colW.map(function (cw) { return '<a:gridCol w="' + E(cw) + '"/>'; }).join('');
449
- var trs = rows.map(function (row, ri) {
450
- var tcs = row.map(function (cell) {
451
- var isHead = ri === 0;
452
- var fz = isHead ? (dense ? 11 : 13) : (dense ? 10 : 12.5);
453
- return '<a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r>' +
454
- '<a:rPr lang="zh-CN" sz="' + Math.round(modeSize(fz) * 100) + '"' + (isHead ? ' b="1"' : '') + '>' +
455
- solid(isHead ? S.onAccent : S.body) +
456
- '<a:latin typeface="' + S.font + '"/><a:ea typeface="' + S.font + '"/></a:rPr>' +
457
- '<a:t>' + esc(cell) + '</a:t></a:r></a:p></a:txBody>' +
458
- '<a:tcPr marL="91440" marR="91440" marT="45720" marB="45720" anchor="ctr">' +
459
- solid(isHead ? S.accent : S.bg) + '</a:tcPr></a:tc>';
460
- }).join('');
461
- return '<a:tr h="' + E(rowH) + '">' + tcs + '</a:tr>';
462
- }).join('');
463
- /* OOXML 规范:p:graphicFrame 的 graphic 子元素属 DrawingML 命名空间(a:graphic,与 pptxgenjs 一致) */
464
- return '<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="' + nid() + '" name="tbl"/>' +
465
- '<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr>' +
466
- '<p:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(w) + '" cy="' + E(rowH * rows.length) + '"/></p:xfrm>' +
467
- '<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">' +
468
- '<a:tbl><a:tblPr firstRow="1" bandRow="0"/><a:tblGrid>' + grid + '</a:tblGrid>' + trs + '</a:tbl>' +
469
- '</a:graphicData></a:graphic></p:graphicFrame>';
470
- }
471
-
472
- /* so-what 结论条 / 页脚来源行(research 页通用可选件,与 HTML 同源版式;区域走布局 IR) */
473
- function soWhatBar(sh, text, S) {
474
- var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'annotation')
475
- : { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
476
- /* 与 B 通道同算法:长结论条有限缩字号,优先保容器不溢出 */
477
- var bodyW = Math.max(2.5, R.w - 0.44 - 1.35);
478
- var fzB = fitFont([String(text || '')], bodyW, R.h - 0.14, { max: 12.5, gapFactor: 0.2, maxShrinkSteps: 3 });
479
- sh.push(shape('rect', R.x, R.y, R.w, R.h, S.soft));
480
- sh.push(shape('rect', R.x, R.y, 0.06, R.h, S.accent));
481
- sh.push(txSp(R.x + 0.22, R.y + 0.06, R.w - 0.44, R.h - 0.12,
482
- [[{ t: 'SO WHAT ', sz: 11, b: 1, col: S.accent, font: S.font, spc: 150 },
483
- { t: text, sz: fzB, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
484
- }
485
- function footnoteLine(sh, text, S) {
486
- var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'footnote')
487
- : { x: MX, y: PT.exhibit.footnoteY, w: CW - 1.2, h: 0.32 };
488
- sh.push(txSp(R.x, R.y, R.w, R.h,
489
- [[{ t: text, sz: 9.5, col: S.faint, font: S.font }]]));
490
- }
491
- /* 待核实清单条(accent 强调色标注 · 与 build_pptx.js / HTML .flagbar 同源) */
492
- function flagBar(sh, items, S, yTop) {
493
- var F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
494
- var list = (items || []).slice(0, F.maxRows).map(String);
495
- if (!list.length) return 0;
496
- var h = F.hdH + list.length * F.rowH + 0.06;
497
- var y = (yTop != null) ? yTop : (CONTENT_BOTTOM - h);
498
- sh.push(shape('rect', MX, y, CW, h, S.soft));
499
- sh.push(shape('rect', MX, y, 0.055, h, S.accent));
500
- sh.push(txSp(MX + 0.22, y + 0.02, CW - 0.44, F.hdH,
501
- [[{ t: '待核实 · 需二次确认', sz: 9.5, b: 1, col: S.accent, font: S.font, spc: 100 }]]));
502
- list.forEach(function (t, i) {
503
- sh.push(txSp(MX + 0.24, y + F.hdH + i * F.rowH, CW - 0.5, F.rowH,
504
- [[{ t: '· ' + t, sz: 9.5, col: S.body, font: S.font }]], { anchor: 'ctr' }));
505
- });
506
- return h;
507
- }
508
- /* 素材图片:预览引擎只产出 slide XML(无 media part)→ 以中性图位块表示;
509
- 配图占位(placeholder)额外发射标签文本——标签串由 imageSpec 常量拼接,与 B 通道**同串**
510
- (cross_verify 逐页比对)。真实图片不发射任何文本,保证 A/B 文本集合一致。 */
511
- function imgPlaceholderLabel(layout, multi) {
512
- var IS = (typeof IMG !== 'undefined' && IMG) || {};
513
- var label = IS.placeholderLabel || '配图占位';
514
- if (multi) return label;
515
- var size = (IS.recommendedSizePx || {})[layout];
516
- if (!size) return label;
517
- return label + ' · ' + (IS.placeholderHintPrefix || '建议 ') + size + (IS.placeholderHintSuffix || 'px');
518
- }
519
- function addImageEl(sh, src, S, x, y, w, h, opts) {
520
- opts = opts || {};
521
- var ph = !!opts.placeholder || !src;
522
- /* name="img"/"imgph":预览模态据此把图位替换为真实图片(ui.js renderSlide),
523
- 不影响文本集合(cross_verify 只比文本)。 */
524
- sh.push(shape('roundRect', x, y, w, h, S.surface,
525
- { line: S.line, dash: ph, adj: 4000, name: ph ? 'imgph' : 'img' }));
526
- if (ph) {
527
- sh.push(txSp(x + 0.08, y + h / 2 - 0.22, Math.max(0.4, w - 0.16), 0.44,
528
- [[{ t: imgPlaceholderLabel(opts.layout || 'full', !!opts.multi), sz: 11, b: 1, col: S.faint, font: S.font }]],
529
- { align: 'ctr', anchor: 'ctr' }));
530
- }
531
- }
532
- /* 版式比例(imageSpec.ratioDefault 或 image.ratio,形如 "3:1")→ 数值宽高比。
533
- 双引擎同源:HTML 用同比例的 .media--r* 锁定类,PPTX 用本函数算高度,版式才一致。 */
534
- function imgRatio(layout, img) {
535
- var IS = (typeof IMG !== 'undefined' && IMG) || {};
536
- var raw = String((img && img.ratio) || (IS.ratioDefault || {})[layout] || '3:1');
537
- var m = raw.match(/^\s*(\d+(?:\.\d+)?)\s*[:/×x]\s*(\d+(?:\.\d+)?)/);
538
- if (!m) return 3;
539
- var a = parseFloat(m[1]), b = parseFloat(m[2]);
540
- return (a > 0 && b > 0) ? a / b : 3;
541
- }
542
- /* 图片版式族(与 HTML .media / .media-grid / .media-compare / .media-wall 同源):
543
- full 版心全宽|half 左图右注|bleed 通栏出血|grid 多图网格|compare 双图对比|wall Logo 墙。
544
- 每版式按 imageSpec.ratioDefault 锁定比例;空间不足时按可用高度收敛并**垂直居中**
545
- (避免"图贴顶、下方大片留白")。返回图注基准 Y。占位符走原生形状,pictures 不增。 */
546
- function imageLayoutShapes(sh, S, layout, img, items, points, isPh, fit, y0, y1) {
547
- var IM = PT.image || {};
548
- var gap = IM.gridGap || 0.16;
549
- var iy = y0;
550
- var ih = Math.max(1.0, y1 - iy);
551
- var capY = iy + ih;
552
- var ratio = imgRatio(layout, img);
553
- function one(x, y, w, h, it, multi) {
554
- var ph = isPh || !(it && it.src);
555
- addImageEl(sh, (it && it.src) || '', S, x, y, w, h,
556
- { placeholder: ph, layout: layout, multi: multi, fit: fit });
557
- }
558
- if (layout === 'grid' && items.length) {
559
- var n = Math.min(items.length, (IMG && IMG.maxPerPage) || 6);
560
- var colsN = (IM.gridCols || {})[String(n)] || Math.min(3, n);
561
- var rowsN = Math.max(1, Math.ceil(n / colsN));
562
- var capH = IM.gridCapH || 0.28;
563
- var cellW = (CW - (colsN - 1) * gap) / colsN;
564
- var capRows = rowsN * (capH + 0.04);
565
- var cellH = Math.min(cellW / ratio, Math.max(0.5, (ih - (rowsN - 1) * gap - capRows) / rowsN));
566
- var blockH = rowsN * cellH + (rowsN - 1) * gap + capRows;
567
- var gy = iy + Math.max(0, (ih - blockH) / 2);
568
- items.slice(0, n).forEach(function (it, i) {
569
- var cx = MX + (i % colsN) * (cellW + gap);
570
- var cy = gy + Math.floor(i / colsN) * (cellH + gap + capH + 0.04);
571
- var cap = (it && it.caption) || '';
572
- one(cx, cy, cellW, cellH, it, true);
573
- if (cap) sh.push(txSp(cx, cy + cellH + 0.02, cellW, capH,
574
- [[{ t: cap, sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
575
- });
576
- capY = gy + blockH;
577
- } else if (layout === 'compare' && items.length >= 2) {
578
- var cgap = IM.compareGap || 0.3;
579
- var cw2 = (CW - cgap) / 2;
580
- var capH2 = 0.34;
581
- var ih2 = Math.min(cw2 / ratio, Math.max(0.5, ih - capH2));
582
- var cy2 = iy + Math.max(0, (ih - ih2 - capH2) / 2);
583
- [0, 1].forEach(function (i) {
584
- var it = items[i] || {};
585
- var cx2 = MX + i * (cw2 + cgap);
586
- one(cx2, cy2, cw2, ih2, it, true);
587
- var cap2 = it.caption || '';
588
- if (cap2) sh.push(txSp(cx2, cy2 + ih2 + 0.02, cw2, 0.3,
589
- [[{ t: cap2, sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
590
- });
591
- capY = cy2 + ih2 + capH2;
592
- } else if (layout === 'wall' && items.length) {
593
- var wgap = IM.wallGap || 0.18;
594
- var wn = Math.min(items.length, 12);
595
- var rowsW = Math.max(1, Math.min(IM.wallMaxRows || 2, Math.ceil(wn / 6)));
596
- var perRow = Math.max(1, Math.ceil(wn / rowsW));
597
- var cellW2 = (CW - (perRow - 1) * wgap) / perRow;
598
- var cellH2 = Math.min(IM.wallCellH || 0.92, cellW2 / ratio,
599
- Math.max(0.4, (ih - (rowsW - 1) * wgap) / rowsW));
600
- var blockH2 = rowsW * cellH2 + (rowsW - 1) * wgap;
601
- var wy = iy + Math.max(0, (ih - blockH2) / 2);
602
- items.slice(0, wn).forEach(function (it, i) {
603
- var cx3 = MX + (i % perRow) * (cellW2 + wgap);
604
- var cy3 = wy + Math.floor(i / perRow) * (cellH2 + wgap);
605
- one(cx3, cy3, cellW2, cellH2, it, true);
606
- });
607
- capY = wy + blockH2;
608
- } else if (layout === 'half') {
609
- var iw = CW * (IM.halfW || 0.56);
610
- var ihh = Math.min(iw / ratio, ih);
611
- one(MX, iy, iw, ihh, img, false);
612
- var rxI = MX + iw + CW * (IM.halfGap || 0.06);
613
- var rwI = CW - iw - CW * (IM.halfGap || 0.06);
614
- var ptsI = points || [];
615
- var rowHI = fitRowH(ih - 0.1, ptsI.length, IM.noteRowH || 0.62, 0.3);
616
- var fzI = fitFont(ptsI.map(function (p) { return (p[0] || '') + (p[1] || ''); }), rwI - 0.3, ih,
617
- { max: 14, gapFactor: 0.6 });
618
- ptsI.forEach(function (pt, i) {
619
- var lyI = iy + i * rowHI;
620
- sh.push(shape('rect', rxI, lyI + rowHI / 2 - 0.06, 0.12, 0.12, S.accent));
621
- sh.push(txSp(rxI + 0.26, lyI, rwI - 0.3, rowHI - 0.04,
622
- [[{ t: (pt[0] || '') + ' ', sz: fzI, b: 1, col: S.ink, font: S.font },
623
- { t: pt[1] || '', sz: fzI - 1, col: S.body, font: S.font }]], { anchor: 't' }));
624
- });
625
- capY = iy + Math.max(ihh, ih * 0.5);
626
- } else if (layout === 'bleed') {
627
- var bh = Math.min(ih, PW / ratio);
628
- var by = iy + Math.max(0, (ih - bh) / 2);
629
- one(0, by, PW, bh, img, false);
630
- capY = by + bh;
631
- } else {
632
- var fh = Math.min(ih, CW / ratio);
633
- var fy = iy + Math.max(0, (ih - fh) / 2);
634
- one(MX, fy, CW, fh, img, false);
635
- capY = fy + fh;
636
- }
637
- return capY;
638
- }
639
-
640
- /* ══ 图表形状渲染(预览/回归用近似版;交付通道 B 用原生可编辑数据图表) ══
641
- A/B 两通道的「文本集合」必须一致(cross_verify 逐页比对):
642
- 类别标签 + 数值标签都渲染为文本,数值标签由 cross_verify 的数值 token 规则过滤。 */
643
- function valText(v, unit) { return String(v) + (unit || ''); }
644
- /* 横向条形(hbar / halftable / exhibit hbar) */
645
- function hbarRows(sh, ec, S, y0, y1, dcols, x0, w) {
646
- x0 = (x0 == null) ? MX : x0;
647
- w = (w == null) ? CW : w;
648
- var labels = ec.labels || [], values = ec.values || [];
649
- var n = Math.max(1, labels.length);
650
- var rowH = Math.min(PT.hbar.rowH, (y1 - y0) / n);
651
- var barH = Math.max(0.16, rowH * (PT.hbar.barRatio || 0.5));
652
- var labelW = PT.hbar.labelW, valW = PT.hbar.valW;
653
- var trackX = x0 + labelW + 0.1, trackW = w - labelW - valW - 0.2;
654
- var vmax = ec.max || Math.max.apply(null, values.concat([1]));
655
- var top = values.length ? Math.max.apply(null, values) : 0;
656
- labels.forEach(function (lb, i) {
657
- var v = values[i] || 0;
658
- var y = y0 + i * rowH + (rowH - barH) / 2;
659
- var hot = v === top;
660
- var dc = dcols ? dcols[i % 5] : null;
661
- sh.push(txSp(x0, y - 0.06, labelW - 0.05, barH + 0.12,
662
- [[{ t: lb, sz: 11, col: S.body, font: S.font }]], { anchor: 'ctr' }));
663
- sh.push(shape('rect', trackX, y, trackW, barH, S.surface, { line: S.line }));
664
- var bw2 = Math.max(0.02, v / vmax * trackW);
665
- sh.push(shape('rect', trackX, y, bw2, barH, dc || (hot ? S.accent : S.faint)));
666
- sh.push(txSp(trackX + trackW + 0.08, y - 0.06, valW, barH + 0.12,
667
- [[{ t: valText(v, ec.unit), sz: 11.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]], { anchor: 'ctr' }));
668
- });
669
- }
670
- /* 竖柱 / 堆叠柱 */
671
- function vbarShapes(sh, ec, S, x, y, w, h, dcols) {
672
- var labels = ec.labels || [], values = ec.values || [];
673
- var nb = Math.max(1, labels.length);
674
- var baseY = y + h, plotW = w, step = plotW / nb;
675
- var bw = Math.min(1.1, step * 0.55);
676
- var vmax = ec.max || Math.max.apply(null, values.concat([1]));
677
- var top = values.length ? Math.max.apply(null, values) : 0;
678
- sh.push(shape('rect', x, baseY, plotW, 0.02, S.line));
679
- values.forEach(function (v, i) {
680
- var hh = Math.max(0.05, v / vmax * (h - 0.5));
681
- var bx = x + i * step + (step - bw) / 2;
682
- var hot = v === top;
683
- var dc = dcols ? dcols[i % 5] : null;
684
- sh.push(shape('roundRect', bx, baseY - hh, bw, hh, dc || (hot ? S.accent : S.faint), { adj: 6000 }));
685
- sh.push(txSp(bx - 0.3, baseY - hh - 0.4, bw + 0.6, 0.32,
686
- [[{ t: valText(v, ec.unit), sz: 12.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]], { align: 'ctr' }));
687
- sh.push(txSp(bx - 0.3, baseY + 0.08, bw + 0.6, 0.32,
688
- [[{ t: labels[i], sz: 10.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
689
- });
690
- }
691
- /* 折线 / 双折线 / 面积 */
692
- function lineShapes(sh, ec, S, x, y, w, h, dcols) {
693
- var labels = ec.labels || [];
694
- var series = (ec.series && ec.series.length) ? ec.series : [{ name: '数值', values: ec.values || [] }];
695
- var n = Math.max(1, labels.length);
696
- var all = [];
697
- series.forEach(function (se) { (se.values || []).forEach(function (v) { all.push(Number(v) || 0); }); });
698
- var vmax = ec.max || Math.max.apply(null, all.concat([1]));
699
- var plotH = h - 0.5, baseY = y + h;
700
- sh.push(shape('rect', x, baseY, w, 0.02, S.line));
701
- var stepX = n > 1 ? w / (n - 1) : 0;
702
- series.forEach(function (se, si) {
703
- var col = dcols ? dcols[si % 5] : (si === 0 ? S.accent : S.faint);
704
- var pts = (se.values || []).map(function (v, i) {
705
- var px = x + (n > 1 ? i * stepX : w / 2);
706
- var py = baseY - Math.max(0.02, (Number(v) || 0) / vmax * plotH);
707
- return [px, py];
708
- });
709
- if (!pts.length) return;
710
- for (var i = 0; i < pts.length - 1; i++) {
711
- sh.push(lineSeg(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], col, 2.5));
712
- }
713
- pts.forEach(function (p) {
714
- sh.push(shape('ellipse', p[0] - 0.055, p[1] - 0.055, 0.11, 0.11, col));
715
- });
716
- });
717
- labels.forEach(function (lb, i) {
718
- var px = x + (n > 1 ? i * stepX : w / 2);
719
- sh.push(txSp(px - 0.5, baseY + 0.08, 1.0, 0.32,
720
- [[{ t: lb, sz: 10.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
721
- });
722
- }
723
- /* 直线段(用细长矩形近似,避免自定义几何——两通道渲染一致) */
724
- function lineSeg(x1, y1, x2, y2, hex, thick) {
725
- var dx = x2 - x1, dy = y2 - y1;
726
- var len = Math.sqrt(dx * dx + dy * dy);
727
- var ang = Math.atan2(dy, dx) * 180 / Math.PI;
728
- var cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
729
- var t = thick / 72;
730
- var rot = '<a:xfrm rot="' + Math.round(ang * 60000) + '">';
731
- return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="ln"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
732
- '<p:spPr>' + rot + '<a:off x="' + E(cx - len / 2) + '" y="' + E(cy - t / 2) + '"/>' +
733
- '<a:ext cx="' + E(len) + '" cy="' + E(t) + '"/></a:xfrm>' +
734
- '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' + solid(hex) + '<a:ln><a:noFill/></a:ln></p:spPr>' +
735
- '<p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>';
736
- }
737
- /* 散点 / 气泡(A 通道近似):只画点、不产文本——与 B 通道 XY 图表(类别为数值,过滤后为空)一致 */
738
- function xyDots(sh, ec, S, x, y, w, h) {
739
- var values = (ec.values || []).map(Number);
740
- var pts = (ec.points && ec.points.length) ? ec.points : null;
741
- var n = pts ? pts.length : Math.max(1, values.length);
742
- var vmax = ec.max || Math.max.apply(null, values.concat([1]));
743
- var baseY = y + h, plotH = Math.max(0.3, h - 0.3);
744
- var stepX = n > 1 ? w / (n - 1) : 0;
745
- sh.push(shape('rect', x, baseY, w, 0.02, S.line));
746
- for (var i = 0; i < n; i++) {
747
- var px = pts ? x + (Number(pts[i][0]) || 0) / vmax * w : x + i * stepX;
748
- var py = pts ? baseY - (Number(pts[i][1]) || 0) / vmax * plotH : baseY - (values[i] || 0) / vmax * plotH;
749
- sh.push(shape('ellipse', px - 0.07, py - 0.07, 0.14, 0.14, S.accent));
750
- }
751
- }
752
- /* 仪表盘(A 通道近似):270° 弧段 + 数值(B 通道 gauge 的类别为数值,过滤后两端一致,故此处不产标签文本) */
753
- function gaugeApprox(sh, ec, S, x, y, w, h) {
754
- var v = Number((ec.values || [0])[0]) || 0;
755
- var max = Number(ec.max) || 100;
756
- var frac = Math.max(0, Math.min(1, v / max));
757
- var r = Math.min(w, h) / 2 - 0.12;
758
- var cx = x + w / 2, cy = y + h / 2;
759
- var steps = 40, startA = Math.PI * 0.75, sweep = Math.PI * 1.5;
760
- var px = null, py = null;
761
- for (var i = 0; i <= steps; i++) {
762
- var a = startA + sweep * (i / steps);
763
- var qx = cx + r * Math.cos(a), qy = cy + r * Math.sin(a);
764
- if (px !== null) sh.push(lineSeg(px, py, qx, qy, (i / steps) <= frac ? S.accent : S.line, 5));
765
- px = qx; py = qy;
766
- }
767
- sh.push(txSp(x, cy - 0.2, w, 0.42,
768
- [[{ t: String(v) + (ec.unit || ''), sz: 18, b: 1, col: S.accent, font: S.fontDisplay }]], { align: 'ctr', anchor: 'ctr' }));
769
- }
770
- /* 饼 / 环(A 通道近似):横向占比条 + 图例(类别标签为文本,数值由 cross_verify 过滤) */
771
- function pieApprox(sh, ec, S, x, y, w, h, dcols) {
772
- var labels = ec.labels || [], values = (ec.values || []).map(Number);
773
- var total = values.reduce(function (a, b) { return a + b; }, 0) || 1;
774
- var barH = Math.max(0.32, Math.min(0.62, h * 0.22));
775
- var barY = y + h / 2 - barH / 2 - 0.2;
776
- var cx0 = x;
777
- labels.forEach(function (lb, i) {
778
- var bw = (values[i] || 0) / total * w;
779
- sh.push(shape('rect', cx0, barY, bw, barH,
780
- dcols ? dcols[i % dcols.length] : (i === 0 ? S.accent : S.faint)));
781
- cx0 += bw;
782
- });
783
- var lx = x;
784
- labels.forEach(function (lb, i) {
785
- sh.push(shape('ellipse', lx, barY + barH + 0.16, 0.1, 0.1,
786
- dcols ? dcols[i % dcols.length] : (i === 0 ? S.accent : S.faint)));
787
- sh.push(txSp(lx + 0.14, barY + barH + 0.12, 1.2, 0.24, [[{ t: lb, sz: 10.5, col: S.body, font: S.font }]]));
788
- sh.push(txSp(lx + 1.36, barY + barH + 0.12, 0.8, 0.24,
789
- [[{ t: String(values[i] != null ? values[i] : '') + (ec.unit || ''), sz: 10.5, b: 1, col: S.body, font: S.font }]]));
790
- lx += 2.3;
791
- });
792
- }
793
- /* 迷你折线(A 近似):无轴无标签,仅趋势线 + 末值(与 B 的 shapeSpark 同文本集) */
794
- function sparkApprox(sh, ec, S, x, y, w, h) {
795
- var vals = (ec.values || []).map(Number);
796
- var vmax = ec.max || Math.max.apply(null, vals.concat([1]));
797
- var n = vals.length;
798
- var stepX = n > 1 ? w / (n - 1) : 0;
799
- var baseY = y + h - 0.06, plotH = Math.max(0.2, h - 0.18);
800
- var px = 0, py = 0;
801
- vals.forEach(function (v, i) {
802
- var cx = x + i * stepX, cy = baseY - Math.max(0.02, (v / vmax) * plotH);
803
- if (i > 0) sh.push(lineSeg(px, py, cx, cy, S.accent, 2));
804
- px = cx; py = cy;
805
- });
806
- sh.push(txSp(x, y, w, 0.3, [[{ t: String(vals[n - 1] != null ? vals[n - 1] : '') + (ec.unit || ''),
807
- sz: 11.5, b: 1, col: S.accent, font: S.font }]], { align: 'r' }));
808
- }
809
- /* K 线(A 近似):实体 + 须,标签 + 收盘值(与 B 的 shapeCandlestick 同文本集) */
810
- function candleApprox(sh, ec, S, x, y, w, h) {
811
- var labels = ec.labels || [];
812
- var rows = (ec.values || []).filter(function (r) { return r && r.length >= 4; })
813
- .map(function (r) { return r.slice(0, 4).map(Number); });
814
- var n = Math.max(1, rows.length);
815
- var flat = [];
816
- rows.forEach(function (r) { r.forEach(function (v) { if (!isNaN(v)) flat.push(v); }); });
817
- var lo = flat.length ? Math.min.apply(null, flat) : 0;
818
- var hi = flat.length ? Math.max.apply(null, flat) : 1;
819
- var span = (hi - lo) || 1;
820
- var axisH = 0.26, plotH = Math.max(0.5, h - axisH - 0.1);
821
- var yFor = function (v) { return y + 0.06 + (1 - (v - lo) / span) * plotH; };
822
- var step = w / n, bw = Math.min(0.42, step * 0.5);
823
- sh.push(shape('rect', x, y + 0.06 + plotH, w, 0.015, S.line));
824
- rows.forEach(function (r, i) {
825
- var o = r[0], hiP = r[1], loP = r[2], cl = r[3];
826
- var cxp = x + step * i + step / 2;
827
- var up = cl >= o;
828
- sh.push(shape('rect', cxp - 0.011, yFor(hiP), 0.022, Math.max(0.02, yFor(loP) - yFor(hiP)), S.faint));
829
- sh.push(shape('rect', cxp - bw / 2, yFor(Math.max(o, cl)), bw,
830
- Math.max(0.04, Math.abs(yFor(cl) - yFor(o))), up ? S.accent : S.line));
831
- if (labels[i]) {
832
- sh.push(txSp(cxp - step / 2, y + plotH + axisH - 0.06, step, 0.24,
833
- [[{ t: String(labels[i]), sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
834
- }
835
- sh.push(txSp(cxp - step / 2, yFor(hiP) - 0.26, step, 0.24,
836
- [[{ t: String(cl) + (ec.unit || ''), sz: 11.5, b: 1, col: up ? S.accent : S.body, font: S.font }]],
837
- { align: 'r' }));
838
- });
839
- }
840
- /* 通用形状近似(与 build_pptx.js 的 shapeProportion 同文本:标签 + 数值) */
841
- function shapeProportion(sh, ec, S, x, y, w, h, dcols) {
842
- var labels = ec.labels || [], values = (ec.values || []).map(Number);
843
- var n = Math.max(1, labels.length);
844
- var labelW = Math.min(2.4, w * 0.24), valW = 0.9;
845
- var vmax = ec.max || Math.max.apply(null, values.concat([1]));
846
- var top = values.length ? Math.max.apply(null, values) : 0;
847
- var rowH = Math.max(0.22, Math.min(0.52, h / n));
848
- var trackX = x + labelW + 0.08, trackW = w - labelW - valW - 0.2;
849
- var barH = Math.max(0.12, rowH * 0.52);
850
- labels.forEach(function (lb, i) {
851
- var v = values[i] || 0, ry = y + i * rowH, hot = v === top;
852
- sh.push(txSp(x, ry, labelW - 0.06, rowH, [[{ t: lb, sz: 11, col: S.body, font: S.font }]], { anchor: 'ctr' }));
853
- sh.push(shape('rect', trackX, ry + (rowH - barH) / 2, trackW, barH, S.surface, { line: S.line }));
854
- sh.push(shape('rect', trackX, ry + (rowH - barH) / 2, Math.max(0.02, Math.abs(v) / vmax * trackW), barH,
855
- dcols ? dcols[i % dcols.length] : (hot ? S.accent : S.faint)));
856
- sh.push(txSp(trackX + trackW + 0.06, ry, valW, rowH,
857
- [[{ t: String(v) + (ec.unit || ''), sz: 11.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]],
858
- { align: 'right', anchor: 'ctr' }));
859
- });
860
- }
861
- /* 数据表行(与 build_pptx.js 的 dataTableRows 同规则) */
862
- function dataTableRows(ec) {
863
- var labels = (ec.labels || []).map(String);
864
- var unit = ec.unit ? '(' + ec.unit + ')' : '';
865
- if (ec.series && ec.series.length) {
866
- var out = [['项目'].concat(ec.series.map(function (se, i) { return se.name || ('系列' + (i + 1)); }))];
867
- labels.forEach(function (lb, i) {
868
- out.push([lb].concat(ec.series.map(function (se) {
869
- var v = (se.values || [])[i]; return v == null ? '' : String(v);
870
- })));
871
- });
872
- return out;
873
- }
874
- var out2 = [['项目', '数值' + unit]];
875
- labels.forEach(function (lb, i) {
876
- var v = (ec.values || [])[i];
877
- out2.push([lb, v == null ? '' : String(v)]);
878
- });
879
- return out2;
880
- }
881
- /* 数据表策略(与 build_pptx.js 的 dataTableMode 同规则) */
882
- function dataTableModeOf(ec) {
883
- var t = (ec.type || 'bar').toLowerCase();
884
- var reg = (typeof REG !== 'undefined' && REG && REG[t]) ? REG[t] : null;
885
- var m = ec.dataTable || (reg && reg.dataTable) || 'notes';
886
- return m === 'appendix' ? 'inline' : m;
887
- }
888
- /* 图表 + 数据表组合(与 build_pptx.js 的 chartBlock 同策略:inline 在图表下方附原生表格) */
889
- function chartBlockShapes(sh, ec, S, x, y, w, h, dcols) {
890
- var dm = dataTableModeOf(ec);
891
- var chH = h;
892
- if (dm === 'inline' && (ec.labels || []).length) {
893
- var rowsData = dataTableRows(ec);
894
- var n = rowsData.length;
895
- var tH = Math.min(h * 0.44, Math.max(0.44, n * 0.22 + 0.06));
896
- chH = Math.max(0.85, h - tH - 0.1);
897
- var colW = rowsData[0].map(function () { return w / rowsData[0].length; });
898
- sh.push(table(x, y + chH + 0.1, w, colW, rowsData, S, { availH: tH, maxRowH: 0.24, minRowH: 0.14 }));
899
- }
900
- chartShapes(sh, ec, S, x, y, w, chH, dcols);
901
- }
902
- /* ══ 复杂信息图页型(A 通道形状近似:文本集合与 B 通道严格一致,cross_verify 逐页比对)══ */
903
- function infoTableRows(sec) {
904
- var t = sec.type || '';
905
- var u = sec.unit ? '(' + sec.unit + ')' : '';
906
- if (t === 'sankey') return [['源', '汇', '流量' + u]].concat((sec.flows || []).map(function (f) { return [String(f[0]), String(f[1]), String(f[2])]; }));
907
- if (t === 'treemap') return [['项目', '数值' + u]].concat((sec.items || []).map(function (it) { return [String(it[0]), String(it[1])]; }));
908
- if (t === 'boxplot') return [['分组', '最小', 'Q1', '中位', 'Q3', '最大']].concat((sec.groups || []).map(function (g) { return g.map(String); }));
909
- if (t === 'network') return [['节点', '名称']].concat((sec.nodes || []).map(function (n) { return [String(n[0]), String(n[1])]; }));
910
- if (t === 'marimekko') {
911
- var head = ['列', '总量' + u].concat((sec.legend || []).map(String));
912
- return [head].concat((sec.cols || []).map(function (c, i) {
913
- return [String(c[0]), String(c[1])].concat(((sec.cells || [])[i] || []).map(String));
914
- }));
915
- }
916
- if (t === 'streamgraph') {
917
- return [['系列'].concat((sec.labels || []).map(String))]
918
- .concat((sec.series || []).map(function (se) {
919
- return [String(se.name || '')].concat((se.values || []).map(String));
920
- }));
921
- }
922
- return null;
923
- }
924
- function infoDataTableMode(sec) {
925
- var m = String(((sec.chart || {}).dataTable) || 'notes').toLowerCase();
926
- return m === 'appendix' ? 'inline' : m;
927
- }
928
- function infoPlotA(sec, y0, y1) {
929
- var rows = infoTableRows(sec);
930
- var inline = (infoDataTableMode(sec) === 'inline') && rows && rows.length;
931
- if (!inline) return { y0: y0, y1: y1, rows: rows };
932
- var tH = Math.min((y1 - y0) * 0.4, Math.max(0.44, rows.length * 0.22 + 0.06));
933
- return { y0: y0, y1: y1 - tH - 0.1, rows: rows, table: { y: y1 - tH, h: tH } };
934
- }
935
- function tmSplitA(items, x, y, w, h, out) {
936
- if (!items.length) return;
937
- if (items.length === 1) { out.push({ it: items[0], x: x, y: y, w: w, h: h }); return; }
938
- var total = items.reduce(function (a, i) { return a + i.v; }, 0) || 1;
939
- var acc = 0, k = 0;
940
- for (; k < items.length - 1; k++) { acc += items[k].v; if (acc >= total / 2) break; }
941
- var first = items.slice(0, k + 1), second = items.slice(k + 1);
942
- var r = first.reduce(function (a, i) { return a + i.v; }, 0) / total;
943
- if (w >= h) {
944
- tmSplitA(first, x, y, w * r, h, out);
945
- tmSplitA(second, x + w * r, y, w * (1 - r), h, out);
946
- } else {
947
- tmSplitA(first, x, y, w, h * r, out);
948
- tmSplitA(second, x, y + h * r, w, h * (1 - r), out);
949
- }
950
- }
951
- /* 信息图近似渲染:按页型画形状 + 落同样的标签文本 */
952
- function infoApprox(sh, sec, S, y0, y1) {
953
- var t = sec.type || '';
954
- var P = infoPlotA(sec, y0, y1);
955
- if (P.table) {
956
- var colW = P.rows[0].map(function () { return CW / P.rows[0].length; });
957
- sh.push(table(MX, P.table.y, CW, colW, P.rows, S, { availH: P.table.h, maxRowH: 0.24, minRowH: 0.15 }));
958
- }
959
- var palette = [S.accent, S.faint, S.body, S.line];
960
- if (t === 'sankey') {
961
- var flows = (sec.flows || []).filter(function (f) { return f && f.length >= 3; });
962
- var nodes = [], idx = {};
963
- flows.forEach(function (f) {
964
- [f[0], f[1]].forEach(function (n) { if (idx[n] == null) { idx[n] = nodes.length; nodes.push(String(n)); } });
965
- });
966
- var level = nodes.map(function () { return 0; });
967
- for (var it2 = 0; it2 < nodes.length + 1; it2++) {
968
- var ch = false;
969
- flows.forEach(function (f) {
970
- var a = idx[f[0]], b = idx[f[1]];
971
- if (level[b] < level[a] + 1) { level[b] = level[a] + 1; ch = true; }
972
- });
973
- if (!ch) break;
974
- }
975
- var maxLv = Math.max.apply(null, level.concat([0]));
976
- var colW2 = CW / (maxLv + 1);
977
- var geo = {};
978
- for (var l = 0; l <= maxLv; l++) {
979
- var lv = nodes.filter(function (n, i) { return level[i] === l; });
980
- var rowH2 = Math.max(0.24, (P.y1 - P.y0) / Math.max(1, lv.length));
981
- lv.forEach(function (n, k2) { geo[n] = { x: MX + l * colW2 + colW2 * 0.5 - 0.16, y: P.y0 + k2 * rowH2, h: rowH2 * 0.7 }; });
982
- }
983
- flows.forEach(function (f) {
984
- var a = geo[f[0]], b = geo[f[1]];
985
- if (!a || !b) return;
986
- sh.push(lineSeg(a.x + 0.32, a.y + a.h / 2, b.x, b.y + b.h / 2, S.soft, 8));
987
- /* 流量标签:与 B 通道同文本(数值 + 单位,由 cross_verify 过滤) */
988
- sh.push(txSp((a.x + b.x) / 2 - 0.5, (a.y + b.y) / 2 - 0.14, 1.0, 0.28,
989
- [[{ t: String(f[2]) + (sec.unit || ''), sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
990
- });
991
- nodes.forEach(function (n) {
992
- var g = geo[n];
993
- sh.push(shape('rect', g.x, g.y, 0.32, g.h, S.accent));
994
- sh.push(txSp(g.x + 0.38, g.y, Math.min(1.4, colW2 * 0.5), g.h,
995
- [[{ t: n, sz: 10.5, b: 1, col: S.body, font: S.font }]], { anchor: 'ctr' }));
996
- });
997
- return;
998
- }
999
- if (t === 'treemap') {
1000
- var items = (sec.items || []).map(function (it) { return { label: String(it[0]), v: Math.max(0, Number(it[1]) || 0) }; })
1001
- .filter(function (i) { return i.v > 0; }).sort(function (a, b) { return b.v - a.v; });
1002
- var out = [];
1003
- tmSplitA(items, MX, P.y0, CW, P.y1 - P.y0, out);
1004
- out.forEach(function (o) {
1005
- sh.push(shape('rect', o.x + 0.03, o.y + 0.03, Math.max(0.05, o.w - 0.06), Math.max(0.05, o.h - 0.06), S.surface, { line: S.line }));
1006
- sh.push(txSp(o.x + 0.1, o.y + 0.06, Math.max(0.2, o.w - 0.2), 0.28, [[{ t: o.it.label, sz: 10.5, b: 1, col: S.ink, font: S.font }]]));
1007
- sh.push(txSp(o.x + 0.1, o.y + 0.32, Math.max(0.2, o.w - 0.2), 0.26,
1008
- [[{ t: String(o.it.v) + (sec.unit || ''), sz: 10, col: S.body, font: S.font }]]));
1009
- });
1010
- return;
1011
- }
1012
- if (t === 'boxplot') {
1013
- var groups = (sec.groups || []).filter(function (g) { return g && g.length >= 6; });
1014
- var step = CW / Math.max(1, groups.length);
1015
- var plotH = (P.y1 - P.y0) - 0.4;
1016
- groups.forEach(function (g, i) {
1017
- var cx = MX + step * i + step / 2;
1018
- var bw = Math.min(0.9, step * 0.5);
1019
- sh.push(shape('rect', cx - 0.012, P.y0, 0.024, plotH, S.faint));
1020
- sh.push(shape('rect', cx - bw / 2, P.y0 + plotH * 0.2, bw, plotH * 0.6, S.soft, { line: S.accent }));
1021
- sh.push(shape('rect', cx - bw / 2, P.y0 + plotH * 0.48, bw, 0.03, S.accent));
1022
- sh.push(txSp(cx - step / 2, P.y1 - 0.34, step, 0.28,
1023
- [[{ t: String(g[0]), sz: 10.5, col: S.body, font: S.font }]], { align: 'ctr' }));
1024
- });
1025
- return;
1026
- }
1027
- if (t === 'network') {
1028
- var nds = sec.nodes || [];
1029
- var n2 = Math.max(1, nds.length);
1030
- var cx0 = MX + CW / 2, cy0 = (P.y0 + P.y1) / 2;
1031
- var r = Math.min(CW / 2, (P.y1 - P.y0) / 2) - 1.0;
1032
- var pos = {};
1033
- nds.forEach(function (nd, i) {
1034
- var a = -Math.PI / 2 + i * 2 * Math.PI / n2;
1035
- pos[String(nd[0])] = { x: cx0 + r * Math.cos(a), y: cy0 + r * Math.sin(a) };
1036
- });
1037
- (sec.edges || []).forEach(function (e) {
1038
- var a = pos[String(e[0])], b = pos[String(e[1])];
1039
- if (!a || !b) return;
1040
- sh.push(lineSeg(a.x, a.y, b.x, b.y, S.line, 1.5));
1041
- });
1042
- nds.forEach(function (nd) {
1043
- var p = pos[String(nd[0])];
1044
- sh.push(shape('ellipse', p.x - 0.26, p.y - 0.26, 0.52, 0.52, S.accent));
1045
- var outward = p.x >= cx0;
1046
- sh.push(txSp(outward ? p.x + 0.32 : p.x - 0.32 - 1.4, p.y - 0.14, 1.4, 0.28,
1047
- [[{ t: String(nd[1] != null ? nd[1] : nd[0]), sz: 10, col: S.body, font: S.font }]],
1048
- { align: outward ? 'l' : 'r', anchor: 'ctr' }));
1049
- });
1050
- return;
1051
- }
1052
- if (t === 'marimekko') {
1053
- var cols = sec.cols || [];
1054
- var legend = (sec.legend || []).map(String);
1055
- var total = cols.reduce(function (a, c) { return a + (Number(c[1]) || 0); }, 0) || 1;
1056
- var plotH2 = (P.y1 - P.y0) - 0.6;
1057
- var availW = CW - 0.06 * Math.max(0, cols.length - 1);
1058
- var cx1 = MX;
1059
- cols.forEach(function (c, i) {
1060
- var cw = Math.max(0.2, (Number(c[1]) || 0) / total * availW);
1061
- var shares = ((sec.cells || [])[i] || []).map(Number);
1062
- var sum = shares.reduce(function (a, b) { return a + b; }, 0) || 1;
1063
- var cy = P.y0;
1064
- shares.forEach(function (v, k) {
1065
- var shh = Math.max(0.02, v / sum * plotH2);
1066
- sh.push(shape('rect', cx1, cy, cw, shh, palette[k % palette.length]));
1067
- cy += shh;
1068
- });
1069
- sh.push(txSp(cx1, P.y0 + plotH2 + 0.06, cw, 0.28,
1070
- [[{ t: String(c[0]), sz: 10.5, b: 1, col: S.body, font: S.font }]], { align: 'ctr' }));
1071
- cx1 += cw + 0.06;
1072
- });
1073
- var lx = MX;
1074
- legend.forEach(function (lg, k) {
1075
- sh.push(shape('rect', lx, P.y1 - 0.2, 0.12, 0.12, palette[k % palette.length]));
1076
- sh.push(txSp(lx + 0.16, P.y1 - 0.26, 1.5, 0.26, [[{ t: lg, sz: 10, col: S.body, font: S.font }]], { anchor: 'ctr' }));
1077
- lx += 1.75;
1078
- });
1079
- return;
1080
- }
1081
- if (t === 'streamgraph') {
1082
- var series = sec.series || [];
1083
- var labels = sec.labels || [];
1084
- var nPts = labels.length || ((series[0] && series[0].values || []).length);
1085
- if (nPts >= 2) {
1086
- var values = series.map(function (se) {
1087
- return (se.values || []).map(function (v) { return Math.max(0, Number(v) || 0); });
1088
- });
1089
- var totals = [];
1090
- for (var i2 = 0; i2 < nPts; i2++) {
1091
- var tt = 0;
1092
- values.forEach(function (v) { tt += (v[i2] || 0); });
1093
- totals.push(tt);
1094
- }
1095
- var maxTot = Math.max.apply(null, totals.concat([1]));
1096
- var plotH3 = P.y1 - P.y0;
1097
- var cyMid = (P.y0 + P.y1) / 2;
1098
- var base = totals.map(function (tv) { return cyMid - tv / maxTot * plotH3 / 2; });
1099
- series.forEach(function (se, si) {
1100
- var upper = base.map(function (bv, i3) { return bv + (values[si][i3] || 0) / maxTot * plotH3 / 2; });
1101
- var stepX = CW / (nPts - 1);
1102
- for (var i4 = 0; i4 < nPts - 1; i4++) {
1103
- var hgt = Math.max(0.04, ((upper[i4] - base[i4]) + (upper[i4 + 1] - base[i4 + 1])) / 2);
1104
- sh.push(shape('rect', MX + i4 * stepX, (base[i4] + base[i4 + 1]) / 2, stepX, hgt, palette[si % palette.length]));
1105
- }
1106
- base = upper;
1107
- sh.push(shape('rect', MX, P.y1 + 0.06 + si * 0.26, 0.12, 0.12, palette[si % palette.length]));
1108
- sh.push(txSp(MX + 0.16, P.y1 + si * 0.26, 1.5, 0.26,
1109
- [[{ t: String(se.name || ('系列' + (si + 1))), sz: 10, col: S.body, font: S.font }]], { anchor: 'ctr' }));
1110
- });
1111
- labels.forEach(function (lb, i5) {
1112
- sh.push(txSp(MX + (nPts > 1 ? i5 / (nPts - 1) : 0.5) * CW - 0.4, P.y1 - 0.02, 0.8, 0.24,
1113
- [[{ t: String(lb), sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
1114
- });
1115
- }
1116
- return;
1117
- }
1118
- }
1119
- /* 统一图表分发(与 scripts/layout-constants.json 的 charts.registry 同分派规则)
1120
- * A 通道为形状近似:类别标签与数值都落为文本,数值 token 由 cross_verify 过滤;
1121
- * 复杂信息图(sankey / treemap / boxplot / network / marimekko / streamgraph)请用专属页型承载。 */
1122
- function chartShapes(sh, ec, S, x, y, w, h, dcols) {
1123
- var t = (ec.type || 'bar').toLowerCase();
1124
- var reg = (typeof REG !== 'undefined' && REG && REG[t]) ? REG[t] : null;
1125
- if (t === 'hbar') { hbarRows(sh, ec, S, y, y + h, dcols, x, w); return; }
1126
- if (t === 'line' || t === 'dualline' || t === 'area') { lineShapes(sh, ec, S, x, y, w, h, dcols); return; }
1127
- if (t === 'scatter' || t === 'bubble') { xyDots(sh, ec, S, x, y, w, h); return; }
1128
- if (t === 'gauge') { gaugeApprox(sh, ec, S, x, y, w, h); return; }
1129
- if (t === 'pie' || t === 'donut' || t === 'multidonut') { pieApprox(sh, ec, S, x, y, w, h, dcols); return; }
1130
- /* sparkline / candlestick 的文本集须与 B 通道专属渲染器一致(否则 cross_verify 误报) */
1131
- if (t === 'sparkline') { sparkApprox(sh, ec, S, x, y, w, h); return; }
1132
- if (t === 'candlestick') { candleApprox(sh, ec, S, x, y, w, h); return; }
1133
- if (reg && reg.pptx === 'shape') { shapeProportion(sh, ec, S, x, y, w, h, dcols); return; }
1134
- vbarShapes(sh, ec, S, x, y, w, h, dcols);
1135
- }
1136
-
1137
- /* 区域 IR 取用(与 scripts/lib_layout_regions.js 同源;未注入时回落 null) */
1138
- function regOf(type, slot, opts) {
1139
- return (typeof regionOf === 'function') ? regionOf(type, slot, opts) : null;
1140
- }
1141
-
1142
- /* ── 幻灯片生成(版式常量同 build_pptx.js · 同一 layout-constants.json) ── */
1143
- function slidesOf(model, S) {
1144
- var slides = [];
1145
- _mode = model.mode || 'presentation';
1146
- function head(sh, eyebrow, title, lead) {
1147
- var H = (typeof regionOf === 'function') ? regionOf('head', 'head') : {
1148
- x: MX, y: PT.common.headEyebrowY, w: CW,
1149
- titleY: PT.common.headTitleY, ruleY: PT.common.headRuleY, leadY: PT.common.leadY,
1150
- };
1151
- if (eyebrow) sh.push(txSp(H.x, H.y, H.w, 0.4, [[{ t: eyebrow, sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1152
- var titleH = eyebrow ? (H.ruleY - H.titleY - 0.06) : 1.0;
1153
- var tSize = estTextH(title, CW, 30, 1.25) > titleH ? 24 : 30;
1154
- sh.push(txSp(H.x, eyebrow ? H.titleY : 0.6, H.w, Math.max(0.6, titleH), [[{ t: title, sz: tSize, b: 1, col: S.ink, font: S.fontDisplay }]]));
1155
- sh.push(shape('rect', H.x, eyebrow ? H.ruleY : 1.6, 0.9, 0.045, S.accent));
1156
- if (lead) sh.push(txSp(H.x, H.leadY || PT.common.leadY, H.w, 0.5, [[{ t: lead, sz: 14, col: S.body, font: S.font }]]));
1157
- }
1158
-
1159
- /* 封面 */
1160
- var CV = PT.cover;
1161
- var c = [shape('rect', 0, 0, PW, PH, S.accent)];
1162
- c.push(txSp(MX, CV.metaY, CW, 0.4, [[{ t: model.meta || '', sz: 12, col: S.soft, font: S.font }]]));
1163
- var cTSize = estTextH(model.title, CW - 1, 44, 1.25) > CV.titleH ? 36 : 44;
1164
- c.push(txSp(MX, CV.titleY, CW - 1, CV.titleH, [[{ t: model.title, sz: cTSize, b: 1, col: S.onAccent, font: S.fontDisplay }]]));
1165
- c.push(txSp(MX, CV.subtitleY, CW - 1, CV.subtitleH, [[{ t: model.subtitle || '', sz: 18, col: S.soft, font: S.font }]]));
1166
- slides.push(c);
1167
-
1168
- /* Agenda(>8 条自动双列,行高自适应防溢出;architecture 极简形态可省略 agenda) */
1169
- var items = model.agenda || [];
1170
- if (items.length) {
1171
- var ag = [];
1172
- head(ag, 'AGENDA', '报告大纲');
1173
- var colN = items.length > 8 ? 2 : 1;
1174
- var perCol = Math.ceil(items.length / colN) || 1;
1175
- var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
1176
- var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
1177
- var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
1178
- items.forEach(function (it, i) {
1179
- var col = Math.floor(i / perCol), row = i % perCol;
1180
- var x = MX + col * (CW / colN), y = CONTENT_TOP + row * rowH;
1181
- ag.push(txSp(x, y, 0.9, Math.min(0.9, rowH), [[{ t: it[0], sz: numSz, b: 1, col: S.accent, font: S.font }]]));
1182
- ag.push(txSp(x + 1.0, y + 0.03, CW / colN - 1.1, Math.min(0.95, rowH),
1183
- [[{ t: it[1], sz: tSz, b: 1, col: S.ink, font: S.font }],
1184
- [{ t: it[2] || '', sz: dSz, col: S.body, font: S.font }]], { lineSpacing: tSz + 5 }));
1185
- });
1186
- slides.push(ag);
1187
- }
1188
-
1189
- /* 章节页 */
1190
- (model.sections || []).forEach(function (sec) {
1191
- var type = sec.type || 'points';
1192
- var sh = [];
1193
- if (type !== 'quote') head(sh, sec.eyebrow, sec.title, sec.lead);
1194
- /* 非 exhibit 页型也可带 Exhibit 编号(帧头徽标;正文区下移让位,与 B 通道同规则) */
1195
- var exBadge = (type !== 'exhibit' && sec.exhibitNo) ? String(sec.exhibitNo).toUpperCase() : null;
1196
- var bodyY0 = sec.lead ? PT.common.bodyYWithLead : CONTENT_TOP;
1197
- var bodyY = exBadge ? bodyY0 + 0.42 : bodyY0;
1198
- if (exBadge) {
1199
- sh.push(txSp(MX, CONTENT_TOP - 0.06, CW, 0.3,
1200
- [[{ t: exBadge, sz: 11.5, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1201
- }
1202
- var flagItems = Array.isArray(sec.flags) ? sec.flags.filter(Boolean) : [];
1203
- var _F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
1204
- var flagH = flagItems.length
1205
- ? (_F.hdH + Math.min(_F.maxRows, flagItems.length) * _F.rowH + 0.06) : 0;
1206
- var bodyBottom = chartBottom(!!sec.soWhat, !!sec.footnote);
1207
- var flagY = 0;
1208
- if (flagH) {
1209
- flagY = CONTENT_BOTTOM - flagH;
1210
- if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
1211
- if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
1212
- bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
1213
- }
1214
-
1215
- if (type === 'metrics') {
1216
- var M = PT.metrics, ms = sec.metrics || [], n = Math.max(1, ms.length);
1217
- var mReg = regOf('metrics', 'primary') || { x: MX, y: M.bandY, w: CW, h: M.cardH };
1218
- var g = cols(mReg.w, n, M.gap);
1219
- var mw = Math.min(M.maxW, g.w);
1220
- var gx = mReg.x + (mReg.w - n * mw - (n - 1) * M.gap) / 2;
1221
- var cardH = Math.min(mReg.h, CONTENT_BOTTOM - mReg.y);
1222
- ms.forEach(function (m, i) {
1223
- var x = gx + i * (mw + M.gap);
1224
- sh.push(shape('roundRect', x, mReg.y, mw, cardH, S.surface, { line: S.line, adj: 4000 }));
1225
- var vSize = estTextH(m[0], mw - 0.2, 40, 1.1) > cardH * 0.5 ? 30 : 40;
1226
- sh.push(txSp(x, M.valY, mw, M.valH, [[{ t: m[0], sz: vSize, b: 1, col: S.accent, font: S.fontDisplay }]], { align: 'ctr' }));
1227
- sh.push(txSp(x + 0.15, M.capY, mw - 0.3, M.capH, [[{ t: m[1], sz: 11.5, col: S.body, font: S.font }]], { align: 'ctr' }));
1228
- });
1229
- } else if (type === 'kpi') {
1230
- var K = PT.kpi, hro = sec.hero || [];
1231
- var hReg = regOf('kpi', 'hero') || { x: MX, y: K.heroY, w: CW * K.heroW, h: K.heroH };
1232
- sh.push(txSp(hReg.x, hReg.y, hReg.w, hReg.h, [[{ t: String(hro[0] || ''), sz: 64, b: 1, col: S.accent, font: S.fontDisplay }]]));
1233
- sh.push(txSp(hReg.x + 0.05, hReg.y + 1.5, hReg.w - 0.4, 0.55, [[{ t: String(hro[1] || ''), sz: 15, b: 1, col: S.ink, font: S.font }]]));
1234
- if (hro[2]) sh.push(txSp(hReg.x + 0.05, hReg.y + 2.1, hReg.w - 0.4, 0.4, [[{ t: '▲ ' + hro[2], sz: 12, b: 1, col: S.accent, font: S.font }]]));
1235
- var kdivX = MX + CW * K.dividerX;
1236
- sh.push(shape('rect', kdivX, K.heroY + 0.15, 0.025, 3.1, S.line));
1237
- var mRegK = regOf('kpi', 'metrics') || { x: kdivX + 0.35, y: K.metricY0, w: CW * (1 - K.dividerX) - 0.5 };
1238
- var kmets = sec.metrics || [];
1239
- var kCap = Math.max(1, Math.floor((CONTENT_BOTTOM - mRegK.y) / K.metricRowH));
1240
- var kRows = Math.min(kmets.length, kCap);
1241
- var kRowH = fitRowH(CONTENT_BOTTOM - mRegK.y, kRows, K.metricRowH, 0.42);
1242
- var kTopH = Math.min(0.55, kRowH * 0.62);
1243
- kmets.slice(0, kRows).forEach(function (m, i) {
1244
- var ky = mRegK.y + i * kRowH;
1245
- sh.push(txSp(mRegK.x, ky, mRegK.w, kTopH, [[{ t: m[0], sz: 22, b: 1, col: S.accent, font: S.fontDisplay }]]));
1246
- sh.push(txSp(mRegK.x, ky + kTopH, mRegK.w, Math.max(0.24, kRowH - kTopH), [[{ t: m[1], sz: 10.5, col: S.faint, font: S.font }]]));
1247
- });
1248
- } else if (type === 'comparison') {
1249
- var C2 = PT.comparison;
1250
- var lR = regOf('comparison', 'left') || { x: MX, y: C2.panelY, w: (CW - C2.panelGap) / 2, h: C2.panelH };
1251
- var rR = regOf('comparison', 'right') || { x: MX + (CW + C2.panelGap) / 2, y: C2.panelY, w: (CW - C2.panelGap) / 2, h: C2.panelH };
1252
- var cw2 = lR.w, cpy = lR.y, cph = lR.h, rx2 = rR.x;
1253
- sh.push(shape('roundRect', lR.x, cpy, cw2, cph, S.surface, { adj: 4000, line: S.line }));
1254
- sh.push(shape('roundRect', rx2, cpy, cw2, cph, S.soft, { adj: 4000 }));
1255
- [{ x: lR.x, d: (sec.left || {}) }, { x: rx2, d: (sec.right || {}) }].forEach(function (sd) {
1256
- var isRight = sd.x === rx2;
1257
- var pts = sd.d.points || [];
1258
- var avail = cph - C2.titleH - 0.34;
1259
- var rowH2 = fitRowH(avail, pts.length, C2.rowH, 0.34);
1260
- var fz = rowH2 < 0.5 ? 11.5 : 13;
1261
- sh.push(txSp(sd.x + 0.22, cpy + 0.18, cw2 - 0.44, C2.titleH,
1262
- [[{ t: sd.d.title || '', sz: 15, b: 1, col: isRight ? S.accent : S.ink, font: S.font }]]));
1263
- pts.forEach(function (p, pi) {
1264
- var py = cpy + 0.18 + C2.titleH + 0.16 + pi * rowH2;
1265
- sh.push(shape('rect', sd.x + 0.24, py + rowH2 / 2 - 0.06, 0.12, 0.12, isRight ? S.accent : S.faint));
1266
- sh.push(txSp(sd.x + 0.5, py, cw2 - 0.75, rowH2 - 0.04,
1267
- [[{ t: p[0] + ' ', sz: fz, b: 1, col: S.ink, font: S.font },
1268
- { t: p[1], sz: fz - 1, col: S.body, font: S.font }]]));
1269
- });
1270
- });
1271
- if (sec.verdict) {
1272
- var vR = regOf('exhibit', 'annotation') || { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
1273
- sh.push(shape('rect', vR.x, vR.y, vR.w, vR.h, S.accent));
1274
- sh.push(txSp(vR.x + 0.22, vR.y + 0.06, vR.w - 0.44, vR.h - 0.12,
1275
- [[{ t: '结论 ', sz: 11, b: 1, col: S.onAccent, font: S.font, spc: 150 },
1276
- { t: sec.verdict, sz: 12.5, b: 1, col: S.onAccent, font: S.font }]], { anchor: 'ctr' }));
1277
- }
1278
- } else if (type === 'quote') {
1279
- var Q = PT.quote;
1280
- var qR = regOf('quote', 'primary') || { x: MX + 1.05, y: Q.textY, w: CW - 2.1, h: Q.textH };
1281
- /* 金句页用主题一致强调带(accent-soft 底)——浅色主题下不再出现深色页 */
1282
- sh.push(shape('rect', 0, 0, PW, PH, S.soft));
1283
- if (sec.eyebrow) sh.push(txSp(MX, PT.common.headEyebrowY, CW, 0.4, [[{ t: sec.eyebrow, sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1284
- sh.push(txSp(MX, PT.common.headTitleY, CW, 1.0, [[{ t: sec.title, sz: 30, b: 1, col: S.ink, font: S.fontDisplay }]]));
1285
- sh.push(shape('rect', MX, PT.common.headRuleY, 0.9, 0.045, S.accent));
1286
- sh.push(txSp(MX + 0.05, Q.markY, 1.2, 1.1, [[{ t: '「', sz: 60, b: 1, col: S.accent, font: S.fontDisplay }]]));
1287
- var qSize = estTextH(sec.quote, qR.w, 20, 1.4) > qR.h ? 16 : 20;
1288
- sh.push(txSp(qR.x, qR.y, qR.w, qR.h, [[{ t: sec.quote, sz: qSize, b: 1, col: S.ink, font: S.fontDisplay }]], { anchor: 'ctr' }));
1289
- sh.push(txSp(qR.x, Q.authorY, qR.w, 0.45, [[{ t: '—— ' + sec.author, sz: 13, b: 1, col: S.accent, font: S.font }]], { align: 'r' }));
1290
- if (sec.context) sh.push(txSp(qR.x, Q.contextY, qR.w, 0.4, [[{ t: sec.context, sz: 11, col: S.faint, font: S.font }]], { align: 'r' }));
1291
- } else if (type === 'image') {
1292
- /* 素材图片页:full 版心全宽 / half 左图右注 / bleed 通栏出血 / grid 多图网格 /
1293
- compare 双图对比 / wall Logo 墙;无图(image.placeholder)走原生占位块 + 标签文本 */
1294
- var IM = PT.image, img = sec.image || {};
1295
- var items = Array.isArray(img.items) ? img.items.filter(Boolean) : [];
1296
- var layout = String(img.layout || (items.length > 1 ? 'grid' : 'full')).toLowerCase();
1297
- var fit = String(img.fit || ((typeof IMG !== 'undefined' && IMG && IMG.fitDefault) || 'cover')).toLowerCase();
1298
- var isPh = !!img.placeholder;
1299
- var iR = regOf('image', 'primary', { top: bodyY, caption: !!img.caption }) ||
1300
- { x: MX, y: Math.max(IM.y, bodyY), w: CW, h: IM.h };
1301
- var iy = iR.y;
1302
- var capH2 = img.caption ? 0.32 : 0;
1303
- var ih = Math.max(1.2, iR.h);
1304
- imageLayoutShapes(sh, S, layout, img, items, sec.points || [], isPh, fit, iy, iy + ih);
1305
- if (img.caption) sh.push(txSp(MX, iy + ih + 0.04, CW, capH2, [[{ t: img.caption, sz: 10.5, col: S.faint, font: S.font }]]));
1306
- } else if (type === 'donut') {
1307
- var dcn = sec.chart || {};
1308
- if (dcn.labels && dcn.values) donutChart(sh, dcn, S, dcn.colors || donutColors(model.style, S, model.theme));
1309
- if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
1310
- } else if (type === 'heatmap') {
1311
- var H = PT.heatmap;
1312
- var hR = regOf('heatmap', 'primary', { bottom: bodyBottom }) || { x: MX, y: H.y, w: CW, h: bodyBottom - H.y };
1313
- var rowsH = sec.rowHeads || [], colsH = sec.colHeads || [], cells = sec.cells || [];
1314
- var nR = Math.max(1, rowsH.length), nC = Math.max(1, colsH.length);
1315
- var availH = hR.h;
1316
- var rowH3 = fitRowH(availH - H.headH, nR, H.cellH, 0.24);
1317
- var gridW = hR.w - H.labelW;
1318
- var cw3 = (gridW - (nC - 1) * H.cellGap) / nC;
1319
- var flat = [], k2;
1320
- for (k2 = 0; k2 < cells.length; k2++) for (var j2 = 0; j2 < (cells[k2] || []).length; j2++) { var nv = Number(cells[k2][j2]); if (!isNaN(nv)) flat.push(nv); }
1321
- var vmax3 = flat.length ? Math.max.apply(null, flat) : 1;
1322
- var vmin3 = flat.length ? Math.min.apply(null, flat) : 0;
1323
- var levels = [S.surface, S.soft, S.line, S.accent];
1324
- colsH.forEach(function (ch2, ci) {
1325
- sh.push(txSp(hR.x + H.labelW + ci * (cw3 + H.cellGap), hR.y, cw3, H.headH,
1326
- [[{ t: ch2, sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1327
- });
1328
- var rowsUse = Math.min(nR, cells.length);
1329
- for (var ri = 0; ri < rowsUse; ri++) {
1330
- var yy = hR.y + H.headH + 0.08 + ri * (rowH3 + H.cellGap);
1331
- sh.push(txSp(hR.x, yy, H.labelW - 0.15, rowH3, [[{ t: rowsH[ri] || '', sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
1332
- for (var ci2 = 0; ci2 < nC; ci2++) {
1333
- var v2 = Number((cells[ri] || [])[ci2]);
1334
- var has2 = !isNaN(v2);
1335
- var p2 = (has2 && vmax3 > vmin3) ? (v2 - vmin3) / (vmax3 - vmin3) : 0.5;
1336
- var lvl = p2 >= 0.86 ? 3 : (p2 >= 0.55 ? 2 : (p2 >= 0.25 ? 1 : 0));
1337
- var cx2 = hR.x + H.labelW + ci2 * (cw3 + H.cellGap);
1338
- sh.push(shape('roundRect', cx2, yy, cw3, rowH3, levels[lvl],
1339
- { adj: 3000, line: lvl === 0 ? S.line : undefined }));
1340
- sh.push(txSp(cx2, yy, cw3, rowH3,
1341
- [[{ t: has2 ? valText(v2, sec.unit) : '—', sz: 11.5, b: lvl >= 2 ? 1 : 0,
1342
- col: lvl >= 3 ? S.onAccent : S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1343
- }
1344
- }
1345
- var lg = sec.scaleLabel || ['低', '高'];
1346
- var lgy = hR.y + H.headH + 0.08 + rowsUse * (rowH3 + H.cellGap) + 0.06;
1347
- sh.push(txSp(hR.x, lgy, 0.6, 0.3, [[{ t: lg[0] || '低', sz: 9.5, col: S.faint, font: S.font }]]));
1348
- for (var li2 = 0; li2 < 4; li2++) {
1349
- sh.push(shape('rect', hR.x + 0.45 + li2 * 0.3, lgy + 0.04, 0.28, 0.16, levels[li2],
1350
- { line: li2 === 0 ? S.line : undefined }));
1351
- }
1352
- sh.push(txSp(hR.x + 1.75, lgy, 0.6, 0.3, [[{ t: lg[1] || '高', sz: 9.5, col: S.faint, font: S.font }]]));
1353
- } else if (type === 'bullet') {
1354
- var B = PT.bullet, its = sec.items || [], nB = Math.max(1, its.length);
1355
- var bR = regOf('bullet', 'primary', { bottom: bodyBottom }) || { x: MX, y: B.y, w: CW, h: bodyBottom - B.y };
1356
- var availB = bR.h;
1357
- var rowHB = fitRowH(availB, nB, B.rowH, 0.26);
1358
- var trackX = bR.x + B.labelW + 0.15;
1359
- var trackW = bR.w - B.labelW - B.valW - 0.3;
1360
- var nums = [], q2;
1361
- for (q2 = 0; q2 < its.length; q2++) { var nq = Number(its[q2][1]); if (!isNaN(nq)) nums.push(nq); }
1362
- var vmaxB = sec.max || (nums.length ? Math.max.apply(null, nums) : 1);
1363
- its.forEach(function (it, i) {
1364
- var yy2 = bR.y + i * rowHB + rowHB / 2 - B.barH / 2;
1365
- var v = Number(it[1]) || 0, tgt = Number(it[2]);
1366
- var hit = !isNaN(tgt) && v >= tgt;
1367
- sh.push(txSp(bR.x, bR.y + i * rowHB, B.labelW, rowHB, [[{ t: String(it[0]), sz: 12, b: 1, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
1368
- sh.push(shape('roundRect', trackX, yy2, trackW, B.barH, S.surface, { adj: 50000, line: S.line }));
1369
- var fw = Math.max(0.02, Math.min(1, v / (vmaxB || 1)) * trackW);
1370
- sh.push(shape('roundRect', trackX, yy2, fw, B.barH, hit ? S.accent : S.faint, { adj: 50000 }));
1371
- if (!isNaN(tgt)) {
1372
- var tx2 = trackX + Math.min(1, tgt / (vmaxB || 1)) * trackW;
1373
- sh.push(shape('rect', tx2 - 0.01, yy2 - 0.07, 0.02, B.barH + 0.14, S.body));
1374
- }
1375
- sh.push(txSp(trackX + trackW + 0.12, bR.y + i * rowHB, B.valW, rowHB,
1376
- [[{ t: String(it[1]) + (sec.unit || ''), sz: 12, b: 1, col: hit ? S.accent : S.body, font: S.font },
1377
- { t: isNaN(tgt) ? '' : ' / ' + tgt + (sec.unit || ''), sz: 10, col: S.faint, font: S.font }]],
1378
- { align: 'r', anchor: 'ctr' }));
1379
- });
1380
- } else if (type === 'pyramid') {
1381
- var P = PT.pyramid, lv = sec.levels || [], nP = Math.max(1, lv.length);
1382
- var pR = regOf('pyramid', 'primary', { bottom: bodyBottom }) || { x: MX, y: P.y, w: CW, h: bodyBottom - P.y };
1383
- var availP = pR.h;
1384
- var rowHP = fitRowH(availP - (nP - 1) * P.gap, nP, P.rowH, 0.26);
1385
- lv.forEach(function (l, i) {
1386
- var frac = P.minW + (1 - P.minW) * (nP === 1 ? 1 : i / (nP - 1));
1387
- var w2 = pR.w * frac, x2 = pR.x + (pR.w - w2) / 2, y2 = pR.y + i * (rowHP + P.gap);
1388
- var acc = isRec(l) && l.accent;
1389
- var lt = isRec(l) ? (l.t || '') : String(l[0] || '');
1390
- var ld = isRec(l) ? (l.d || '') : String(l[1] || '');
1391
- sh.push(shape('roundRect', x2, y2, w2, rowHP, acc ? S.soft : S.surface, { adj: 5000, line: acc ? undefined : S.line }));
1392
- var lwP = Math.min(P.labelW, w2 - 0.44);
1393
- sh.push(txSp(x2 + 0.22, y2, lwP, rowHP, [[{ t: lt, sz: 13.5, b: 1, col: acc ? S.accent : S.ink, font: S.font }]], { anchor: 'ctr' }));
1394
- if (ld) sh.push(txSp(x2 + 0.22 + lwP, y2, w2 - 0.44 - lwP, rowHP, [[{ t: ld, sz: 11, col: S.faint, font: S.font }]], { anchor: 'ctr' }));
1395
- });
1396
- } else if (type === 'steps') {
1397
- var S2 = PT.steps, st = sec.steps || [], maxPer = S2.maxPerRow || 6, groups = sec.groups || [];
1398
- var sR = regOf('steps', 'primary') || { x: MX, y: S2.y, w: CW, h: S2.barH };
1399
- var chunks = [];
1400
- if (groups.length) {
1401
- var kk = 0;
1402
- groups.forEach(function (gp) { var cnt = Number(gp[1]) || 0; chunks.push({ label: gp[0], items: st.slice(kk, kk + cnt) }); kk += cnt; });
1403
- if (kk < st.length) chunks.push({ label: '', items: st.slice(kk) });
1404
- } else {
1405
- for (var i3 = 0; i3 < st.length; i3 += maxPer) chunks.push({ label: '', items: st.slice(i3, i3 + maxPer) });
1406
- }
1407
- var rowsN = Math.max(1, chunks.length);
1408
- var availS = bodyBottom - sR.y;
1409
- var rowHS = fitRowH(availS - (rowsN - 1) * 0.3, rowsN, sR.h || S2.barH, 0.3);
1410
- var cyS = sR.y;
1411
- chunks.forEach(function (ck) {
1412
- if (ck.label) {
1413
- sh.push(txSp(sR.x, cyS - 0.02, sR.w, 0.26, [[{ t: ck.label, sz: 10, b: 1, col: S.faint, font: S.font, spc: 100 }]]));
1414
- cyS += 0.28;
1415
- }
1416
- var n4 = Math.max(1, ck.items.length);
1417
- var g4 = cols(sR.w, n4, S2.gap + S2.arrowW);
1418
- ck.items.forEach(function (it, i) {
1419
- var acc = isRec(it) && it.accent;
1420
- var t4 = isRec(it) ? (it.t || '') : String(it[0] || '');
1421
- var d4 = isRec(it) ? (it.d || '') : String(it[1] || '');
1422
- var x4 = sR.x + i * g4.step;
1423
- sh.push(shape('roundRect', x4, cyS, g4.w, rowHS, acc ? S.soft : S.surface, { adj: 5000, line: acc ? undefined : S.line }));
1424
- var num = String(i + 1); if (num.length < 2) num = '0' + num;
1425
- var pd = S2.pad, numH = S2.numH, ttlH = S2.titleH;
1426
- sh.push(txSp(x4 + 0.14, cyS + pd, g4.w - 0.28, numH, [[{ t: num, sz: 10, b: 1, col: S.accent, font: S.font }]]));
1427
- sh.push(txSp(x4 + 0.14, cyS + pd + numH, g4.w - 0.28, ttlH, [[{ t: t4, sz: 13, b: 1, col: acc ? S.accent : S.ink, font: S.font }]]));
1428
- if (d4) sh.push(txSp(x4 + 0.14, cyS + pd + numH + ttlH, g4.w - 0.28, Math.max(0.2, rowHS - pd * 2 - numH - ttlH), [[{ t: d4, sz: 10, col: S.faint, font: S.font }]]));
1429
- if (i < n4 - 1) {
1430
- sh.push(txSp(x4 + g4.w, cyS, S2.arrowW + S2.gap, rowHS, [[{ t: '→', sz: 12, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1431
- }
1432
- });
1433
- cyS += rowHS + 0.3;
1434
- });
1435
- } else if (type === 'table') {
1436
- var tR = regOf('table', 'primary', { bottom: bodyBottom }) || { x: MX, y: CONTENT_TOP, w: CW, h: bodyBottom - CONTENT_TOP };
1437
- var headRow = sec.table.head;
1438
- var colW = sec.table.colW || headRow.map(function () { return tR.w / headRow.length; });
1439
- sh.push(table(tR.x, tR.y, tR.w, colW, [headRow].concat(sec.table.rows), S, { availH: tR.h }));
1440
- } else if (type === 'timeline') {
1441
- var T = PT.timeline, ps = sec.phases || [], np = Math.max(1, ps.length);
1442
- var tLR = regOf('timeline', 'primary') || { x: MX, y: T.labelY, w: CW };
1443
- var stepW = (tLR.w - 1.0) / np;
1444
- sh.push(shape('rect', tLR.x + 0.2, T.axisY, tLR.w - 0.4, 0.02, S.line));
1445
- ps.forEach(function (p, i) {
1446
- var x5 = tLR.x + 0.2 + i * stepW;
1447
- var on = p[3] === 'done' || p[3] === 'now';
1448
- sh.push(shape('ellipse', x5 - T.dotR / 2, T.dotY, T.dotR, T.dotR, on ? S.accent : S.line));
1449
- sh.push(txSp(x5 - 0.1, T.labelY, stepW - 0.2, 0.4, [[{ t: p[0], sz: 12, b: 1, col: S.accent, font: S.font, spc: 100 }]]));
1450
- sh.push(txSp(x5 - 0.1, T.nameY, stepW - 0.2, 0.5, [[{ t: p[1], sz: 17, b: 1, col: p[3] === 'now' ? S.accent : S.ink, font: S.fontDisplay }]]));
1451
- sh.push(txSp(x5 - 0.1, T.descY, stepW - 0.25, T.descH, [[{ t: p[2] || '', sz: 12, col: S.body, font: S.font }]], { lineSpacing: 18 }));
1452
- });
1453
- } else if (type === 'bar') {
1454
- var ch = sec.chart || {};
1455
- var dcols2 = (ch.colors || dataColors(model.style, model.theme));
1456
- var botY = chartBottom(!!sec.soWhat, !!sec.footnote) - (sec.note ? 0.42 : 0.05);
1457
- var isHBar = ch.type === 'hbar';
1458
- var bRR = regOf('bar', isHBar ? 'hbar' : 'primary', { bottom: botY }) ||
1459
- { x: isHBar ? MX : MX + PT.bar.chartX, y: isHBar ? PT.bar.hbarY0 : PT.bar.chartY,
1460
- w: isHBar ? CW : CW - PT.bar.chartW, h: 3 };
1461
- if (isHBar) {
1462
- chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, bRR.h, dcols2);
1463
- } else {
1464
- chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, Math.max(1.5, bRR.h), dcols2);
1465
- }
1466
- if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
1467
- } else if (type === 'twocol' || type === 'threecol') {
1468
- var ps2 = sec.paragraphs || [];
1469
- var nCol = (type === 'threecol') ? 3 : 2;
1470
- var gapC = (type === 'threecol') ? PT.research.col3Gap : PT.twocol.colGap;
1471
- var tcR = regOf('twocol', 'primary', { top: bodyY, bottom: bodyBottom }) ||
1472
- { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1473
- var gC = cols(tcR.w, nCol, gapC);
1474
- var perC = Math.ceil(ps2.length / nCol);
1475
- var availC = tcR.h;
1476
- /* 探针与 build_pptx.js 同算法:按「各栏字数之和」的最大值估算(保证两通道字号一致) */
1477
- var longest = 0, gi, pi2;
1478
- for (gi = 0; gi < nCol; gi++) {
1479
- var colG = ps2.slice(gi * perC, (gi + 1) * perC), ssum = 0;
1480
- for (pi2 = 0; pi2 < colG.length; pi2++) ssum += String(colG[pi2][1] || '').length;
1481
- if (ssum > longest) longest = ssum;
1482
- }
1483
- var probe = [], w4;
1484
- for (w4 = 0; w4 < longest / 120; w4++) probe.push(new Array(121).join('x'));
1485
- if (!probe.length) probe.push(new Array(1 + longest).join('x'));
1486
- var fzC = fitFont(probe, gC.w, availC, { max: 13.5, gapFactor: 1.2 });
1487
- for (var ci3 = 0; ci3 < nCol; ci3++) {
1488
- var colC = ps2.slice(ci3 * perC, (ci3 + 1) * perC);
1489
- if (!colC.length) continue;
1490
- var runsC = [];
1491
- colC.forEach(function (p) {
1492
- runsC.push([{ t: p[0] + ' ', sz: fzC, b: 1, col: S.ink, font: S.font },
1493
- { t: p[1], sz: fzC - 1, col: S.body, font: S.font }]);
1494
- });
1495
- sh.push(txSp(tcR.x + ci3 * gC.step, tcR.y, gC.w, availC, runsC,
1496
- { lineSpacing: fzC * 1.5, spaceBefore: 7 }));
1497
- }
1498
- } else if (type === 'halftable') {
1499
- var yH = sec.lead ? PT.common.bodyYWithLead : PT.research.denseTableY;
1500
- var hLR = regOf('halftable', 'left', { top: yH, bottom: bodyBottom }) ||
1501
- { x: MX, y: yH, w: CW * PT.research.halfTableW, h: bodyBottom - yH };
1502
- var hRR = regOf('halftable', 'right', { top: yH, bottom: bodyBottom }) ||
1503
- { x: MX + CW * PT.research.halfChartX, y: yH, w: CW * PT.research.halfChartW, h: bodyBottom - yH };
1504
- var lwH = hLR.w;
1505
- if (sec.table && sec.table.head) {
1506
- var colWH = sec.table.colW || sec.table.head.map(function () { return lwH / sec.table.head.length; });
1507
- sh.push(table(hLR.x, hLR.y, lwH, colWH, [sec.table.head].concat(sec.table.rows || []), S,
1508
- { availH: hLR.h, maxRowH: PT.research.denseRowH + 0.1, minRowH: 0.28 }));
1509
- }
1510
- var ecH = sec.chart || {};
1511
- if (ecH.labels && ecH.values) {
1512
- chartBlockShapes(sh, ecH, S, hRR.x, hRR.y, hRR.w, Math.max(1.2, hRR.h),
1513
- (ecH.colors || dataColors(model.style, model.theme)));
1514
- }
1515
- } else if (type === 'matrix') {
1516
- var rhH = sec.rowHeads || [], chH = sec.colHeads || [];
1517
- var mxR = regOf('matrix', 'primary', { bottom: bodyBottom }) ||
1518
- { x: MX, y: PT.research.matrixY, w: CW, h: bodyBottom - PT.research.matrixY };
1519
- var mY = mxR.y, mLW = PT.research.matrixLabelW;
1520
- var rowsC = sec.cells || [], nR2 = Math.max(1, rowsC.length);
1521
- var availM = mxR.h - 0.42;
1522
- var mCH = fitRowH(availM, nR2, PT.research.matrixCellH, 0.3);
1523
- var mW = (mxR.w - mLW) / Math.max(1, chH.length);
1524
- chH.forEach(function (ch3, ci) {
1525
- sh.push(txSp(mxR.x + mLW + ci * mW, mY, mW, 0.34, [[{ t: ch3, sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1526
- });
1527
- rowsC.forEach(function (rowC, ri) {
1528
- var y6 = mY + 0.42 + ri * mCH;
1529
- sh.push(txSp(mxR.x, y6 + (mCH - 0.5) / 2, mLW - 0.15, 0.5, [[{ t: rhH[ri] || '', sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
1530
- rowC.forEach(function (cell, ci) {
1531
- var acc = isRec(cell) && cell.accent;
1532
- var txt = isRec(cell) ? (cell.t || '') : String(cell);
1533
- var cx4 = mxR.x + mLW + ci * mW;
1534
- sh.push(shape('roundRect', cx4 + 0.04, y6, mW - 0.08, mCH - 0.1, acc ? S.soft : S.surface,
1535
- { adj: 3000, line: acc ? undefined : S.line }));
1536
- sh.push(txSp(cx4 + 0.14, y6 + 0.08, mW - 0.28, mCH - 0.26, [[{ t: txt, sz: 11, col: acc ? S.accent : S.body, font: S.font }]], { anchor: 'ctr' }));
1537
- });
1538
- });
1539
- } else if (type === 'lane') {
1540
- var lnS = sec.lanes || [], nL = Math.max(1, lnS.length);
1541
- var startL = sec.lead ? PT.common.bodyYWithLead : PT.arch.fullStartY;
1542
- var lnR = regOf('lane', 'primary', { mode: model.mode, bottom: bodyBottom }) ||
1543
- { x: MX, y: startL, w: CW, h: bodyBottom - startL };
1544
- var availL = lnR.h;
1545
- var lnH = fitRowH(availL - (nL - 1) * PT.arch.laneGap, nL, PT.arch.laneH, 0.3);
1546
- var lnGap = PT.arch.laneGap, lnHW = PT.arch.laneHeadW;
1547
- lnS.forEach(function (ln, li) {
1548
- var y7 = lnR.y + li * (lnH + lnGap);
1549
- sh.push(shape('roundRect', lnR.x, y7, lnR.w, lnH, S.surface, { adj: 2500, line: S.line }));
1550
- sh.push(shape('rect', lnR.x, y7, lnHW, lnH, S.surface, { line: S.line }));
1551
- sh.push(txSp(lnR.x + 0.08, y7, lnHW - 0.16, lnH, [[{ t: ln[0], sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1552
- var steps = ln[1] || [];
1553
- var stepW2 = Math.min(PT.arch.stepMaxW, (lnR.w - lnHW - 0.4 - (steps.length - 1) * PT.arch.stepGap) / Math.max(1, steps.length));
1554
- steps.forEach(function (st, si) {
1555
- var stAcc = isRec(st) && st.accent;
1556
- var stT = isRec(st) ? st.t : st;
1557
- var sx2 = lnR.x + lnHW + 0.2 + si * (stepW2 + PT.arch.stepGap);
1558
- sh.push(shape('roundRect', sx2, y7 + 0.13, stepW2, lnH - 0.26, stAcc ? S.soft : S.bg,
1559
- { adj: 4000, line: stAcc ? undefined : S.line }));
1560
- sh.push(txSp(sx2 + 0.06, y7 + 0.13, stepW2 - 0.12, lnH - 0.26,
1561
- [[{ t: stT, sz: 11, b: stAcc ? 1 : 0, col: stAcc ? S.accent : S.ink, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1562
- if (si < steps.length - 1) {
1563
- sh.push(txSp(sx2 + stepW2, y7 + 0.13, PT.arch.stepGap, lnH - 0.26, [[{ t: '→', sz: 11, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1564
- }
1565
- });
1566
- });
1567
- } else if (type === 'cards') {
1568
- var cdC = PT.cards, cds = sec.cards || [];
1569
- var cR = regOf('cards', 'primary', { bottom: bodyBottom }) ||
1570
- { x: MX, y: cdC.startY, w: CW, h: bodyBottom - cdC.startY };
1571
- var cN = Math.min(sec.columns || 3, cds.length || 1);
1572
- var rN = Math.ceil((cds.length || 1) / cN);
1573
- var gw2 = (cR.w - (cN - 1) * cdC.gap) / cN;
1574
- var gh2 = fitRowH(cR.h - (rN - 1) * cdC.gap, rN, cdC.maxH, 0.6);
1575
- cds.forEach(function (cd, i) {
1576
- var col2 = i % cN, row2 = Math.floor(i / cN);
1577
- var x8 = cR.x + col2 * (gw2 + cdC.gap), y8 = cR.y + row2 * (gh2 + cdC.gap);
1578
- sh.push(shape('roundRect', x8, y8, gw2, gh2, S.surface, { line: S.line, adj: 5000 }));
1579
- /* 卡片头路标(与 B 通道同源:accent 小方块 = PPTX 侧图标等价物) */
1580
- var ico2 = 0.18;
1581
- sh.push(shape('roundRect', x8 + 0.18, y8 + 0.2, ico2, ico2, S.accent, { adj: 2500 }));
1582
- sh.push(txSp(x8 + 0.18 + ico2 + 0.1, y8 + 0.14, gw2 - 0.36 - ico2 - 0.1, cdC.titleH,
1583
- [[{ t: cd.title, sz: 15, b: 1, col: S.ink, font: S.font }]]));
1584
- var paras2 = (cd.points || []).map(function (pt) {
1585
- var line;
1586
- if (pt && pt.length != null && typeof pt !== 'string') {
1587
- var k = pt[0] == null ? '' : String(pt[0]);
1588
- var v = pt[1] == null ? '' : String(pt[1]);
1589
- line = (k && v) ? ('· ' + k + ' ' + v) : ('· ' + (k || v));
1590
- } else if (pt && typeof pt === 'object') {
1591
- line = '· ' + String(pt.t || '') + (pt.d ? ' ' + String(pt.d) : '');
1592
- } else {
1593
- line = '· ' + String(pt == null ? '' : pt);
1594
- }
1595
- return [{ t: line, sz: 12, col: S.body, font: S.font }];
1596
- });
1597
- sh.push(txSp(x8 + 0.18, y8 + 0.14 + cdC.titleH + 0.06, gw2 - 0.36, Math.max(0.4, gh2 - cdC.titleH - 0.34), paras2,
1598
- { lineSpacing: 18 }));
1599
- });
1600
- } else if (type === 'split') {
1601
- /* 双区自由组合页:左区与右区各可为 要点 / 图表 / 表格 / 图片(与 B 通道同语义)。 */
1602
- var SP = PT.split;
1603
- var lRs = regOf('split', 'left', { bottom: bodyBottom }) ||
1604
- { x: MX, y: SP.chartY, w: CW * SP.leftW, h: bodyBottom - SP.chartY };
1605
- var rRs = regOf('split', 'right', { bottom: bodyBottom }) ||
1606
- { x: MX + CW * SP.rightX, y: SP.chartY, w: CW * SP.rightW, h: bodyBottom - SP.chartY };
1607
- var lw2 = lRs.w, rw2 = rRs.w, rxL = rRs.x;
1608
- var lt2 = (sec.left && sec.left.type) || 'points';
1609
- /* 要点区渲染(左右两区共用;与 B 通道同算法同文本) */
1610
- var drawPointsL = function (el, x, w) {
1611
- var ptsP = (el && el.points) || [];
1612
- var lyP = bodyY, availP = bodyBottom - lyP;
1613
- var rowHP = fitRowH(availP, ptsP.length, SP.rowH, 0.3);
1614
- var probeP = [], q5;
1615
- for (q5 = 0; q5 < ptsP.length; q5++) probeP.push(String(ptsP[q5][0] || '') + String(ptsP[q5][1] || ''));
1616
- var fzP = fitFont(probeP, w - 0.4, availP, { max: 14, gapFactor: 0.6 });
1617
- ptsP.forEach(function (pt, i) {
1618
- var ly2 = lyP + i * rowHP;
1619
- sh.push(shape('rect', x, ly2 + rowHP / 2 - 0.06, 0.12, 0.12, S.accent));
1620
- sh.push(txSp(x + 0.28, ly2, w - 0.4, rowHP - 0.04,
1621
- [[{ t: pt[0] + ' ', sz: fzP, b: 1, col: S.ink, font: S.font },
1622
- { t: pt[1], sz: fzP - 1, col: S.body, font: S.font }]]));
1623
- });
1624
- };
1625
- if (lt2 === 'table') {
1626
- var lhL = [sec.left.head].concat(sec.left.rows);
1627
- var lcwL = sec.left.colW || sec.left.head.map(function () { return lw2 / sec.left.head.length; });
1628
- sh.push(table(lRs.x, SP.tableY, lw2, lcwL, lhL, S, { availH: bodyBottom - SP.tableY, maxRowH: SP.tableRowH, minRowH: 0.3 }));
1629
- } else if (lt2 === 'image') {
1630
- var imL = (sec.left && sec.left.image) || {};
1631
- addImageEl(sh, imL.src, S, lRs.x, SP.chartY, lw2, Math.max(1.4, bodyBottom - SP.chartY),
1632
- { placeholder: !!imL.placeholder || !imL.src, layout: 'half', multi: false, fit: imL.fit });
1633
- if (imL.caption) sh.push(txSp(lRs.x, SP.capY, lw2, 0.35, [[{ t: imL.caption, sz: 11, col: S.faint, font: S.font }]], { align: 'ctr' }));
1634
- } else if (lt2 !== 'points') {
1635
- var lc2 = sec.left;
1636
- chartBlockShapes(sh, lc2, S, lRs.x, SP.chartY, lw2,
1637
- Math.max(1.4, bodyBottom - SP.chartY - (lc2.cap ? 0.1 : 0)),
1638
- lc2.colors || dataColors(model.style, model.theme));
1639
- if (lc2.cap) sh.push(txSp(lRs.x, SP.capY, lw2, 0.35, [[{ t: lc2.cap, sz: 12, col: S.body, font: S.font }]], { align: 'ctr' }));
1640
- } else {
1641
- drawPointsL(sec.left, lRs.x, lw2);
1642
- }
1643
- var rt = (sec.right && sec.right.type) || 'bar';
1644
- if (rt === 'table') {
1645
- var rrL = [sec.right.head].concat(sec.right.rows);
1646
- var rcwL = sec.right.colW || sec.right.head.map(function () { return rw2 / sec.right.head.length; });
1647
- sh.push(table(rxL, SP.tableY, rw2, rcwL, rrL, S, { availH: bodyBottom - SP.tableY, maxRowH: SP.tableRowH, minRowH: 0.3 }));
1648
- } else if (rt === 'image') {
1649
- var im2 = (sec.right && sec.right.image) || {};
1650
- addImageEl(sh, im2.src, S, rxL, SP.chartY, rw2, Math.max(1.4, bodyBottom - SP.chartY),
1651
- { placeholder: !!im2.placeholder || !im2.src, layout: 'half', multi: false });
1652
- if (im2.caption) sh.push(txSp(rxL, SP.capY, rw2, 0.35, [[{ t: im2.caption, sz: 11, col: S.faint, font: S.font }]], { align: 'ctr' }));
1653
- } else if (rt === 'points') {
1654
- drawPointsL(sec.right, rxL, rw2);
1655
- } else {
1656
- var bc2 = sec.right;
1657
- var dcols3 = bc2.colors || dataColors(model.style, model.theme);
1658
- chartBlockShapes(sh, bc2, S, rxL, SP.chartY, rw2, Math.max(1.4, bodyBottom - SP.chartY - (bc2.cap ? 0.1 : 0)), dcols3);
1659
- if (bc2.cap) sh.push(txSp(rxL, SP.capY, rw2, 0.35, [[{ t: bc2.cap, sz: 12, col: S.body, font: S.font }]], { align: 'ctr' }));
1660
- }
1661
- } else if (type === 'diagram') {
1662
- var D = PT.diagram, lys = sec.layers || [];
1663
- var dgR = regOf('diagram', 'primary', { mode: _mode, bottom: bodyBottom }) ||
1664
- { x: MX, y: D.bodyStartY, w: CW, h: bodyBottom - D.bodyStartY };
1665
- var dStart = dgR.y;
1666
- var dEnd = dgR.y + dgR.h;
1667
- var nLay = Math.max(1, lys.length);
1668
- var lh2 = Math.max(0.6, Math.min(D.maxLayerH, (dEnd - dStart) / nLay - D.layerGap));
1669
- lys.forEach(function (lay, li) {
1670
- var y9 = dStart + li * (lh2 + D.layerGap);
1671
- var focus = lay[2] === 'focus';
1672
- sh.push(shape('roundRect', dgR.x, y9, D.layerBarW, lh2, focus ? S.accent : S.surface,
1673
- { adj: 3000, line: focus ? undefined : S.line }));
1674
- sh.push(txSp(dgR.x + 0.1, y9 + 0.1, D.layerBarW - 0.2, lh2 - 0.2, [[{ t: lay[0], sz: 12, b: 1, col: focus ? S.onAccent : S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1675
- var nodes = lay[1] || [], nn2 = Math.max(1, nodes.length);
1676
- var availW = dgR.w - D.layerBarW - 0.2;
1677
- var nw2 = Math.min(D.nodeMaxW, (availW - (nn2 - 1) * D.nodeGap) / nn2);
1678
- nodes.forEach(function (nd, ni) {
1679
- var x10 = dgR.x + D.layerBarW + 0.2 + ni * (nw2 + D.nodeGap);
1680
- var ndacc = isRec(nd) ? nd.accent : false;
1681
- var ntt = isRec(nd) ? nd.t : nd;
1682
- var nds = isRec(nd) ? (nd.d || '') : '';
1683
- sh.push(shape('roundRect', x10, y9, nw2, lh2, ndacc ? S.soft : S.surface, { adj: 4000, line: S.line }));
1684
- sh.push(txSp(x10 + 0.12, y9 + (nds ? 0.12 : lh2 / 2 - 0.2), nw2 - 0.24, 0.38,
1685
- [[{ t: ntt, sz: 12.5, b: 1, col: ndacc ? S.accent : S.ink, font: S.font }]], { anchor: nds ? 't' : 'ctr' }));
1686
- if (nds) sh.push(txSp(x10 + 0.12, y9 + lh2 - 0.5, nw2 - 0.24, 0.4, [[{ t: nds, sz: 10, col: S.faint, font: S.font }]]));
1687
- });
1688
- if (li < lys.length - 1) {
1689
- var cy2 = y9 + lh2 + D.layerGap / 2;
1690
- var cxm2 = dgR.x + D.layerBarW / 2;
1691
- sh.push(shape('rect', cxm2 - 0.01, cy2 - 0.1, 0.02, 0.14, focus ? S.accent : S.line));
1692
- sh.push(shape('rect', cxm2 - 0.06, cy2 + 0.04, 0.12, 0.02, focus ? S.accent : S.line));
1693
- }
1694
- });
1695
- if (sec.legend) {
1696
- var lx2 = MX;
1697
- sec.legend.forEach(function (lg2) {
1698
- sh.push(txSp(lx2, PH - 0.62, 3.2, 0.3, [[{ t: '● ' + lg2, sz: 10, col: S.faint, font: S.font }]]));
1699
- lx2 += 2.0;
1700
- });
1701
- }
1702
- } else if (type === 'exhibit') {
1703
- var eReg = regOf('exhibit', 'primary')
1704
- || { x: MX, y: PT.exhibit.chartY, w: CW, badgeY: PT.exhibit.badgeY };
1705
- if (sec.exhibitNo) {
1706
- sh.push(txSp(MX, eReg.badgeY != null ? eReg.badgeY : PT.exhibit.badgeY, CW, 0.35, [[{ t: String(sec.exhibitNo).toUpperCase(), sz: 11.5, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1707
- }
1708
- var ec2 = sec.chart || {};
1709
- if (ec2.labels && ec2.values) {
1710
- var edcols2 = (ec2.colors || dataColors(model.style, model.theme));
1711
- var hh2 = Math.max(1.2, chartBottom(!!sec.soWhat, !!sec.footnote) - eReg.y - 0.05);
1712
- if (ec2.type === 'hbar') {
1713
- chartBlockShapes(sh, ec2, S, eReg.x, eReg.y, eReg.w, hh2, edcols2);
1714
- } else {
1715
- chartBlockShapes(sh, ec2, S, eReg.x + 0.4, eReg.y - 0.1, eReg.w - 0.8, hh2 + 0.15, edcols2);
1716
- }
1717
- }
1718
- } else if (type === 'sankey' || type === 'treemap' || type === 'boxplot'
1719
- || type === 'network' || type === 'marimekko' || type === 'streamgraph') {
1720
- var infoR = regOf(type, 'primary', { bottom: bodyBottom }) ||
1721
- { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1722
- infoApprox(sh, sec, S, infoR.y, infoR.y + infoR.h);
1723
- } else {
1724
- /* points(默认):要点列表 + 可选右侧指标列 */
1725
- var P2 = PT.points, pts3 = sec.points || [];
1726
- var hasM = !!(sec.metrics && sec.metrics.length);
1727
- var ptsR = regOf('points', 'primary', { top: bodyY, bottom: bodyBottom }) ||
1728
- { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1729
- /* 指标列最多 3 个 + 文本区严格分区(与 build_pptx.js 同算法) */
1730
- var nm2 = hasM ? Math.min(sec.metrics.length, 3) : 0;
1731
- var mw2 = hasM ? Math.min(P2.metricW, Math.max(1.6, (CW - 4.8) / nm2)) : 0;
1732
- var textW = hasM ? Math.max(4.2, (PW - MX - nm2 * mw2) - MX - 0.35) : CW - 0.4;
1733
- var availPt = ptsR.h;
1734
- var rowHPt = fitRowH(availPt, pts3.length, P2.rowH, 0.3);
1735
- var probeP = [], q5;
1736
- for (q5 = 0; q5 < pts3.length; q5++) probeP.push(String(pts3[q5][0] || '') + String(pts3[q5][1] || ''));
1737
- var fzP = fitFont(probeP, textW - 0.32, availPt, { max: 15, gapFactor: 0.55 });
1738
- pts3.forEach(function (p, i) {
1739
- var y11 = ptsR.y + i * rowHPt;
1740
- sh.push(shape('rect', ptsR.x, y11 + rowHPt / 2 - P2.markSize / 2, P2.markSize, P2.markSize, S.accent));
1741
- sh.push(txSp(ptsR.x + 0.32, y11, textW, rowHPt - 0.04,
1742
- [[{ t: p[0] + ' ', sz: fzP, b: 1, col: S.ink, font: S.font },
1743
- { t: p[1], sz: fzP - 1, col: S.body, font: S.font }]]));
1744
- });
1745
- if (hasM) {
1746
- var gx2 = PW - MX - nm2 * mw2;
1747
- var vSz = nm2 >= 3 ? 28 : 34;
1748
- sec.metrics.slice(0, nm2).forEach(function (m, i) {
1749
- var x12 = gx2 + i * mw2;
1750
- sh.push(txSp(x12, P2.metricY, mw2 - 0.2, P2.metricValH, [[{ t: m[0], sz: vSz, b: 1, col: S.accent, font: S.fontDisplay }]]));
1751
- sh.push(txSp(x12, P2.metricCapY, mw2 - 0.2, 0.9, [[{ t: m[1], sz: 11, col: S.faint, font: S.font }]]));
1752
- });
1753
- }
1754
- }
1755
- /* research 通用可选件:so-what 结论条 + 页脚来源行 + 待核实条(共用同一槽位) */
1756
- if (sec.soWhat) soWhatBar(sh, sec.soWhat, S);
1757
- if (sec.footnote) footnoteLine(sh, sec.footnote, S);
1758
- if (flagH) flagBar(sh, flagItems, S, flagY);
1759
- slides.push(sh);
1760
- });
1761
-
1762
- /* 收尾(主题一致强调带——accent-soft 底 + 常规文字,浅色模式不再出深色页) */
1763
- var CL = PT.closing;
1764
- var cl = [shape('rect', 0, 0, PW, PH, S.soft)];
1765
- cl.push(shape('rect', MX, CL.eyebrowY, 0.9, 0.045, S.accent));
1766
- cl.push(txSp(MX, CL.eyebrowY + 0.16, CW, 0.4, [[{ t: '下一步', sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1767
- cl.push(txSp(MX, CL.titleY, CW - 1, CL.titleH, [[{ t: model.closing.title, sz: 30, b: 1, col: S.ink, font: S.fontDisplay }]]));
1768
- var clPts = model.closing.points || [], clN = Math.max(1, clPts.length);
1769
- var clW = (CW - (clN - 1) * 0.3) / clN;
1770
- clPts.forEach(function (p, i) {
1771
- var x13 = MX + i * (clW + 0.3);
1772
- cl.push(shape('roundRect', x13, CL.pointsY - 0.2, clW, CL.pointTitleH + CL.pointBodyH + 0.32, S.surface, { line: S.line, adj: 4000 }));
1773
- cl.push(txSp(x13 + 0.18, CL.pointsY, clW - 0.36, CL.pointTitleH, [[{ t: p[0], sz: 17, b: 1, col: S.accent, font: S.font }]]));
1774
- cl.push(txSp(x13 + 0.18, CL.pointsY + CL.pointTitleH, clW - 0.36, CL.pointBodyH, [[{ t: p[1], sz: 12.5, col: S.body, font: S.font }]]));
1775
- });
1776
- slides.push(cl);
1777
-
1778
- /* 页码 */
1779
- var total = slides.length;
1780
- slides.forEach(function (sh, i) {
1781
- sh.push(txSp(PW - 1.4, (LAY.pageNumY != null ? LAY.pageNumY : PH - 0.5), 0.9, 0.3,
1782
- [[{ t: (i + 1) + ' / ' + total, sz: 10, col: S.faint, font: S.font }]], { align: 'r' }));
1783
- });
1784
- return slides;
1785
- }
1786
-
1787
- /* ── 打包骨架 ── */
1788
- var NS = {
1789
- a: 'http://schemas.openxmlformats.org/drawingml/2006/main',
1790
- p: 'http://schemas.openxmlformats.org/presentationml/2006/main',
1791
- r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
1792
- };
1793
- function slideXml(shapes) {
1794
- return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
1795
- '<p:sld xmlns:a="' + NS.a + '" xmlns:p="' + NS.p + '" xmlns:r="' + NS.r + '">' +
1796
- '<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
1797
- '<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
1798
- shapes.join('') + '</p:spTree></p:cSld><p:clrMapOvr><a:overrideClrMapping bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:clrMapOvr></p:sld>';
1799
- }
1800
- function relsXml(pairs) {
1801
- var rs = pairs.map(function (p) {
1802
- return '<Relationship Id="' + p[0] + '" Type="' + p[1] + '" Target="' + p[2] + '"/>';
1803
- }).join('');
1804
- return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
1805
- '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rs + '</Relationships>';
1806
- }
1807
- var RT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
1808
-
1809
- /* 每页 slide XML(预览模态用:与导出同一引擎,所见即所得)
1810
- theme="dark" 时用 PRESETS_DARK(与页面深色主题同源),默认 light */
1811
- function themePresets(theme) {
1812
- return (theme === 'dark' && typeof PRESETS_DARK !== 'undefined' &&
1813
- PRESETS_DARK && Object.keys(PRESETS_DARK).length) ? PRESETS_DARK : PRESETS;
1814
- }
1815
- function slidesXml(model) {
1816
- var S = themePresets(model && model.theme)[model.style] || PRESETS['business-blue'];
1817
- return slidesOf(model, S).map(slideXml);
1818
- }
1819
-
1820
- /* ── 模型自检(schema 单源驱动:scripts/model-schema.json 经 sync_runtime 注入的 MODEL_SCHEMA;
1821
- 与 scripts/extract_model.py 本地校验同一份定义,双端不漂移) ── */
1822
- function schemaGet(obj, path) {
1823
- var cur = obj, ks = path.split('.');
1824
- for (var i = 0; i < ks.length; i++) {
1825
- if (cur == null || typeof cur !== 'object') return undefined;
1826
- cur = cur[ks[i]];
1827
- }
1828
- return cur;
1829
- }
1830
- function schemaFieldOk(sec, spec) {
1831
- /* 'anyof:a|b:c'(任一满足,用于 image 页型 src/items/placeholder 三选一);
1832
- 'a.b:array' / 'a.b:str' / 'a.b'(真值检查)——与 scripts/extract_model.py 同语义。 */
1833
- if (spec.indexOf('anyof:') === 0) {
1834
- return spec.slice(6).split('|').some(function (alt) { return alt && schemaFieldOk(sec, alt); });
1835
- }
1836
- var m = spec.match(/^(.*?)(?::(array|str))?$/), path = m[1], kind = m[2];
1837
- var v = schemaGet(sec, path);
1838
- if (kind === 'array') return Array.isArray(v) && v.length > 0;
1839
- if (kind === 'str') return typeof v === 'string' && v.trim().length > 0;
1840
- return !!v;
1841
- }
1842
- /* 图片模型软校验:版式 / 裁切策略 / 多图数量 / 外链 src——与 validate_pptx.py 硬门禁同口径,
1843
- 浏览器预览与 extract_model.py 都能提前看到问题。 */
1844
- function imageModelWarnings(sec, where, warnings) {
1845
- var IS = (typeof IMG !== 'undefined' && IMG) || {};
1846
- var holders = [{ img: sec.image, at: where + '.image' }];
1847
- if (sec.right && sec.right.image) holders.push({ img: sec.right.image, at: where + '.right.image' });
1848
- holders.forEach(function (h) {
1849
- var img = h.img;
1850
- if (!img || typeof img !== 'object') return;
1851
- var layouts = IS.layouts || ['full', 'half', 'bleed', 'grid', 'compare', 'wall'];
1852
- var fitEnum = IS.fitEnum || ['cover', 'contain'];
1853
- var items = Array.isArray(img.items) ? img.items.filter(Boolean) : [];
1854
- var layout = String(img.layout || (items.length > 1 ? 'grid' : 'full')).toLowerCase();
1855
- if (layouts.indexOf(layout) < 0) {
1856
- warnings.push(h.at + '.layout="' + layout + '" 未知(应为 ' + layouts.join('/') + ')');
1857
- }
1858
- if (img.fit && fitEnum.indexOf(String(img.fit).toLowerCase()) < 0) {
1859
- warnings.push(h.at + '.fit="' + img.fit + '" 未知(应为 ' + fitEnum.join('/') + ')');
1860
- }
1861
- if (img.placeholder && (img.src || items.length)) {
1862
- warnings.push(h.at + ' 同时声明 placeholder 与图片源(placeholder 优先,图片将被忽略)');
1863
- }
1864
- if (items.length > (IS.maxPerPage || 6)) {
1865
- warnings.push(h.at + '.items 共 ' + items.length + ' 张,超出单页上限 ' + (IS.maxPerPage || 6));
1866
- }
1867
- if (layout === 'compare' && items.length && items.length !== 2) {
1868
- warnings.push(h.at + '.items 为 compare 版式时应为 2 张(当前 ' + items.length + ')');
1869
- }
1870
- [img].concat(items).forEach(function (it) {
1871
- if (it && typeof it === 'object' && typeof it.src === 'string' &&
1872
- /^\s*(?:https?:)?\/\//.test(it.src)) {
1873
- warnings.push(h.at + ' 含外链 src(只允许 data: 内联或相对路径)');
1874
- }
1875
- });
1876
- });
1877
- }
1878
-
1879
- function validateModel(model) {
1880
- var missing = [], warnings = [];
1881
- var M = (typeof MODEL_SCHEMA !== 'undefined' && MODEL_SCHEMA) || {};
1882
- var pageTypes = M.pageTypes || {};
1883
- if (!model || typeof model !== 'object') {
1884
- return { ok: false, missing: ['window.REPORT_MODEL(内容模型缺失)'], warnings: warnings, pages: 0 };
1885
- }
1886
- (M.model && M.model.required || ['title', 'sections:array', 'closing.title', 'closing.points:array'])
1887
- .forEach(function (spec) {
1888
- if (!schemaFieldOk(model, spec)) missing.push(spec.split(':')[0] + '(顶层必填)');
1889
- });
1890
- var mode = model.mode || 'presentation';
1891
- var hasAgenda = Array.isArray(model.agenda) && model.agenda.length > 0;
1892
- var agMin = (M.model && M.model.agendaMin && M.model.agendaMin[mode]) || 0;
1893
- if (agMin > 0 && (!hasAgenda || model.agenda.length < agMin)) {
1894
- missing.push('agenda(大纲 ≥' + agMin + ' 条)');
1895
- }
1896
- var secs = model.sections || [];
1897
- if (!Array.isArray(secs) || secs.length < ((M.model && M.model.sectionsMin) || 1)) {
1898
- missing.push('sections(章节页 ≥1)');
1899
- }
1900
- if (!model.closing || !model.closing.title) missing.push('closing(收尾页)');
1901
- secs.forEach(function (sec, i) {
1902
- var t = sec.type || 'points';
1903
- var def = pageTypes[t];
1904
- var where = 'sections[' + i + ']';
1905
- if (!def) { missing.push(where + '.type="' + t + '"(未知页型)'); return; }
1906
- if (!sec.title) missing.push(where + '.title(第 ' + (i + 1) + ' 章标题)');
1907
- (def.required || []).forEach(function (spec) {
1908
- if (!schemaFieldOk(sec, spec)) missing.push(where + '.' + spec + '(' + (def.label || t) + '必填)');
1909
- });
1910
- if (def.modes && def.modes.indexOf(mode) < 0) {
1911
- warnings.push('sections[' + i + '] 页型 "' + t + '" 文档口径适用于 ' + def.modes.join('/') + ',当前 mode="' + mode + '"');
1912
- }
1913
- imageModelWarnings(sec, where, warnings);
1914
- });
1915
- if (M.modes && model.mode && M.modes.indexOf(model.mode) < 0) {
1916
- warnings.push('mode="' + model.mode + '" 未知,按 presentation 处理');
1917
- }
1918
- if (model.style && !PRESETS[model.style]) warnings.push('style="' + model.style + '" 未知,回落商务蓝');
1919
- if (model.theme && model.theme !== 'light' && model.theme !== 'dark') {
1920
- warnings.push('theme="' + model.theme + '" 未知(应为 light/dark),按浅色导出');
1921
- }
1922
- if (secs.length < ((M.model && M.model.sectionsRecommended) || 3)) {
1923
- warnings.push('章节页仅 ' + secs.length + ' 页,正式报告建议 ≥3');
1924
- }
1925
- var agMax = (M.model && M.model.agendaComfortMax) || 16;
1926
- if ((model.agenda || []).length > agMax) {
1927
- warnings.push('agenda ' + model.agenda.length + ' 条超出单页舒适上限 ' + agMax + ',建议拆分');
1928
- }
1929
- var pages = 1 + (hasAgenda ? 1 : 0) + secs.length + 1; /* 封面+大纲(可选)+章节+收尾 */
1930
- return { ok: missing.length === 0, missing: missing, warnings: warnings, pages: pages };
1931
- }
1932
-
1933
- var api = {
1934
- slidesXml: slidesXml,
1935
- validateModel: validateModel,
1936
- /* 以下供 scripts/gen_channel_a.js 在 Node 侧组装完整 PPTX 包(回归双裁判内部质量路径) */
1937
- slidesOf: slidesOf, slideXml: slideXml, relsXml: relsXml, NS: NS, RT: RT, PRESETS: PRESETS,
1938
- PRESETS_DARK: (typeof PRESETS_DARK !== 'undefined' ? PRESETS_DARK : null),
1939
- dataColors: dataColors,
1940
- esc: esc, solid: solid, E: E
1941
- };
1942
- if (typeof module !== 'undefined' && module.exports) module.exports = api; /* Node:gen_channel_a.js 用 */
1943
- g.TopPptHtml = api;
1944
- })(typeof window !== 'undefined' ? window : globalThis);
1
+ /* ═══════════════════════════════════════════════════════════════════════════
2
+ * TopPPT HTML· 浏览器端 PPTX 预览运行时
3
+ * 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
4
+ *
5
+ * 定位:HTML 是阅读与演示界面,PPTX 是协作与修改格式——两者来自同一内容模型
6
+ * (window.REPORT_MODEL)。页面只提供「预览 PPTX」(WYSIWYG 模态,与智能体精导通道
7
+ * 同一序列化引擎)与可复制 AI 提示词;正式 PPTX 一律由本地智能体走精导通道
8
+ * (build_pptx.js)生成并过 strict 硬门禁,浏览器端导出达不到该质量标准。
9
+ *
10
+ * API:
11
+ * TopPptHtml.slidesXml(model) → string[](每页 slide XML,预览模态渲染用)
12
+ * TopPptHtml.validateModel(model) → {ok, missing[], warnings[], pages}(schema 单源驱动)
13
+ * TopPptHtml.slidesOf / slideXml / relsXml / NS / PRESETS
14
+ * → 供 scripts/gen_channel_a.js 在 Node 侧组装完整
15
+ * PPTX 包(回归双裁判内部质量路径,页面不使用)
16
+ *
17
+ * model = window.REPORT_MODEL(JSON,与页面正文同源):
18
+ * { mode, style, theme(light|dark:PPTX 按此选亮/暗 token,默认 light),
19
+ * title, subtitle, meta, agenda:[[num,标题,说明]…](architecture 可省),
20
+ * sections:[{type: points|metrics|kpi|table|timeline|steps|bar|donut|heatmap|bullet|pyramid|
21
+ * image|cards|split|comparison|quote|diagram|exhibit|twocol|threecol|halftable|
22
+ * matrix|lane, soWhat?, footnote?, flags?, image?, …}],
23
+ * closing:{title,points} }
24
+ * 通用可选字段:soWhat(结论条)/ footnote(来源行)/ flags(待核实清单 → accent 强调色条)
25
+ * / image(素材图片或配图占位,{src|items|placeholder, layout, fit, caption, alt, hint},
26
+ * src 仅 data: 或相对路径;layout ∈ full/half/bleed/grid/compare/wall,见 layout-constants.json imageSpec)。
27
+ * 页型字段定义(29 种)与 chart.type 白名单(30 类)的唯一事实源:scripts/model-schema.json。
28
+ *
29
+ * ★ 单源机制(scripts/sync_runtime.py 注入,标记块内禁止手改):
30
+ * - 版式常量:scripts/layout-constants.json → __TOPPPT_CONSTANTS__ 块;
31
+ * 通道 B(build_pptx.js)require 同一份 JSON,两通道产出一致。
32
+ * - DSL schema:scripts/model-schema.json → __TOPPPT_SCHEMA__ 块;
33
+ * scripts/extract_model.py 读同一份文件,双端校验同源。
34
+ * ★ 三模式独立排版比例尺(MTS)——research 咨询密排 / architecture 全幅图页型;
35
+ * modeSize() 把调用点的 presentation 基准 pt 映射到当前模式比例尺。
36
+ * ★ 本运行时只做「预览与回归裁判」:图表为形状近似渲染(交付通道 B 用原生数据图表,
37
+ * 可在 PowerPoint 中"编辑数据");文本与版式几何两通道同源,cross_verify 逐页比对。
38
+ * ═══════════════════════════════════════════════════════════════════════════ */
39
+ (function (g) {
40
+ 'use strict';
41
+ /* 双形态载荷判定:[[t,d]…] 数组 vs [{t,d,accent}…] 记录。typeof [] === 'object'。 */
42
+ function isRec(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
43
+
44
+ /* __TOPPPT_CONSTANTS_START__ */
45
+ /* ── 由 scripts/sync_runtime.py 从 scripts/layout-constants.json 注入 · 禁止手改 ── */
46
+ /* MTS = 三模式独立排版比例尺(按模式取 pt;基准 TS_BASE = presentation 档) */
47
+ /* PRESETS_DARK = 风格 dark token(REPORT_MODEL.theme="dark" 时导出深色版)*/
48
+ /* GRID = 12 列网格;TYPO = 语义字阶 C0–T14;CONT = 容器内边距;REG = 图表登记四元组;DEEP = 深度模式配置 */
49
+ /* IMG = 素材图片规格与配图占位约定(版式/裁切/建议尺寸/占位标签,双引擎同源) */
50
+ /* STYLE_DATA_COLORS(_DARK) = 9 套风格各自的编码色板 c1–c5(数据系列;结构色纪律不变) */
51
+ /* LAYOUT_SLOTS + regionOf = 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */
52
+ var PW = 13.333, PH = 7.5, MX = 0.6, CW = PW - 2 * MX, EMU = 914400;
53
+ var TS_BASE = {"coverTitle":44,"coverSub":18,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10};
54
+ var MTS = {"presentation":{"coverTitle":44,"coverSub":18,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10},"research":{"coverTitle":36,"coverSub":14,"h1":22,"h2":13.5,"lead":11.5,"body":10.5,"caption":9,"micro":8.5},"architecture":{"coverTitle":44,"coverSub":19,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10}};
55
+ var PT = {"layout":{"contentTop":2.35,"contentBottom":6.9,"contentBottomWithNote":6.4,"pageNumY":7.0},"common":{"headEyebrowY":0.48,"headTitleY":0.82,"headRuleY":1.72,"leadY":1.88,"bodyY":2.35,"bodyYWithLead":2.55,"pagerY":7.0},"cover":{"metaY":0.7,"titleY":2.4,"titleH":2.0,"subtitleY":4.5,"subtitleH":0.8},"closing":{"eyebrowY":0.7,"titleY":1.2,"titleH":1.2,"pointsY":3.0,"pointTitleH":0.6,"pointBodyH":1.2},"points":{"listY":2.5,"rowH":0.72,"rowHMin":0.5,"markSize":0.14,"metricY":4.7,"metricW":2.6,"metricValH":0.9,"metricCapY":5.6},"metrics":{"bandY":3.1,"cardH":2.3,"valY":3.45,"valH":1.1,"capY":4.55,"capH":0.7,"gap":0.3,"maxW":2.9},"table":{"y":2.5,"rowH":0.55,"rowHMin":0.32,"headRowH":0.5},"bar":{"hbarY0":2.6,"hbarY1":6.25,"chartX":0.6,"chartY":2.95,"chartW":1.2,"chartH":3.35,"noteY":6.55},"note":{"y":6.55,"h":0.4},"exhibit":{"badgeY":2.35,"chartY":2.75,"chartH":3.25,"soWhatY":6.05,"footnoteY":6.72},"twocol":{"colGap":0.5,"bodyH":3.6,"rowHMin":0.5},"hbar":{"labelW":1.9,"valW":1.0,"rowH":0.55,"barRatio":0.5},"diagram":{"layerBarW":1.35,"layerGap":0.15,"nodeGap":0.15,"bodyStartY":2.9,"maxLayerH":1.15,"nodeMaxW":2.2},"split":{"leftW":0.55,"rightW":0.415,"rightX":0.585,"pointsY":2.7,"rowH":0.78,"capY":2.55,"chartY":3.0,"chartH":3.7,"tableY":2.7,"tableRowH":0.5},"cards":{"startY":3.0,"gap":0.25,"maxH":2.0,"titleH":0.4},"timeline":{"labelY":2.95,"axisY":3.58,"dotY":3.51,"dotR":0.18,"nameY":3.9,"descY":4.45,"descH":1.2},"research":{"col3Gap":0.38,"matrixLabelW":1.5,"matrixCellH":0.85,"matrixY":2.2,"denseTableY":2.2,"denseRowH":0.42,"halfTableW":0.52,"halfChartX":0.57,"halfChartW":0.43},"arch":{"fullStartY":2.1,"fullEndY":6.9,"laneH":0.72,"laneHMin":0.5,"laneGap":0.14,"laneHeadW":1.3,"stepGap":0.12,"stepMaxW":1.9},"kpi":{"heroY":2.9,"heroW":0.52,"heroH":1.6,"dividerX":0.56,"metricY0":2.95,"metricRowH":0.85},"comparison":{"panelGap":0.4,"panelY":2.55,"panelH":3.4,"titleH":0.45,"rowH":0.62},"quote":{"markY":1.75,"textY":2.7,"textH":2.3,"authorY":5.25,"contextY":5.8},"donut":{"centerX":3.05,"centerY":4.6,"r":1.6,"holeR":0.95,"legendX":5.6,"legendRowH":0.55,"legendLabelW":3.4,"legendValW":1.3,"legendPctX":5.3,"legendPctW":1.3},"steps":{"y":3.05,"barH":1.35,"gap":0.12,"arrowW":0.3,"numH":0.3,"titleH":0.34,"pad":0.08,"maxPerRow":6},"heatmap":{"y":2.6,"labelW":1.7,"headH":0.42,"cellH":0.6,"cellGap":0.06,"legendW":1.6},"pyramid":{"y":2.35,"rowH":0.72,"gap":0.08,"minW":0.34,"labelW":3.2},"bullet":{"y":2.6,"rowH":0.8,"labelW":2.4,"valW":1.7,"barH":0.26},"image":{"y":2.3,"h":4.2,"halfW":0.56,"halfGap":0.06,"capY":6.6,"capH":0.3,"radius":0.09,"noteRowH":0.62,"gridGap":0.16,"gridCols":{"2":2,"3":3,"4":4,"6":3},"gridCapH":0.28,"compareGap":0.3,"compareTagH":0.26,"wallCellH":0.92,"wallGap":0.18,"wallMaxRows":2},"flagbar":{"hdH":0.26,"rowH":0.24,"maxRows":3,"gap":0.12},"sankey":{"y":2.45,"h":4.1,"nodeW":0.32,"nodeGap":0.14,"labelW":1.55,"minBandH":0.22,"maxNodes":12,"maxFlows":24},"treemap":{"y":2.45,"h":4.1,"gap":0.05,"labelMinH":0.34,"maxLeaves":16,"maxDepth":3},"boxplot":{"y":2.7,"h":3.7,"labelW":1.5,"groupGap":0.3,"boxMaxW":0.9,"whiskerCapW":0.34,"maxGroups":8},"network":{"y":2.4,"h":4.2,"nodeR":0.28,"nodeRMin":0.18,"labelMaxW":1.4,"maxNodes":18,"maxEdges":30},"marimekko":{"y":2.6,"h":3.75,"labelW":1.55,"colGap":0.06,"legendH":0.42,"maxCols":6,"maxSegs":4},"streamgraph":{"y":2.6,"h":3.75,"labelW":1.4,"bandMinH":0.12,"maxSeries":6}};
56
+ var PRESETS = {
57
+ 'business-blue': { accent:'1A73E8', ink:'202124', body:'5F6368', faint:'80868B', bg:'FFFFFF', surface:'F8F9FA', line:'DADCE0', soft:'E8F0FE', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
58
+ 'apple-mono': { accent:'1D1D1F', ink:'1D1D1F', body:'6E6E73', faint:'86868B', bg:'FFFFFF', surface:'F5F5F7', line:'D2D2D7', soft:'F5F5F7', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
59
+ 'mckinsey': { accent:'003A70', ink:'101828', body:'475467', faint:'667085', bg:'FFFFFF', surface:'F2F4F7', line:'D0D5DD', soft:'EEF4FB', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
60
+ 'brand-red': { accent:'D0021B', ink:'1F1A1B', body:'5D5456', faint:'8A8083', bg:'FFFFFF', surface:'FAF7F7', line:'E2D9DA', soft:'FBECEC', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
61
+ 'warm-sand': { accent:'96681F', ink:'2A2521', body:'6B5F52', faint:'93836F', bg:'FBF9F6', surface:'F6F2EC', line:'E0D7C9', soft:'F3ECDD', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
62
+ 'deep-teal': { accent:'0F6B5C', ink:'14201E', body:'4A5A57', faint:'73837F', bg:'FFFFFF', surface:'F2F6F5', line:'CFDEDC', soft:'E4F1EE', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
63
+ 'graphite-dark': { accent:'0369A1', ink:'1A1D21', body:'4B5158', faint:'767C84', bg:'F4F5F7', surface:'ECEEF1', line:'CED1D6', soft:'E2EFF7', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
64
+ 'indigo-violet': { accent:'5B5BD6', ink:'1B1A2E', body:'52506B', faint:'7A779A', bg:'FFFFFF', surface:'F4F4FB', line:'D7D7E8', soft:'ECECF9', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
65
+ 'spectrum': { accent:'4563EF', ink:'171A21', body:'4D5566', faint:'7B8494', bg:'FBFCFE', surface:'F4F6FA', line:'D6DCE6', soft:'ECEEFF', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' }
66
+ };
67
+ var PRESETS_DARK = {
68
+ 'business-blue': { accent:'8AB4F8', ink:'E8EAED', body:'ADB4BD', faint:'868D96', bg:'0D0F13', surface:'191D23', line:'2F353D', soft:'16243A', onAccent:'0D1B2E', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
69
+ 'apple-mono': { accent:'F5F5F7', ink:'F5F5F7', body:'A1A1A6', faint:'86868B', bg:'000000', surface:'161617', line:'38383D', soft:'1D1D1F', onAccent:'000000', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
70
+ 'mckinsey': { accent:'7FB2E5', ink:'E4E7EC', body:'AAB3C2', faint:'8A94A6', bg:'0C1116', surface:'12181F', line:'2A3342', soft:'12233A', onAccent:'0C1A2E', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
71
+ 'brand-red': { accent:'FF5A66', ink:'F3ECEC', body:'C4B6B7', faint:'9A8D8E', bg:'130D0E', surface:'1A1213', line:'37272A', soft:'331518', onAccent:'2A0D10', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
72
+ 'warm-sand': { accent:'D9B36A', ink:'ECE5DB', body:'C4B8A8', faint:'9B8D7A', bg:'15120E', surface:'1D1913', line:'3A3128', soft:'2E2417', onAccent:'2A1F10', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
73
+ 'deep-teal': { accent:'4FC3B0', ink:'E2ECEA', body:'B3C2BF', faint:'86948F', bg:'0D1312', surface:'131B1A', line:'293834', soft:'12312C', onAccent:'0C2420', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
74
+ 'graphite-dark': { accent:'4DA3FF', ink:'EEF2F6', body:'B9C2CD', faint:'85909D', bg:'0B0E12', surface:'171D24', line:'2B3540', soft:'132841', onAccent:'07182B', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
75
+ 'indigo-violet': { accent:'A5A4F0', ink:'E6E5F5', body:'B6B4D4', faint:'8B89B0', bg:'100F1C', surface:'171629', line:'322F52', soft:'232146', onAccent:'171631', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
76
+ 'spectrum': { accent:'8BA0F8', ink:'E8EBF2', body:'A7AFBF', faint:'7E8798', bg:'0E1116', surface:'171C26', line:'2B3342', soft:'1B2242', onAccent:'141A38', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' }
77
+ };
78
+ var STYLE_DATA_COLORS = {"business-blue":["1a73e8","3526bb","21a199","b77148","99983d"],"apple-mono":["1d1d1f","0071e3","0f7b6c","c2410c","7c3aed"],"mckinsey":["003a70","2826bb","21a18e","b76848","99903d"],"brand-red":["d0021b","bb6326","d634ae","40a182","438aa8"],"warm-sand":["96681f","839324","cf3b3e","4d88b2","4a47a4"],"deep-teal":["0f6b5c","2683bb","22aa3e","b74870","a85c43"],"graphite-dark":["0369a1","2637bb","1f9977","b75948","a18a40"],"indigo-violet":["5b5bd6","7432ae","4197c8","a08f4b","7ca04b"],"spectrum":["4563ef","5b26bb","269ebb","af8746","83993d"]};
79
+ var STYLE_DATA_COLORS_DARK = {"business-blue":["8ab4f8","7966e0","88e7e5","d4ad91","cbce83"],"apple-mono":["f5f5f7","2997ff","2dd4bf","ff9f0a","bf5af2"],"mckinsey":["7fb2e5","6f6bdb","8ce3d8","d1a694","cbc586"],"brand-red":["ff5a66","e09e66","e788cb","91d4c2","83b4ce"],"warm-sand":["d9b36a","c2d571","df9391","97b5ce","8e89c7"],"deep-teal":["4fc3b0","7aaecd","97d8a4","c99cac","c29c8f"],"graphite-dark":["4da3ff","6c66e0","88e7dc","d4a691","cec983"],"indigo-violet":["a5a4f0","a967e0","89c3e7","d3c792","aece83"],"spectrum":["8ba0f8","9066e0","88d6e7","d4ba91","bdce83"]};
80
+ var GRID = {"columns":12,"gutter":0.16,"colW":0.8644,"x":[0.6,1.6244,2.6488,3.6732,4.6976,5.722,6.7464,7.7708,8.7952,9.8196,10.844,11.8684,12.733],"y":[2.35,3.27,4.19,5.11,6.03,6.9],"safe":{"top":0.4,"bottom":0.4,"left":0.6,"right":0.6}};
81
+ var TYPO = {"levels":[{"id":"C0","name":"封面/章节幕标题","role":"coverTitle","usage":"封面主标题、章节幕标题"},{"id":"T1","name":"页码/章节徽章","role":"caption","usage":"页码徽章、章节编号"},{"id":"T2","name":"页面主标题/结论标题","role":"h1","usage":"每页顶部结论句(research 行动标题)"},{"id":"T3","name":"副标题/语境说明","role":"lead","usage":"主标题下方导语"},{"id":"T4","name":"模块标题/图表标题","role":"h2","usage":"卡片标题、图表标题、面板标题"},{"id":"T5","name":"证据编号/轻量标签","role":"micro","usage":"Exhibit 编号、状态标签"},{"id":"T6","name":"证据块标题/小节标题","role":"h2","usage":"分栏小标题、证据块标题"},{"id":"T7","name":"正文解释段落","role":"body","usage":"证据解释、管理解读正文、表格正文"},{"id":"T8","name":"结论条文字","role":"body","usage":"so-what 结论条、核心结论框"},{"id":"T9","name":"SO WHAT 标签","role":"caption","usage":"so-what 标签行"},{"id":"T10","name":"SO WHAT 正文/业务含义","role":"body","usage":"so-what 正文、行动含义、表格行动项"},{"id":"T11","name":"图表轴/图例/刻度/微标签","role":"micro","usage":"坐标轴、图例、刻度、单位(禁用于表格正文与完整短句)"},{"id":"T12","name":"图表数据标签","role":"caption","usage":"折线点值、柱形标签、百分比标注"},{"id":"T13","name":"关键 KPI 大数字","role":"h1","usage":"KPI 大数字、hero 数值"},{"id":"T14","name":"注释/口径/来源/页脚","role":"micro","usage":"footnote、来源行、页脚、小页码"}],"tableSemanticRule":{"body":["T7","T10"],"microAllowed":["T11"],"microForbiddenFor":["表格正文","行动项","风险项","解释句","建议句","长项目符号","完整短句"]}};
82
+ var CONT = {"pad":{"card":0.18,"panel":0.2,"cell":0.08,"band":0.16,"soWhat":0.16,"kpi":0.18,"chart":0.1,"head":0.05,"footer":0.05,"node":0.06},"minPad":0.06,"textMetrics":{"lineFactor":1.45,"emAsciiRatio":0.52},"overflowRule":"溢出处置优先级:① 优化内容(列表化/精炼/合并/拆页/换组合版式)→ ② 调整容器尺寸与行宽 → ③ 有限度缩小字号(见 fontShrink)→ ④ 仍装不下则拆页。禁止无限缩字号硬塞,也禁止把「精简」当第一手段砍掉决策必需信息。","fontShrink":{"ladder":[15,14,13.5,13,12.5,12,11.5,11,10.5,10,9.5,9,8.5],"maxShrinkSteps":4,"floorPt":{"presentation":10,"research":9,"architecture":10},"preferContentFirst":true,"warnBelow":9.5},"continuousTextRule":"语义连续的一句话 / so-what 主句 / 结论句 / 表格单元格正文不得拆成多个独立文本框(避免异常空格、断句、基线漂移);局部加粗高亮必须用同一文本框内富文本。"};
83
+ var REG = {"bar":{"html":"svg-bars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160}},"hbar":{"html":"svg-hbars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":100}},"stack":{"html":"svg-stack","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160},"nativeOpt":{"barGrouping":"stacked"}},"stackline":{"html":"svg-stackline","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":40},"nativeOpt":{"barGrouping":"percentStacked"}},"line":{"html":"svg-line","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"dualline":{"html":"svg-dualline","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"area":{"html":"svg-area","pptx":"native","nativeType":"area","dataTable":"notes","min":{"vbHeightMin":160}},"donut":{"html":"svg-donut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"multidonut":{"html":"svg-mdonut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"pie":{"html":"svg-pie","pptx":"native","nativeType":"pie","dataTable":"notes","min":{"pxWidthMin":140}},"radar":{"html":"svg-radar","pptx":"native","nativeType":"radar","dataTable":"notes","min":{"pxWidthMin":220}},"scatter":{"html":"svg-scatter","pptx":"native","nativeType":"scatter","dataTable":"notes","min":{"vbHeightMin":180}},"bubble":{"html":"svg-bubble","pptx":"native","nativeType":"bubble","dataTable":"notes","min":{"vbHeightMin":180}},"waterfall":{"html":"svg-wf","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":170},"nativeTrick":"stacked+hiddenBase+connector"},"gauge":{"html":"svg-gauge","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":180},"nativeTrick":"doughnut+firstSliceAng+hiddenRemainder"},"pareto":{"html":"svg-pareto","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":180},"nativeTrick":"multiType+bar+line"},"gantt":{"html":"svg-gantt","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":130},"path":{"forbidPresetShapes":true}},"vsbar":{"html":"svg-vsbar","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"progress":{"html":"svg-progress","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"sparkline":{"html":"svg-spark","pptx":"shape","dataTable":"off","min":{"vbHeightMin":20},"path":{"forbidPresetShapes":true}},"funnel":{"html":"svg-funnel","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"minSamplePoints":8,"forbidPresetShapes":true}},"treemap":{"html":"svg-treemap","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":170},"path":{"forbidPresetShapes":true}},"marimekko":{"html":"svg-marimekko","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"boxplot":{"html":"svg-boxplot","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"network":{"html":"svg-network","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"forbidPresetShapes":true}},"sankey":{"html":"svg-sankey","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"minSamplePoints":16,"trackBothEdges":true,"forbidPresetShapes":true}},"streamgraph":{"html":"svg-stream","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"minSamplePoints":24,"trackBothEdges":true,"forbidPresetShapes":true}},"slope":{"html":"svg-slope","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"dumbbell":{"html":"svg-dumbbell","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"lollipop":{"html":"svg-lollipop","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"dotplot":{"html":"svg-dotplot","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":120},"path":{"forbidPresetShapes":true}},"bulletchart":{"html":"svg-bullet","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"waffle":{"html":"svg-waffle","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"radialbar":{"html":"svg-radialbar","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":200},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"rose":{"html":"svg-rose","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":180},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"candlestick":{"html":"svg-candle","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}}};
84
+ var DEEP = {"triggers":["高保真","1:1","一比一","精确还原","按图还原","正式交付","不能偏移","像素级","严格对照","逐页验收"],"complexCharts":["sankey","streamgraph","treemap","marimekko","boxplot","network","rose","radialbar","candlestick","funnel"],"checks":{"anchors":true,"containerOverflow":true,"renderCompare":true,"manifest":true},"renderCompare":{"engine":"soffice","fallback":"powerpoint-com","skipIfMissing":true,"dpi":96}};
85
+ var IMG = {"layouts":["full","half","bleed","grid","compare","wall"],"multiLayouts":["grid","compare","wall"],"fitEnum":["cover","contain"],"fitDefault":"cover","formats":["png","jpeg","jpg","webp","svg"],"ratioDefault":{"full":"3:1","half":"4:3","bleed":"21:9","grid":"4:3","compare":"4:3","wall":"1:1"},"ratioCssClass":{"full":"media--r3-1","half":"media--r4-3","bleed":"media--r21-9","grid":"media--r4-3","compare":"media--r4-3","wall":"media--r1-1"},"recommendedSizePx":{"full":"2400×800","half":"1400×1050","bleed":"2560×1100","grid":"900×675","compare":"1400×1050","wall":"480×480"},"maxInlineBytes":1572864,"maxTotalInlineBytes":8388608,"maxPerPage":6,"placeholderLabel":"配图占位","placeholderHintPrefix":"建议 ","placeholderHintSuffix":"px","zeroExternal":true,"qualityRule":"位图优先提供 ≥2× 显示尺寸;截图用 16:9 或 4:3 锁定比例;数据图不要用截图(应用内联 SVG 图表,可随主题变色)。"};
86
+ var LAYOUT_SLOTS = {"version":"9.0","pageTypes":{"points":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".grid.g-2 / .card / .ul","pptx":"points columns","minUnits":1},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar / footnoteLine"}]},"metrics":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".metric","pptx":"metric row","minUnits":4},{"id":"annotation","role":"annotation","required":false,"html":"口径行 / .sowhat","pptx":"footnoteLine / soWhatBar"}]},"kpi":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"hero","role":"primary","required":true,"html":".t-metric hero","pptx":"hero number"},{"id":"metrics","role":"secondary","required":false,"html":".metric","pptx":"support metrics"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar"}]},"exhibit":{"mode":["research","presentation"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead / .exhibit__hd","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart] / .exhibit","pptx":"nativeChart | shapeChart","channel":"registry"},{"id":"secondary","role":"secondary","required":false,"html":"要点列表","pptx":"side bullets"},{"id":"annotation","role":"annotation","required":true,"html":".exhibit__src / .sowhat","pptx":"footnoteLine / soWhatBar / notes"}]},"split":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"left","role":"primary","required":true,"html":"左区(points/table/image/chart)","pptx":"left zone"},{"id":"right","role":"secondary","required":true,"html":"右区(points/table/image/chart)","pptx":"right zone"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar"}]},"table":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".tbl-wrap table","pptx":"addTable","maxRows":{"presentation":8,"research":16}},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar / footnoteLine"}]},"bar":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart]","pptx":"nativeChart | shapeChart","channel":"registry"},{"id":"annotation","role":"annotation","required":true,"html":"note / dataTable","pptx":"footnoteLine / notes / inline table"}]},"donut":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=donut]","pptx":"addChart doughnut"},{"id":"annotation","role":"annotation","required":false,"html":"中心合计 / 图例 / .note","pptx":"center label / legend / notes"}]},"twocol":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".cols-2","pptx":"two text columns"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"threecol":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".cols-3","pptx":"three text columns"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"halftable":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"左表","pptx":"addTable left"},{"id":"secondary","role":"secondary","required":true,"html":"右图","pptx":"chart right"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"matrix":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".matrix","pptx":"matrix cells"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"comparison":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"left","role":"primary","required":true,"html":"左面板","pptx":"left panel"},{"id":"right","role":"secondary","required":true,"html":"右面板","pptx":"right panel"},{"id":"annotation","role":"annotation","required":false,"html":"verdict / .sowhat","pptx":"verdict bar"}]},"quote":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"金句正文","pptx":"quote text"}]},"timeline":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".tl","pptx":"timeline axis"}]},"steps":{"mode":["presentation","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".steps","pptx":"step row"}]},"cards":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".card grid","pptx":"card grid"}]},"heatmap":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".heat","pptx":"heat cells"}]},"bullet":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".bul","pptx":"bullet rows"}]},"pyramid":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".pyr","pptx":"pyramid layers"}]},"image":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".media*","pptx":"imageLayoutShapes"}]},"diagram":{"mode":["architecture","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".arch / layers","pptx":"layered nodes + edges","maxNodes":24,"maxLayers":4},{"id":"annotation","role":"annotation","required":false,"html":".arch__legend / .note","pptx":"legend chips / notes"}]},"lane":{"mode":["architecture","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".lane","pptx":"lanes + steps"}]},"sankey":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=sankey]","pptx":"infoSankey","maxNodes":12},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"treemap":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=treemap]","pptx":"infoTreemap"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"boxplot":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=boxplot]","pptx":"infoBoxplot"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"network":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=network]","pptx":"infoNetwork"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"marimekko":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=marimekko]","pptx":"infoMarimekko"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"streamgraph":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=streamgraph]","pptx":"infoStreamgraph"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]}}};
87
+
88
+ /* regionOf(pageType, slotId, opts) — 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */
89
+ function regionOf(pageType, slotId, opts) {
90
+ var type = String(pageType || 'points'), slot = String(slotId || '');
91
+ opts = opts || {};
92
+ var L = PT.layout || {}, C = PT.common || {};
93
+ var topDefault = C.bodyY || 2.5;
94
+ var bottomDefault = opts.withNote ? (L.contentBottomWithNote || 6.4) : (L.contentBottom || 6.9);
95
+ if (slot === 'head' || slot === 'chrome') {
96
+ return { x: MX, y: C.headEyebrowY || 0.55, w: CW,
97
+ h: Math.max(0.4, (C.headRuleY || 1.95) - (C.headEyebrowY || 0.55)),
98
+ titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY };
99
+ }
100
+ if (slot === 'annotation' || slot === 'soWhat') {
101
+ var sy = (PT.exhibit && PT.exhibit.soWhatY != null) ? PT.exhibit.soWhatY : 6.05;
102
+ return { x: MX, y: sy, w: CW, h: 0.62 };
103
+ }
104
+ if (slot === 'footnote') {
105
+ var fy = (PT.exhibit && PT.exhibit.footnoteY != null) ? PT.exhibit.footnoteY : ((PT.note && PT.note.y) || 6.55);
106
+ return { x: MX, y: fy, w: CW - 1.2, h: (PT.note && PT.note.h) || 0.32 };
107
+ }
108
+ var P = PT[type] || {};
109
+ if (type === 'exhibit' && slot === 'primary') {
110
+ var ey = P.chartY || 2.95;
111
+ return { x: MX, y: ey, w: CW, h: Math.max(1.2, (L.contentBottomWithNote || 6.4) - ey), badgeY: P.badgeY };
112
+ }
113
+ if (type === 'metrics' && slot === 'primary') {
114
+ return { x: MX, y: P.bandY || 3.1, w: CW, h: P.cardH || 2.3, gap: P.gap, maxW: P.maxW,
115
+ valY: P.valY, valH: P.valH, capY: P.capY, capH: P.capH };
116
+ }
117
+ if (type === 'table' && slot === 'primary') {
118
+ var ty = P.y || topDefault;
119
+ return { x: MX, y: ty, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - ty) };
120
+ }
121
+ if (type === 'bar' && slot === 'primary') {
122
+ var by = P.chartY || 2.95;
123
+ var bb = opts.bottom != null ? opts.bottom : bottomDefault;
124
+ return { x: MX + (P.chartX || 0), y: by, w: CW - (P.chartW || 0), h: Math.max(1.2, bb - by) };
125
+ }
126
+ if (type === 'bar' && slot === 'hbar') {
127
+ var y0 = P.hbarY0 || 2.6;
128
+ var y1 = opts.bottom != null ? opts.bottom : (P.hbarY1 || 6.25);
129
+ return { x: MX, y: y0, w: CW, h: Math.max(1.2, y1 - y0) };
130
+ }
131
+ if (type === 'points' && (slot === 'primary' || slot === 'list')) {
132
+ var ly = P.listY || topDefault;
133
+ return { x: MX, y: ly, w: CW, h: Math.max(0.8, (opts.bottom != null ? opts.bottom : bottomDefault) - ly) };
134
+ }
135
+ if (type === 'split' && (slot === 'left' || slot === 'right')) {
136
+ var st = P.chartY || topDefault;
137
+ var sb = opts.bottom != null ? opts.bottom : bottomDefault;
138
+ if (slot === 'left') return { x: MX, y: st, w: CW * (P.leftW || 0.55), h: Math.max(1.2, sb - st) };
139
+ return { x: MX + CW * (P.rightX != null ? P.rightX : 0.585), y: st, w: CW * (P.rightW || 0.415), h: Math.max(1.2, sb - st) };
140
+ }
141
+ if (type === 'twocol' && (slot === 'primary' || slot === 'left' || slot === 'right')) {
142
+ var gap = P.colGap || 0.5, colW = (CW - gap) / 2;
143
+ var tt = opts.top != null ? opts.top : topDefault;
144
+ var tb = opts.bottom != null ? opts.bottom : bottomDefault;
145
+ if (slot === 'right') return { x: MX + colW + gap, y: tt, w: colW, h: Math.max(1, tb - tt) };
146
+ return { x: MX, y: tt, w: colW, h: Math.max(1, tb - tt) };
147
+ }
148
+ if ((type === 'diagram' || type === 'lane') && slot === 'primary') {
149
+ var arch = PT.arch || {};
150
+ var dy = (opts.mode === 'architecture' && arch.fullStartY != null) ? arch.fullStartY : (P.bodyStartY || 2.9);
151
+ var db = (opts.mode === 'architecture' && arch.fullEndY != null) ? arch.fullEndY
152
+ : (opts.bottom != null ? opts.bottom : bottomDefault);
153
+ return { x: MX, y: dy, w: CW, h: Math.max(1.2, db - dy) };
154
+ }
155
+ if (type === 'kpi' && (slot === 'hero' || slot === 'primary')) {
156
+ return { x: MX, y: P.heroY || 2.9, w: CW * (P.heroW || 0.52), h: P.heroH || 1.6,
157
+ dividerX: P.dividerX, metricY0: P.metricY0, metricRowH: P.metricRowH };
158
+ }
159
+ if (type === 'comparison' && (slot === 'left' || slot === 'primary' || slot === 'right')) {
160
+ var cg = P.panelGap || 0.4, pw = (CW - cg) / 2;
161
+ if (slot === 'right') return { x: MX + pw + cg, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
162
+ return { x: MX, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
163
+ }
164
+ if (type === 'quote' && slot === 'primary') {
165
+ return { x: MX + 1.05, y: P.textY || 2.7, w: CW - 2.1, h: P.textH || 2.3 };
166
+ }
167
+ if (type === 'heatmap' && slot === 'primary') {
168
+ var hy = P.y || 2.6;
169
+ return { x: MX, y: hy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - hy) };
170
+ }
171
+ if (type === 'bullet' && slot === 'primary') {
172
+ var uy = P.y || 2.6;
173
+ return { x: MX, y: uy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - uy) };
174
+ }
175
+ if (type === 'pyramid' && slot === 'primary') {
176
+ var py2 = P.y || 2.35;
177
+ return { x: MX, y: py2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - py2) };
178
+ }
179
+ if (type === 'steps' && slot === 'primary') {
180
+ return { x: MX, y: P.y || 3.05, w: CW, h: P.barH || 1.35 };
181
+ }
182
+ if (type === 'cards' && slot === 'primary') {
183
+ var cy2 = P.startY || 3.0;
184
+ return { x: MX, y: cy2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - cy2) };
185
+ }
186
+ if (type === 'timeline' && slot === 'primary') {
187
+ return { x: MX, y: P.labelY || 2.95, w: CW, h: Math.max(1, bottomDefault - (P.labelY || 2.95)) };
188
+ }
189
+ if (type === 'image' && slot === 'primary') {
190
+ var iy2 = Math.max(P.y || 2.3, opts.top || 0);
191
+ return { x: MX, y: iy2, w: CW, h: Math.max(1.2, Math.min(P.h || 4.2, (opts.bottom != null ? opts.bottom : bottomDefault) - iy2)) };
192
+ }
193
+ if ((type === 'halftable') && (slot === 'left' || slot === 'primary')) {
194
+ var rs = PT.research || {};
195
+ return { x: MX, y: topDefault, w: CW * (rs.halfTableW || 0.52), h: Math.max(1, bottomDefault - topDefault) };
196
+ }
197
+ if ((type === 'halftable') && (slot === 'right' || slot === 'secondary')) {
198
+ var rs2 = PT.research || {};
199
+ return { x: MX + CW * (rs2.halfChartX || 0.57), y: topDefault, w: CW * (rs2.halfChartW || 0.43), h: Math.max(1, bottomDefault - topDefault) };
200
+ }
201
+ if (type === 'matrix' && slot === 'primary') {
202
+ var rm = PT.research || {};
203
+ var my = rm.matrixY || 2.3;
204
+ return { x: MX, y: my, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - my) };
205
+ }
206
+ var INFO = { sankey:1, treemap:1, boxplot:1, network:1, marimekko:1, streamgraph:1 };
207
+ if (INFO[type] && slot === 'primary') {
208
+ var iy3 = P.y || topDefault;
209
+ var ih3 = Math.max(1.2, Math.min(P.h || 4.1, (opts.bottom != null ? opts.bottom : bottomDefault) - iy3));
210
+ return { x: MX, y: iy3, w: CW, h: ih3 };
211
+ }
212
+ return { x: MX, y: topDefault, w: CW, h: Math.max(0.8, bottomDefault - topDefault) };
213
+ }
214
+
215
+ /* __TOPPPT_CONSTANTS_END__ */
216
+
217
+ /* __TOPPPT_SCHEMA_START__ */
218
+ /* ── 由 scripts/sync_runtime.py 从 scripts/model-schema.json 注入 · 禁止手改 ── */
219
+ /* DSL schema 单源:浏览器端 validateModel 与 scripts/extract_model.py 消费同一份定义 */
220
+ var MODEL_SCHEMA = {"version":"9.0","modes":["presentation","research","architecture"],"model":{"required":["title","sections:array","closing.title","closing.points:array"],"optional":["mode","style","theme","subtitle","meta","agenda:array"],"agendaMin":{"presentation":3,"research":3,"architecture":0},"sectionsMin":1,"sectionsRecommended":3,"agendaComfortMax":16},"commonSectionFields":["eyebrow","title","lead","soWhat","footnote","flags","image","exhibitNo","layoutPreset"],"flagsHint":"flags = 待核实/待二次修改条目字符串数组(如 '2027 增速 xx%(口径未定)');渲染为 accent 强调色标注行,提示用户核对。","imageHint":"image = 素材图片/配图占位对象。三选一必填:src(用户图,data: 内联或相对路径,零外链铁律禁 http(s))|items(多图版式:grid/compare/wall,元素 {src?, alt?, caption?, placeholder?})|placeholder:true(无图时出配图占位,原生形状渲染、pictures 不增)。其余可选:layout(full/half/bleed/grid/compare/wall,见 layout-constants.json imageSpec.layouts)、fit(cover 默认 / contain)、caption、alt、hint、ratio。","imageLayoutHint":"layout 与 HTML 版式对应:full=版心全宽图|half=左图右注(points 为右栏注解)|bleed=通栏出血|grid=多图网格(items 2/3/4/6 张)|compare=双图 A/B 对比(items 2 张)|wall=Logo 墙(items 3–12 张小图)。占位符标签文本由双引擎按 imageSpec 拼同一串,保证 A/B 通道逐页文本一致。","chartTypes":["bar","hbar","stack","stackline","line","dualline","area","donut","multidonut","pie","radar","scatter","bubble","waterfall","gauge","pareto","funnel","gantt","vsbar","progress","sparkline","slope","dumbbell","lollipop","dotplot","bulletchart","waffle","radialbar","rose","candlestick"],"chartTypesHint":"chart.type 可取值 = 本表(30 类:16 原生 + 14 形状还原)。其余 6 类复杂信息图(sankey/treemap/boxplot/network/marimekko/streamgraph)是**专属页型**而非 chart.type——它们要按列/段/节点等结构化载荷表达,故走 sections[].type。不变量:charts.registry.types == chartTypes ∪ 上述 6 类页型(sync_runtime.py 校验)。","chartDataTable":{"enum":["notes","inline","appendix","off"],"default":"notes","rule":"非原生图表(registry.pptx=shape)的 dataTable 不得为 off;缺失时按 registry 默认值补齐。"},"pageTypes":{"points":{"label":"要点列表页","modes":["presentation","research","architecture"],"required":["points:array"],"optional":["metrics:array"]},"metrics":{"label":"指标带页(4–6 个核心数字)","modes":["presentation","research"],"required":["metrics:array"]},"kpi":{"label":"大数指标页(hero 大数字 + 支撑指标行)","modes":["presentation","research"],"required":["hero:array"],"optional":["metrics:array"]},"table":{"label":"对比表页","modes":["presentation","research","architecture"],"required":["table.head:array","table.rows:array"],"optional":["table.colW:array"]},"timeline":{"label":"时间线 / 路线页","modes":["presentation","research"],"required":["phases:array"]},"steps":{"label":"步骤条页(N 步横排 + 箭头,可分组)","modes":["presentation","research","architecture"],"required":["steps:array"],"optional":["groups:array","note"]},"bar":{"label":"图表页(chart.type 见 chartTypes;native 通道走原生数据图表,shape 通道走高保真形状还原 + 数据表)","modes":["presentation","research"],"required":["chart.labels:array","chart.values:array"],"optional":["chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.start:array","chart.target:array","chart.dataTable","note"]},"donut":{"label":"环形图页(原生 pie 楔形 + 图例)","modes":["presentation","research"],"required":["chart.labels:array","chart.values:array"],"optional":["chart.unit","chart.centerLabel","chart.colors:array","chart.dataTable","note"]},"heatmap":{"label":"热力矩阵页(行 × 列 + 强度色阶)","modes":["presentation","research"],"required":["rowHeads:array","colHeads:array","cells:array"],"optional":["unit","scaleLabel:array","note"]},"bullet":{"label":"达成对比页(实际 vs 目标条)","modes":["presentation","research"],"required":["items:array"],"optional":["unit","max","note"]},"pyramid":{"label":"金字塔页(层级递进,顶层最窄)","modes":["presentation","research","architecture"],"required":["levels:array"],"optional":["note"]},"image":{"label":"素材图片页(full/half/bleed/grid/compare/wall,效果与位置锁定;支持配图占位)","modes":["presentation","research","architecture"],"required":["anyof:image.src:str|image.items:array|image.placeholder"],"optional":["image.caption","image.alt","image.layout","image.fit","image.ratio","image.hint","points:array","note"]},"cards":{"label":"卡片网格页(cards 元素须为 {title, points:[[k,v]…]},不接受元组)","modes":["presentation","research"],"required":["cards:array"],"optional":["columns"]},"split":{"label":"双区组合页(left / right 各可为 要点|图表|表格|图片:type = points(默认)| table | image | 图表类型名;图表/表格字段直接挂在对应侧)","modes":["presentation","research"],"required":["anyof:left.points:array|left.type:str"],"optional":["left.cap","left.image","left.labels:array","left.values:array","left.series:array","left.points:array","left.max","left.unit","left.colors:array","left.dataTable","left.head:array","left.rows:array","left.colW:array","right.type","right.cap","right.image","right.labels:array","right.values:array","right.series:array","right.points:array","right.max","right.unit","right.colors:array","right.dataTable","right.head:array","right.rows:array","right.colW:array"]},"comparison":{"label":"对比页(左右双栏 + 可选结论条)","modes":["presentation","research"],"required":["left.title","left.points:array","right.title","right.points:array"],"optional":["verdict"]},"quote":{"label":"引用 / 金句页(深色全幅)","modes":["presentation","research"],"required":["quote","author"],"optional":["context"]},"diagram":{"label":"分层架构页(architecture 模式走全幅几何)","modes":["presentation","research","architecture"],"required":["layers:array"],"optional":["legend:array"]},"exhibit":{"label":"Exhibit 编号图表页(research R2)","modes":["research"],"required":["chart.labels:array","chart.values:array"],"optional":["exhibitNo","chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.dataTable","note"]},"twocol":{"label":"双栏论证页(research R1)","modes":["research"],"required":["paragraphs:array"]},"threecol":{"label":"三栏证据页(research R6)","modes":["research"],"required":["paragraphs:array"]},"halftable":{"label":"半表半图页(research R7)","modes":["research"],"required":["table.head:array","table.rows:array","chart.labels:array","chart.values:array"],"optional":["table.colW:array","chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.dataTable","note"]},"matrix":{"label":"矩阵图页(research R8)","modes":["research"],"required":["rowHeads:array","colHeads:array","cells:array"]},"lane":{"label":"泳道页(architecture A2)","modes":["architecture"],"required":["lanes:array"]},"sankey":{"label":"桑基图页(节点-流带,流向与流量)","modes":["research","architecture"],"required":["flows:array"],"optional":["unit","chart.dataTable","note"]},"treemap":{"label":"树图页(面积编码的层级构成)","modes":["research","architecture"],"required":["items:array"],"optional":["unit","chart.dataTable","note"]},"boxplot":{"label":"箱线图页(分布对比:min/q1/median/q3/max)","modes":["research","architecture"],"required":["groups:array"],"optional":["unit","chart.dataTable","note"]},"network":{"label":"关系网络页(节点-边拓扑)","modes":["research","architecture"],"required":["nodes:array","edges:array"],"optional":["chart.dataTable","note"]},"marimekko":{"label":"马赛克图页(列宽 × 列高双重编码)","modes":["research","architecture"],"required":["cols:array","cells:array"],"optional":["unit","chart.dataTable","note"]},"streamgraph":{"label":"流带图页(时间上的构成演变)","modes":["research","architecture"],"required":["series:array"],"optional":["labels:array","chart.dataTable","note"]}}};
221
+ /* __TOPPPT_SCHEMA_END__ */
222
+
223
+ var _mode = 'presentation'; /* 当前模式(slidesOf 里按 model.mode 设置) */
224
+ /* 三模式独立排版比例尺:调用点给 presentation 基准 pt → 取最接近的 TS_BASE 角色 →
225
+ 查 MTS[mode][role]。research 是咨询密排比例尺(正文 10.5pt),不再是简单 ×0.8。
226
+ 未知模式回落 presentation 档(MTS 与 mode 取值同源于 model-schema.json 的 modes)。 */
227
+ function modeSize(base) {
228
+ var mts = (typeof MTS !== 'undefined' && MTS) ? (MTS[_mode] || MTS.presentation) : null;
229
+ if (!mts) return base;
230
+ var best = null, bd = 1e9;
231
+ for (var k in TS_BASE) {
232
+ if (!TS_BASE.hasOwnProperty(k)) continue;
233
+ var d = Math.abs(TS_BASE[k] - base);
234
+ if (d < bd) { bd = d; best = mts[k]; }
235
+ }
236
+ /* 钳制与取整必须与 build_pptx.js 的 sz() 逐字一致——同一张表配不同后处理 = 两通道字号漂移 */
237
+ return Math.max(8.5, Math.round((best == null ? base : best) * 2) / 2);
238
+ }
239
+ /* 编码色板:9 套风格**各有自己的** c1–c5(单源 styleDataColors / styleDataColorsDark)。
240
+ 与 HTML 侧的 .f-c1~c5 语义类同源;结构色纪律不受影响——本表只用于数据系列。 */
241
+ function dataColors(style, theme) {
242
+ var dark = (theme === 'dark');
243
+ var map = (dark && typeof STYLE_DATA_COLORS_DARK !== 'undefined' && STYLE_DATA_COLORS_DARK)
244
+ ? STYLE_DATA_COLORS_DARK
245
+ : (typeof STYLE_DATA_COLORS !== 'undefined' ? STYLE_DATA_COLORS : null);
246
+ if (!map) return null;
247
+ var c = map[style];
248
+ return (c && c.length) ? c : null; // null = 用 S 的中性色(在调用处映射)
249
+ }
250
+
251
+ /* ── 版式安全边界(与 build_pptx.js 同一常量单源) ── */
252
+ var LAY = PT.layout || {};
253
+ var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
254
+ var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
255
+
256
+ /* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
257
+ function estLines(text, widthIn, fontPt) {
258
+ var usable = Math.max(24, widthIn * 72 - 8), w = 0, lines = 1;
259
+ var s = String(text == null ? '' : text);
260
+ for (var i = 0; i < s.length; i++) {
261
+ if (s[i] === '\n') { lines++; w = 0; continue; }
262
+ var cw = /[\x00-\xff]/.test(s[i]) ? fontPt * 0.52 : fontPt;
263
+ if (w + cw > usable) { lines++; w = cw; } else { w += cw; }
264
+ }
265
+ return lines;
266
+ }
267
+ function estTextH(text, widthIn, fontPt, lineFactor) {
268
+ return estLines(text, widthIn, fontPt) * fontPt * (lineFactor || 1.45) / 72;
269
+ }
270
+ var FONT_LADDER = [15, 14, 13.5, 13, 12.5, 12, 11.5, 11, 10.5, 10, 9.5, 9, 8.5];
271
+ /* 有限缩字号(v8.2,与 build_pptx.js 同策略):先优化内容/组合/拆页,最后才缩字号。
272
+ maxShrinkSteps 限制下探步数;floorPt 按模式绝对下限。 */
273
+ function modeFloorPtA(mode) {
274
+ var floors = (typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.floorPt) ||
275
+ { presentation: 10, research: 9, architecture: 10 };
276
+ var m = String(mode || 'presentation');
277
+ return (floors[m] != null) ? floors[m] : 10;
278
+ }
279
+ function fitFont(items, widthIn, availIn, opts) {
280
+ opts = opts || {};
281
+ var max = (opts.max == null) ? 99 : opts.max, lf = opts.lineFactor || 1.45;
282
+ var gapF = (opts.gapFactor == null) ? 0.5 : opts.gapFactor;
283
+ var mode = (typeof CONTENT !== 'undefined' && CONTENT && CONTENT.mode) ||
284
+ (typeof TOPPPT_MODE !== 'undefined' ? TOPPPT_MODE : 'presentation');
285
+ var floor = (opts.floor != null) ? opts.floor : modeFloorPtA(mode);
286
+ var maxSteps = (opts.maxShrinkSteps != null) ? opts.maxShrinkSteps
287
+ : ((typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.maxShrinkSteps != null)
288
+ ? CONT.fontShrink.maxShrinkSteps : 4);
289
+ var baseLadder = (typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.ladder) || FONT_LADDER;
290
+ var raw = baseLadder.filter(function (v) { return v <= max && v >= floor; });
291
+ var needH = function (f) {
292
+ var h = 0;
293
+ for (var i = 0; i < items.length; i++) h += estTextH(items[i], widthIn, f, lf) + f * gapF / 72;
294
+ return h + (opts.pad || 0);
295
+ };
296
+ for (var li = 0; li < raw.length; li++) {
297
+ if (needH(raw[li]) <= availIn) return raw[li];
298
+ }
299
+ var preferred = raw.length ? raw[0] : floor;
300
+ var prefIdx = baseLadder.indexOf(preferred);
301
+ var limited = baseLadder.slice(prefIdx, prefIdx + 1 + maxSteps)
302
+ .filter(function (v) { return v >= floor && v <= max; });
303
+ for (var lj = 0; lj < limited.length; lj++) {
304
+ if (needH(limited[lj]) <= availIn) return limited[lj];
305
+ }
306
+ if (limited.length) return limited[limited.length - 1];
307
+ return floor;
308
+ }
309
+ function cols(total, n, gap) {
310
+ var w = (total - (n - 1) * (gap || 0)) / Math.max(1, n);
311
+ return { w: w, step: w + (gap || 0) };
312
+ }
313
+ /* 行高收敛:与 build_pptx.js 同算法(宁可行高更小也不越界) */
314
+ function fitRowH(avail, n, pref, floor) {
315
+ var lo = (floor == null) ? 0.22 : floor;
316
+ return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
317
+ }
318
+ function chartBottom(hasSoWhat, hasFootnote) {
319
+ if (hasFootnote) return PT.exhibit.footnoteY - 0.12;
320
+ if (hasSoWhat) return PT.exhibit.soWhatY - 0.12;
321
+ return CONTENT_BOTTOM;
322
+ }
323
+
324
+ /* ── XML 工具 ── */
325
+ function esc(s) {
326
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
327
+ .replace(/"/g, '&quot;').replace(/'/g, '&apos;');
328
+ }
329
+ function E(v) { return Math.round(v * EMU); }
330
+ function solid(hex) { return '<a:solidFill><a:srgbClr val="' + hex + '"/></a:solidFill>'; }
331
+
332
+ var _id = 10;
333
+ function nid() { return ++_id; }
334
+
335
+ /* 文本框:paras = [[{t,b,sz,col,font,spc}…]…](每个内层数组=一段,可混排多 run)
336
+ opts.lineSpacing / opts.spaceBefore 为基准 pt(与 build_pptx.js 的 lineSpacing/paraSpaceBefore 同源),
337
+ 序列化时统一过 modeSize 映射,保证两通道行距一致。 */
338
+ function txSp(x, y, w, h, paras, opts) {
339
+ opts = opts || {};
340
+ var pPr = '';
341
+ if (opts.lineSpacing) pPr += '<a:lnSpc><a:spcPts val="' + Math.round(modeSize(opts.lineSpacing) * 100) + '"/></a:lnSpc>';
342
+ if (opts.spaceBefore) pPr += '<a:spcBef><a:spcPts val="' + Math.round(modeSize(opts.spaceBefore) * 100) + '"/></a:spcBef>';
343
+ var pPrXml = (opts.align || pPr)
344
+ ? '<a:pPr' + (opts.align ? ' algn="' + opts.align + '"' : '') + '>' + pPr + '</a:pPr>'
345
+ : '';
346
+ var body = paras.map(function (runs) {
347
+ var rs = runs.map(function (r) {
348
+ var pr = '<a:rPr lang="zh-CN" sz="' + Math.round(modeSize(r.sz) * 100) + '"' +
349
+ (r.b ? ' b="1"' : '') + (r.spc ? ' spc="' + r.spc + '"' : '') + '>' +
350
+ solid(r.col) +
351
+ '<a:latin typeface="' + esc(r.font) + '"/><a:ea typeface="' + esc(r.font) + '"/></a:rPr>';
352
+ return '<a:r>' + pr + '<a:t>' + esc(r.t) + '</a:t></a:r>';
353
+ }).join('');
354
+ return '<a:p>' + pPrXml + rs + '</a:p>';
355
+ }).join('');
356
+ var fill = opts.fill ? solid(opts.fill) : '<a:noFill/>';
357
+ return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="tx"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>' +
358
+ '<p:spPr><a:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(w) + '" cy="' + E(h) + '"/></a:xfrm>' +
359
+ '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' + fill + '<a:ln><a:noFill/></a:ln></p:spPr>' +
360
+ '<p:txBody><a:bodyPr wrap="square" anchor="' + (opts.anchor || 't') + '"><a:normAutofit/></a:bodyPr><a:lstStyle/>' +
361
+ body + '</p:txBody></p:sp>';
362
+ }
363
+
364
+ /* 形状:rect / roundRect / ellipse / pie(opts.av = avLst 原始内联 XML,供 donut 楔形角度) */
365
+ function shape(geom, x, y, w, h, fillHex, opts) {
366
+ opts = opts || {};
367
+ var av;
368
+ if (opts.av) av = '<a:avLst>' + opts.av + '</a:avLst>';
369
+ else if (geom === 'roundRect') av = '<a:avLst><a:gd name="adj" fmla="val ' + (opts.adj || 8000) + '"/></a:avLst>';
370
+ else av = '<a:avLst/>';
371
+ var ln = opts.line
372
+ ? '<a:ln w="9525">' + (opts.dash ? '<a:prstDash val="dash"/>' : '') + solid(opts.line) + '</a:ln>'
373
+ : '<a:ln><a:noFill/></a:ln>';
374
+ return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="' + (opts.name || 'sh') + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
375
+ '<p:spPr><a:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(Math.max(w, 0.01)) + '" cy="' + E(Math.max(h, 0.01)) + '"/></a:xfrm>' +
376
+ '<a:prstGeom prst="' + geom + '">' + av + '</a:prstGeom>' + solid(fillHex) + ln + '</p:spPr>' +
377
+ '<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="zh-CN"/></a:p></p:txBody></p:sp>';
378
+ }
379
+ /* pie 楔形 avLst:角度(度)× 60000;donut 页型两通道共用同一角度拆分规则 */
380
+ function pieAv(a1, a2) {
381
+ return '<a:gd name="adj1" fmla="val ' + Math.round(a1 * 60000) + '"/>' +
382
+ '<a:gd name="adj2" fmla="val ' + Math.round(a2 * 60000) + '"/>';
383
+ }
384
+ /* 环形图(donut 页型):原生 pie 楔形 + 背景色圆(挖孔)+ 中心文字 + 右侧图例。
385
+ 角度约定:12 点钟(270°)起顺时针;跨 360° 的楔形拆成两段(≤359.9 封顶)。 */
386
+ function donutWedges(labels, values) {
387
+ var total = 0;
388
+ (values || []).forEach(function (v) { total += Math.max(0, v); });
389
+ var segs = [], a = 270;
390
+ (values || []).forEach(function (v, i) {
391
+ var frac = total > 0 ? Math.max(0, v) / total : 0;
392
+ var b = a + frac * 360;
393
+ var s = a % 360, e = b % 360;
394
+ if (frac <= 0) { a = b; return; }
395
+ if (b - a >= 359.9) { segs.push([0, 359.9, i]); }
396
+ else if (b <= 360) { segs.push([a, Math.min(b, 359.9), i]); }
397
+ else { segs.push([a, 359.9, i]); if (e > 0.1) segs.push([0, e, i]); }
398
+ a = b;
399
+ });
400
+ return segs;
401
+ }
402
+ function donutChart(sh, ec, S, dcols) {
403
+ var d = PT.donut, labels = ec.labels || [], values = ec.values || [];
404
+ var segs = donutWedges(labels, values);
405
+ var bx = d.centerX - d.r, by = d.centerY - d.r, bw = d.r * 2;
406
+ segs.forEach(function (sg) {
407
+ sh.push(shape('pie', bx, by, bw, bw, dcols[sg[2] % dcols.length], { av: pieAv(sg[0], sg[1]) }));
408
+ });
409
+ var hb = d.holeR * 2;
410
+ sh.push(shape('ellipse', d.centerX - d.holeR, d.centerY - d.holeR, hb, hb, S.bg));
411
+ var total = 0; values.forEach(function (v) { total += Math.max(0, v); });
412
+ var totalStr = (Math.round(total * 10) / 10) + (ec.unit || '');
413
+ sh.push(txSp(d.centerX - d.holeR, d.centerY - 0.42, hb, 0.5,
414
+ [[{ t: totalStr, sz: 20, b: 1, col: S.ink, font: S.fontDisplay }]], { align: 'ctr', anchor: 'ctr' }));
415
+ sh.push(txSp(d.centerX - d.holeR, d.centerY + 0.08, hb, 0.35,
416
+ [[{ t: ec.centerLabel || '合计', sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
417
+ var n = Math.max(1, labels.length);
418
+ var rowH = Math.min(d.legendRowH, 3.4 / n);
419
+ var ly0 = d.centerY - (n * rowH) / 2 + 0.1;
420
+ var sum2 = total || 1;
421
+ labels.forEach(function (lb, i) {
422
+ var v = values[i] || 0;
423
+ var y = ly0 + i * rowH;
424
+ sh.push(shape('ellipse', d.legendX, y + 0.08, 0.16, 0.16, dcols[i % dcols.length]));
425
+ sh.push(txSp(d.legendX + 0.3, y, d.legendLabelW, rowH,
426
+ [[{ t: lb, sz: 12, b: 1, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
427
+ sh.push(txSp(d.legendX + 3.8, y, d.legendValW, rowH,
428
+ [[{ t: String(v) + (ec.unit || ''), sz: 12, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
429
+ sh.push(txSp(d.legendPctX, y, d.legendPctW, rowH,
430
+ [[{ t: (Math.round(v / sum2 * 1000) / 10) + '%', sz: 10.5, col: S.faint, font: S.font }]], { align: 'r', anchor: 'ctr' }));
431
+ });
432
+ }
433
+ function donutColors(style, S, theme) {
434
+ var d = dataColors(style, theme);
435
+ return d || [S.accent, S.faint, S.body, S.line];
436
+ }
437
+
438
+ /* 表格(graphicFrame + a:tbl);rows[0]=表头;行高按可用高度自适应(与 B 通道同规则) */
439
+ function table(x, y, w, colW, rows, S, opts) {
440
+ opts = opts || {};
441
+ var n = rows.length;
442
+ var maxRowH = (opts.maxRowH == null) ? PT.table.rowH : opts.maxRowH;
443
+ var minRowH = (opts.minRowH == null) ? PT.table.rowHMin : opts.minRowH;
444
+ var availH = (opts.availH == null) ? (CONTENT_BOTTOM - y) : opts.availH;
445
+ var rowH = Math.max(minRowH, Math.min(maxRowH, availH / Math.max(1, n)));
446
+ var dense = rowH < 0.42;
447
+ var total = colW.reduce(function (a, b) { return a + b; }, 0);
448
+ var grid = colW.map(function (cw) { return '<a:gridCol w="' + E(cw) + '"/>'; }).join('');
449
+ var trs = rows.map(function (row, ri) {
450
+ var tcs = row.map(function (cell) {
451
+ var isHead = ri === 0;
452
+ var fz = isHead ? (dense ? 11 : 13) : (dense ? 10 : 12.5);
453
+ return '<a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r>' +
454
+ '<a:rPr lang="zh-CN" sz="' + Math.round(modeSize(fz) * 100) + '"' + (isHead ? ' b="1"' : '') + '>' +
455
+ solid(isHead ? S.onAccent : S.body) +
456
+ '<a:latin typeface="' + S.font + '"/><a:ea typeface="' + S.font + '"/></a:rPr>' +
457
+ '<a:t>' + esc(cell) + '</a:t></a:r></a:p></a:txBody>' +
458
+ '<a:tcPr marL="91440" marR="91440" marT="45720" marB="45720" anchor="ctr">' +
459
+ solid(isHead ? S.accent : S.bg) + '</a:tcPr></a:tc>';
460
+ }).join('');
461
+ return '<a:tr h="' + E(rowH) + '">' + tcs + '</a:tr>';
462
+ }).join('');
463
+ /* OOXML 规范:p:graphicFrame 的 graphic 子元素属 DrawingML 命名空间(a:graphic,与 pptxgenjs 一致) */
464
+ return '<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="' + nid() + '" name="tbl"/>' +
465
+ '<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr>' +
466
+ '<p:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(w) + '" cy="' + E(rowH * rows.length) + '"/></p:xfrm>' +
467
+ '<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">' +
468
+ '<a:tbl><a:tblPr firstRow="1" bandRow="0"/><a:tblGrid>' + grid + '</a:tblGrid>' + trs + '</a:tbl>' +
469
+ '</a:graphicData></a:graphic></p:graphicFrame>';
470
+ }
471
+
472
+ /* so-what 结论条 / 页脚来源行(research 页通用可选件,与 HTML 同源版式;区域走布局 IR) */
473
+ function soWhatBar(sh, text, S) {
474
+ var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'annotation')
475
+ : { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
476
+ /* 与 B 通道同算法:长结论条有限缩字号,优先保容器不溢出 */
477
+ var bodyW = Math.max(2.5, R.w - 0.44 - 1.35);
478
+ var fzB = fitFont([String(text || '')], bodyW, R.h - 0.14, { max: 12.5, gapFactor: 0.2, maxShrinkSteps: 3 });
479
+ sh.push(shape('rect', R.x, R.y, R.w, R.h, S.soft));
480
+ sh.push(shape('rect', R.x, R.y, 0.06, R.h, S.accent));
481
+ sh.push(txSp(R.x + 0.22, R.y + 0.06, R.w - 0.44, R.h - 0.12,
482
+ [[{ t: 'SO WHAT ', sz: 11, b: 1, col: S.accent, font: S.font, spc: 150 },
483
+ { t: text, sz: fzB, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
484
+ }
485
+ function footnoteLine(sh, text, S) {
486
+ var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'footnote')
487
+ : { x: MX, y: PT.exhibit.footnoteY, w: CW - 1.2, h: 0.32 };
488
+ sh.push(txSp(R.x, R.y, R.w, R.h,
489
+ [[{ t: text, sz: 9.5, col: S.faint, font: S.font }]]));
490
+ }
491
+ /* 待核实清单条(accent 强调色标注 · 与 build_pptx.js / HTML .flagbar 同源) */
492
+ function flagBar(sh, items, S, yTop) {
493
+ var F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
494
+ var list = (items || []).slice(0, F.maxRows).map(String);
495
+ if (!list.length) return 0;
496
+ var h = F.hdH + list.length * F.rowH + 0.06;
497
+ var y = (yTop != null) ? yTop : (CONTENT_BOTTOM - h);
498
+ sh.push(shape('rect', MX, y, CW, h, S.soft));
499
+ sh.push(shape('rect', MX, y, 0.055, h, S.accent));
500
+ sh.push(txSp(MX + 0.22, y + 0.02, CW - 0.44, F.hdH,
501
+ [[{ t: '待核实 · 需二次确认', sz: 9.5, b: 1, col: S.accent, font: S.font, spc: 100 }]]));
502
+ list.forEach(function (t, i) {
503
+ sh.push(txSp(MX + 0.24, y + F.hdH + i * F.rowH, CW - 0.5, F.rowH,
504
+ [[{ t: '· ' + t, sz: 9.5, col: S.body, font: S.font }]], { anchor: 'ctr' }));
505
+ });
506
+ return h;
507
+ }
508
+ /* 素材图片:预览引擎只产出 slide XML(无 media part)→ 以中性图位块表示;
509
+ 配图占位(placeholder)额外发射标签文本——标签串由 imageSpec 常量拼接,与 B 通道**同串**
510
+ (cross_verify 逐页比对)。真实图片不发射任何文本,保证 A/B 文本集合一致。 */
511
+ function imgPlaceholderLabel(layout, multi) {
512
+ var IS = (typeof IMG !== 'undefined' && IMG) || {};
513
+ var label = IS.placeholderLabel || '配图占位';
514
+ if (multi) return label;
515
+ var size = (IS.recommendedSizePx || {})[layout];
516
+ if (!size) return label;
517
+ return label + ' · ' + (IS.placeholderHintPrefix || '建议 ') + size + (IS.placeholderHintSuffix || 'px');
518
+ }
519
+ function addImageEl(sh, src, S, x, y, w, h, opts) {
520
+ opts = opts || {};
521
+ var ph = !!opts.placeholder || !src;
522
+ /* name="img"/"imgph":预览模态据此把图位替换为真实图片(ui.js renderSlide),
523
+ 不影响文本集合(cross_verify 只比文本)。 */
524
+ sh.push(shape('roundRect', x, y, w, h, S.surface,
525
+ { line: S.line, dash: ph, adj: 4000, name: ph ? 'imgph' : 'img' }));
526
+ if (ph) {
527
+ sh.push(txSp(x + 0.08, y + h / 2 - 0.22, Math.max(0.4, w - 0.16), 0.44,
528
+ [[{ t: imgPlaceholderLabel(opts.layout || 'full', !!opts.multi), sz: 11, b: 1, col: S.faint, font: S.font }]],
529
+ { align: 'ctr', anchor: 'ctr' }));
530
+ }
531
+ }
532
+ /* 版式比例(imageSpec.ratioDefault 或 image.ratio,形如 "3:1")→ 数值宽高比。
533
+ 双引擎同源:HTML 用同比例的 .media--r* 锁定类,PPTX 用本函数算高度,版式才一致。 */
534
+ function imgRatio(layout, img) {
535
+ var IS = (typeof IMG !== 'undefined' && IMG) || {};
536
+ var raw = String((img && img.ratio) || (IS.ratioDefault || {})[layout] || '3:1');
537
+ var m = raw.match(/^\s*(\d+(?:\.\d+)?)\s*[:/×x]\s*(\d+(?:\.\d+)?)/);
538
+ if (!m) return 3;
539
+ var a = parseFloat(m[1]), b = parseFloat(m[2]);
540
+ return (a > 0 && b > 0) ? a / b : 3;
541
+ }
542
+ /* 图片版式族(与 HTML .media / .media-grid / .media-compare / .media-wall 同源):
543
+ full 版心全宽|half 左图右注|bleed 通栏出血|grid 多图网格|compare 双图对比|wall Logo 墙。
544
+ 每版式按 imageSpec.ratioDefault 锁定比例;空间不足时按可用高度收敛并**垂直居中**
545
+ (避免"图贴顶、下方大片留白")。返回图注基准 Y。占位符走原生形状,pictures 不增。 */
546
+ function imageLayoutShapes(sh, S, layout, img, items, points, isPh, fit, y0, y1) {
547
+ var IM = PT.image || {};
548
+ var gap = IM.gridGap || 0.16;
549
+ var iy = y0;
550
+ var ih = Math.max(1.0, y1 - iy);
551
+ var capY = iy + ih;
552
+ var ratio = imgRatio(layout, img);
553
+ function one(x, y, w, h, it, multi) {
554
+ var ph = isPh || !(it && it.src);
555
+ addImageEl(sh, (it && it.src) || '', S, x, y, w, h,
556
+ { placeholder: ph, layout: layout, multi: multi, fit: fit });
557
+ }
558
+ if (layout === 'grid' && items.length) {
559
+ var n = Math.min(items.length, (IMG && IMG.maxPerPage) || 6);
560
+ var colsN = (IM.gridCols || {})[String(n)] || Math.min(3, n);
561
+ var rowsN = Math.max(1, Math.ceil(n / colsN));
562
+ var capH = IM.gridCapH || 0.28;
563
+ var cellW = (CW - (colsN - 1) * gap) / colsN;
564
+ var capRows = rowsN * (capH + 0.04);
565
+ var cellH = Math.min(cellW / ratio, Math.max(0.5, (ih - (rowsN - 1) * gap - capRows) / rowsN));
566
+ var blockH = rowsN * cellH + (rowsN - 1) * gap + capRows;
567
+ var gy = iy + Math.max(0, (ih - blockH) / 2);
568
+ items.slice(0, n).forEach(function (it, i) {
569
+ var cx = MX + (i % colsN) * (cellW + gap);
570
+ var cy = gy + Math.floor(i / colsN) * (cellH + gap + capH + 0.04);
571
+ var cap = (it && it.caption) || '';
572
+ one(cx, cy, cellW, cellH, it, true);
573
+ if (cap) sh.push(txSp(cx, cy + cellH + 0.02, cellW, capH,
574
+ [[{ t: cap, sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
575
+ });
576
+ capY = gy + blockH;
577
+ } else if (layout === 'compare' && items.length >= 2) {
578
+ var cgap = IM.compareGap || 0.3;
579
+ var cw2 = (CW - cgap) / 2;
580
+ var capH2 = 0.34;
581
+ var ih2 = Math.min(cw2 / ratio, Math.max(0.5, ih - capH2));
582
+ var cy2 = iy + Math.max(0, (ih - ih2 - capH2) / 2);
583
+ [0, 1].forEach(function (i) {
584
+ var it = items[i] || {};
585
+ var cx2 = MX + i * (cw2 + cgap);
586
+ one(cx2, cy2, cw2, ih2, it, true);
587
+ var cap2 = it.caption || '';
588
+ if (cap2) sh.push(txSp(cx2, cy2 + ih2 + 0.02, cw2, 0.3,
589
+ [[{ t: cap2, sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
590
+ });
591
+ capY = cy2 + ih2 + capH2;
592
+ } else if (layout === 'wall' && items.length) {
593
+ var wgap = IM.wallGap || 0.18;
594
+ var wn = Math.min(items.length, 12);
595
+ var rowsW = Math.max(1, Math.min(IM.wallMaxRows || 2, Math.ceil(wn / 6)));
596
+ var perRow = Math.max(1, Math.ceil(wn / rowsW));
597
+ var cellW2 = (CW - (perRow - 1) * wgap) / perRow;
598
+ var cellH2 = Math.min(IM.wallCellH || 0.92, cellW2 / ratio,
599
+ Math.max(0.4, (ih - (rowsW - 1) * wgap) / rowsW));
600
+ var blockH2 = rowsW * cellH2 + (rowsW - 1) * wgap;
601
+ var wy = iy + Math.max(0, (ih - blockH2) / 2);
602
+ items.slice(0, wn).forEach(function (it, i) {
603
+ var cx3 = MX + (i % perRow) * (cellW2 + wgap);
604
+ var cy3 = wy + Math.floor(i / perRow) * (cellH2 + wgap);
605
+ one(cx3, cy3, cellW2, cellH2, it, true);
606
+ });
607
+ capY = wy + blockH2;
608
+ } else if (layout === 'half') {
609
+ var iw = CW * (IM.halfW || 0.56);
610
+ var ihh = Math.min(iw / ratio, ih);
611
+ one(MX, iy, iw, ihh, img, false);
612
+ var rxI = MX + iw + CW * (IM.halfGap || 0.06);
613
+ var rwI = CW - iw - CW * (IM.halfGap || 0.06);
614
+ var ptsI = points || [];
615
+ var rowHI = fitRowH(ih - 0.1, ptsI.length, IM.noteRowH || 0.62, 0.3);
616
+ var fzI = fitFont(ptsI.map(function (p) { return (p[0] || '') + (p[1] || ''); }), rwI - 0.3, ih,
617
+ { max: 14, gapFactor: 0.6 });
618
+ ptsI.forEach(function (pt, i) {
619
+ var lyI = iy + i * rowHI;
620
+ sh.push(shape('rect', rxI, lyI + rowHI / 2 - 0.06, 0.12, 0.12, S.accent));
621
+ sh.push(txSp(rxI + 0.26, lyI, rwI - 0.3, rowHI - 0.04,
622
+ [[{ t: (pt[0] || '') + ' ', sz: fzI, b: 1, col: S.ink, font: S.font },
623
+ { t: pt[1] || '', sz: fzI - 1, col: S.body, font: S.font }]], { anchor: 't' }));
624
+ });
625
+ capY = iy + Math.max(ihh, ih * 0.5);
626
+ } else if (layout === 'bleed') {
627
+ var bh = Math.min(ih, PW / ratio);
628
+ var by = iy + Math.max(0, (ih - bh) / 2);
629
+ one(0, by, PW, bh, img, false);
630
+ capY = by + bh;
631
+ } else {
632
+ var fh = Math.min(ih, CW / ratio);
633
+ var fy = iy + Math.max(0, (ih - fh) / 2);
634
+ one(MX, fy, CW, fh, img, false);
635
+ capY = fy + fh;
636
+ }
637
+ return capY;
638
+ }
639
+
640
+ /* ══ 图表形状渲染(预览/回归用近似版;交付通道 B 用原生可编辑数据图表) ══
641
+ A/B 两通道的「文本集合」必须一致(cross_verify 逐页比对):
642
+ 类别标签 + 数值标签都渲染为文本,数值标签由 cross_verify 的数值 token 规则过滤。 */
643
+ function valText(v, unit) { return String(v) + (unit || ''); }
644
+ /* 横向条形(hbar / halftable / exhibit hbar) */
645
+ function hbarRows(sh, ec, S, y0, y1, dcols, x0, w) {
646
+ x0 = (x0 == null) ? MX : x0;
647
+ w = (w == null) ? CW : w;
648
+ var labels = ec.labels || [], values = ec.values || [];
649
+ var n = Math.max(1, labels.length);
650
+ var rowH = Math.min(PT.hbar.rowH, (y1 - y0) / n);
651
+ var barH = Math.max(0.16, rowH * (PT.hbar.barRatio || 0.5));
652
+ var labelW = PT.hbar.labelW, valW = PT.hbar.valW;
653
+ var trackX = x0 + labelW + 0.1, trackW = w - labelW - valW - 0.2;
654
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
655
+ var top = values.length ? Math.max.apply(null, values) : 0;
656
+ labels.forEach(function (lb, i) {
657
+ var v = values[i] || 0;
658
+ var y = y0 + i * rowH + (rowH - barH) / 2;
659
+ var hot = v === top;
660
+ var dc = dcols ? dcols[i % 5] : null;
661
+ sh.push(txSp(x0, y - 0.06, labelW - 0.05, barH + 0.12,
662
+ [[{ t: lb, sz: 11, col: S.body, font: S.font }]], { anchor: 'ctr' }));
663
+ sh.push(shape('rect', trackX, y, trackW, barH, S.surface, { line: S.line }));
664
+ var bw2 = Math.max(0.02, v / vmax * trackW);
665
+ sh.push(shape('rect', trackX, y, bw2, barH, dc || (hot ? S.accent : S.faint)));
666
+ sh.push(txSp(trackX + trackW + 0.08, y - 0.06, valW, barH + 0.12,
667
+ [[{ t: valText(v, ec.unit), sz: 11.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]], { anchor: 'ctr' }));
668
+ });
669
+ }
670
+ /* 竖柱 / 堆叠柱 */
671
+ function vbarShapes(sh, ec, S, x, y, w, h, dcols) {
672
+ var labels = ec.labels || [], values = ec.values || [];
673
+ var nb = Math.max(1, labels.length);
674
+ var baseY = y + h, plotW = w, step = plotW / nb;
675
+ var bw = Math.min(1.1, step * 0.55);
676
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
677
+ var top = values.length ? Math.max.apply(null, values) : 0;
678
+ sh.push(shape('rect', x, baseY, plotW, 0.02, S.line));
679
+ values.forEach(function (v, i) {
680
+ var hh = Math.max(0.05, v / vmax * (h - 0.5));
681
+ var bx = x + i * step + (step - bw) / 2;
682
+ var hot = v === top;
683
+ var dc = dcols ? dcols[i % 5] : null;
684
+ sh.push(shape('roundRect', bx, baseY - hh, bw, hh, dc || (hot ? S.accent : S.faint), { adj: 6000 }));
685
+ sh.push(txSp(bx - 0.3, baseY - hh - 0.4, bw + 0.6, 0.32,
686
+ [[{ t: valText(v, ec.unit), sz: 12.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]], { align: 'ctr' }));
687
+ sh.push(txSp(bx - 0.3, baseY + 0.08, bw + 0.6, 0.32,
688
+ [[{ t: labels[i], sz: 10.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
689
+ });
690
+ }
691
+ /* 折线 / 双折线 / 面积 */
692
+ function lineShapes(sh, ec, S, x, y, w, h, dcols) {
693
+ var labels = ec.labels || [];
694
+ var series = (ec.series && ec.series.length) ? ec.series : [{ name: '数值', values: ec.values || [] }];
695
+ var n = Math.max(1, labels.length);
696
+ var all = [];
697
+ series.forEach(function (se) { (se.values || []).forEach(function (v) { all.push(Number(v) || 0); }); });
698
+ var vmax = ec.max || Math.max.apply(null, all.concat([1]));
699
+ var plotH = h - 0.5, baseY = y + h;
700
+ sh.push(shape('rect', x, baseY, w, 0.02, S.line));
701
+ var stepX = n > 1 ? w / (n - 1) : 0;
702
+ series.forEach(function (se, si) {
703
+ var col = dcols ? dcols[si % 5] : (si === 0 ? S.accent : S.faint);
704
+ var pts = (se.values || []).map(function (v, i) {
705
+ var px = x + (n > 1 ? i * stepX : w / 2);
706
+ var py = baseY - Math.max(0.02, (Number(v) || 0) / vmax * plotH);
707
+ return [px, py];
708
+ });
709
+ if (!pts.length) return;
710
+ for (var i = 0; i < pts.length - 1; i++) {
711
+ sh.push(lineSeg(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], col, 2.5));
712
+ }
713
+ pts.forEach(function (p) {
714
+ sh.push(shape('ellipse', p[0] - 0.055, p[1] - 0.055, 0.11, 0.11, col));
715
+ });
716
+ });
717
+ labels.forEach(function (lb, i) {
718
+ var px = x + (n > 1 ? i * stepX : w / 2);
719
+ sh.push(txSp(px - 0.5, baseY + 0.08, 1.0, 0.32,
720
+ [[{ t: lb, sz: 10.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
721
+ });
722
+ }
723
+ /* 直线段(用细长矩形近似,避免自定义几何——两通道渲染一致) */
724
+ function lineSeg(x1, y1, x2, y2, hex, thick) {
725
+ var dx = x2 - x1, dy = y2 - y1;
726
+ var len = Math.sqrt(dx * dx + dy * dy);
727
+ var ang = Math.atan2(dy, dx) * 180 / Math.PI;
728
+ var cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
729
+ var t = thick / 72;
730
+ var rot = '<a:xfrm rot="' + Math.round(ang * 60000) + '">';
731
+ return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="ln"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
732
+ '<p:spPr>' + rot + '<a:off x="' + E(cx - len / 2) + '" y="' + E(cy - t / 2) + '"/>' +
733
+ '<a:ext cx="' + E(len) + '" cy="' + E(t) + '"/></a:xfrm>' +
734
+ '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' + solid(hex) + '<a:ln><a:noFill/></a:ln></p:spPr>' +
735
+ '<p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>';
736
+ }
737
+ /* 散点 / 气泡(A 通道近似):只画点、不产文本——与 B 通道 XY 图表(类别为数值,过滤后为空)一致 */
738
+ function xyDots(sh, ec, S, x, y, w, h) {
739
+ var values = (ec.values || []).map(Number);
740
+ var pts = (ec.points && ec.points.length) ? ec.points : null;
741
+ var n = pts ? pts.length : Math.max(1, values.length);
742
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
743
+ var baseY = y + h, plotH = Math.max(0.3, h - 0.3);
744
+ var stepX = n > 1 ? w / (n - 1) : 0;
745
+ sh.push(shape('rect', x, baseY, w, 0.02, S.line));
746
+ for (var i = 0; i < n; i++) {
747
+ var px = pts ? x + (Number(pts[i][0]) || 0) / vmax * w : x + i * stepX;
748
+ var py = pts ? baseY - (Number(pts[i][1]) || 0) / vmax * plotH : baseY - (values[i] || 0) / vmax * plotH;
749
+ sh.push(shape('ellipse', px - 0.07, py - 0.07, 0.14, 0.14, S.accent));
750
+ }
751
+ }
752
+ /* 仪表盘(A 通道近似):270° 弧段 + 数值(B 通道 gauge 的类别为数值,过滤后两端一致,故此处不产标签文本) */
753
+ function gaugeApprox(sh, ec, S, x, y, w, h) {
754
+ var v = Number((ec.values || [0])[0]) || 0;
755
+ var max = Number(ec.max) || 100;
756
+ var frac = Math.max(0, Math.min(1, v / max));
757
+ var r = Math.min(w, h) / 2 - 0.12;
758
+ var cx = x + w / 2, cy = y + h / 2;
759
+ var steps = 40, startA = Math.PI * 0.75, sweep = Math.PI * 1.5;
760
+ var px = null, py = null;
761
+ for (var i = 0; i <= steps; i++) {
762
+ var a = startA + sweep * (i / steps);
763
+ var qx = cx + r * Math.cos(a), qy = cy + r * Math.sin(a);
764
+ if (px !== null) sh.push(lineSeg(px, py, qx, qy, (i / steps) <= frac ? S.accent : S.line, 5));
765
+ px = qx; py = qy;
766
+ }
767
+ sh.push(txSp(x, cy - 0.2, w, 0.42,
768
+ [[{ t: String(v) + (ec.unit || ''), sz: 18, b: 1, col: S.accent, font: S.fontDisplay }]], { align: 'ctr', anchor: 'ctr' }));
769
+ }
770
+ /* 饼 / 环(A 通道近似):横向占比条 + 图例(类别标签为文本,数值由 cross_verify 过滤) */
771
+ function pieApprox(sh, ec, S, x, y, w, h, dcols) {
772
+ var labels = ec.labels || [], values = (ec.values || []).map(Number);
773
+ var total = values.reduce(function (a, b) { return a + b; }, 0) || 1;
774
+ var barH = Math.max(0.32, Math.min(0.62, h * 0.22));
775
+ var barY = y + h / 2 - barH / 2 - 0.2;
776
+ var cx0 = x;
777
+ labels.forEach(function (lb, i) {
778
+ var bw = (values[i] || 0) / total * w;
779
+ sh.push(shape('rect', cx0, barY, bw, barH,
780
+ dcols ? dcols[i % dcols.length] : (i === 0 ? S.accent : S.faint)));
781
+ cx0 += bw;
782
+ });
783
+ var lx = x;
784
+ labels.forEach(function (lb, i) {
785
+ sh.push(shape('ellipse', lx, barY + barH + 0.16, 0.1, 0.1,
786
+ dcols ? dcols[i % dcols.length] : (i === 0 ? S.accent : S.faint)));
787
+ sh.push(txSp(lx + 0.14, barY + barH + 0.12, 1.2, 0.24, [[{ t: lb, sz: 10.5, col: S.body, font: S.font }]]));
788
+ sh.push(txSp(lx + 1.36, barY + barH + 0.12, 0.8, 0.24,
789
+ [[{ t: String(values[i] != null ? values[i] : '') + (ec.unit || ''), sz: 10.5, b: 1, col: S.body, font: S.font }]]));
790
+ lx += 2.3;
791
+ });
792
+ }
793
+ /* 迷你折线(A 近似):无轴无标签,仅趋势线 + 末值(与 B 的 shapeSpark 同文本集) */
794
+ function sparkApprox(sh, ec, S, x, y, w, h) {
795
+ var vals = (ec.values || []).map(Number);
796
+ var vmax = ec.max || Math.max.apply(null, vals.concat([1]));
797
+ var n = vals.length;
798
+ var stepX = n > 1 ? w / (n - 1) : 0;
799
+ var baseY = y + h - 0.06, plotH = Math.max(0.2, h - 0.18);
800
+ var px = 0, py = 0;
801
+ vals.forEach(function (v, i) {
802
+ var cx = x + i * stepX, cy = baseY - Math.max(0.02, (v / vmax) * plotH);
803
+ if (i > 0) sh.push(lineSeg(px, py, cx, cy, S.accent, 2));
804
+ px = cx; py = cy;
805
+ });
806
+ sh.push(txSp(x, y, w, 0.3, [[{ t: String(vals[n - 1] != null ? vals[n - 1] : '') + (ec.unit || ''),
807
+ sz: 11.5, b: 1, col: S.accent, font: S.font }]], { align: 'r' }));
808
+ }
809
+ /* K 线(A 近似):实体 + 须,标签 + 收盘值(与 B 的 shapeCandlestick 同文本集) */
810
+ function candleApprox(sh, ec, S, x, y, w, h) {
811
+ var labels = ec.labels || [];
812
+ var rows = (ec.values || []).filter(function (r) { return r && r.length >= 4; })
813
+ .map(function (r) { return r.slice(0, 4).map(Number); });
814
+ var n = Math.max(1, rows.length);
815
+ var flat = [];
816
+ rows.forEach(function (r) { r.forEach(function (v) { if (!isNaN(v)) flat.push(v); }); });
817
+ var lo = flat.length ? Math.min.apply(null, flat) : 0;
818
+ var hi = flat.length ? Math.max.apply(null, flat) : 1;
819
+ var span = (hi - lo) || 1;
820
+ var axisH = 0.26, plotH = Math.max(0.5, h - axisH - 0.1);
821
+ var yFor = function (v) { return y + 0.06 + (1 - (v - lo) / span) * plotH; };
822
+ var step = w / n, bw = Math.min(0.42, step * 0.5);
823
+ sh.push(shape('rect', x, y + 0.06 + plotH, w, 0.015, S.line));
824
+ rows.forEach(function (r, i) {
825
+ var o = r[0], hiP = r[1], loP = r[2], cl = r[3];
826
+ var cxp = x + step * i + step / 2;
827
+ var up = cl >= o;
828
+ sh.push(shape('rect', cxp - 0.011, yFor(hiP), 0.022, Math.max(0.02, yFor(loP) - yFor(hiP)), S.faint));
829
+ sh.push(shape('rect', cxp - bw / 2, yFor(Math.max(o, cl)), bw,
830
+ Math.max(0.04, Math.abs(yFor(cl) - yFor(o))), up ? S.accent : S.line));
831
+ if (labels[i]) {
832
+ sh.push(txSp(cxp - step / 2, y + plotH + axisH - 0.06, step, 0.24,
833
+ [[{ t: String(labels[i]), sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
834
+ }
835
+ sh.push(txSp(cxp - step / 2, yFor(hiP) - 0.26, step, 0.24,
836
+ [[{ t: String(cl) + (ec.unit || ''), sz: 11.5, b: 1, col: up ? S.accent : S.body, font: S.font }]],
837
+ { align: 'r' }));
838
+ });
839
+ }
840
+ /* 通用形状近似(与 build_pptx.js 的 shapeProportion 同文本:标签 + 数值) */
841
+ function shapeProportion(sh, ec, S, x, y, w, h, dcols) {
842
+ var labels = ec.labels || [], values = (ec.values || []).map(Number);
843
+ var n = Math.max(1, labels.length);
844
+ var labelW = Math.min(2.4, w * 0.24), valW = 0.9;
845
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
846
+ var top = values.length ? Math.max.apply(null, values) : 0;
847
+ var rowH = Math.max(0.22, Math.min(0.52, h / n));
848
+ var trackX = x + labelW + 0.08, trackW = w - labelW - valW - 0.2;
849
+ var barH = Math.max(0.12, rowH * 0.52);
850
+ labels.forEach(function (lb, i) {
851
+ var v = values[i] || 0, ry = y + i * rowH, hot = v === top;
852
+ sh.push(txSp(x, ry, labelW - 0.06, rowH, [[{ t: lb, sz: 11, col: S.body, font: S.font }]], { anchor: 'ctr' }));
853
+ sh.push(shape('rect', trackX, ry + (rowH - barH) / 2, trackW, barH, S.surface, { line: S.line }));
854
+ sh.push(shape('rect', trackX, ry + (rowH - barH) / 2, Math.max(0.02, Math.abs(v) / vmax * trackW), barH,
855
+ dcols ? dcols[i % dcols.length] : (hot ? S.accent : S.faint)));
856
+ sh.push(txSp(trackX + trackW + 0.06, ry, valW, rowH,
857
+ [[{ t: String(v) + (ec.unit || ''), sz: 11.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]],
858
+ { align: 'right', anchor: 'ctr' }));
859
+ });
860
+ }
861
+ /* 数据表行(与 build_pptx.js 的 dataTableRows 同规则) */
862
+ function dataTableRows(ec) {
863
+ var labels = (ec.labels || []).map(String);
864
+ var unit = ec.unit ? '(' + ec.unit + ')' : '';
865
+ if (ec.series && ec.series.length) {
866
+ var out = [['项目'].concat(ec.series.map(function (se, i) { return se.name || ('系列' + (i + 1)); }))];
867
+ labels.forEach(function (lb, i) {
868
+ out.push([lb].concat(ec.series.map(function (se) {
869
+ var v = (se.values || [])[i]; return v == null ? '' : String(v);
870
+ })));
871
+ });
872
+ return out;
873
+ }
874
+ var out2 = [['项目', '数值' + unit]];
875
+ labels.forEach(function (lb, i) {
876
+ var v = (ec.values || [])[i];
877
+ out2.push([lb, v == null ? '' : String(v)]);
878
+ });
879
+ return out2;
880
+ }
881
+ /* 数据表策略(与 build_pptx.js 的 dataTableMode 同规则) */
882
+ function dataTableModeOf(ec) {
883
+ var t = (ec.type || 'bar').toLowerCase();
884
+ var reg = (typeof REG !== 'undefined' && REG && REG[t]) ? REG[t] : null;
885
+ var m = ec.dataTable || (reg && reg.dataTable) || 'notes';
886
+ return m === 'appendix' ? 'inline' : m;
887
+ }
888
+ /* 图表 + 数据表组合(与 build_pptx.js 的 chartBlock 同策略:inline 在图表下方附原生表格) */
889
+ function chartBlockShapes(sh, ec, S, x, y, w, h, dcols) {
890
+ var dm = dataTableModeOf(ec);
891
+ var chH = h;
892
+ if (dm === 'inline' && (ec.labels || []).length) {
893
+ var rowsData = dataTableRows(ec);
894
+ var n = rowsData.length;
895
+ var tH = Math.min(h * 0.44, Math.max(0.44, n * 0.22 + 0.06));
896
+ chH = Math.max(0.85, h - tH - 0.1);
897
+ var colW = rowsData[0].map(function () { return w / rowsData[0].length; });
898
+ sh.push(table(x, y + chH + 0.1, w, colW, rowsData, S, { availH: tH, maxRowH: 0.24, minRowH: 0.14 }));
899
+ }
900
+ chartShapes(sh, ec, S, x, y, w, chH, dcols);
901
+ }
902
+ /* ══ 复杂信息图页型(A 通道形状近似:文本集合与 B 通道严格一致,cross_verify 逐页比对)══ */
903
+ function infoTableRows(sec) {
904
+ var t = sec.type || '';
905
+ var u = sec.unit ? '(' + sec.unit + ')' : '';
906
+ if (t === 'sankey') return [['源', '汇', '流量' + u]].concat((sec.flows || []).map(function (f) { return [String(f[0]), String(f[1]), String(f[2])]; }));
907
+ if (t === 'treemap') return [['项目', '数值' + u]].concat((sec.items || []).map(function (it) { return [String(it[0]), String(it[1])]; }));
908
+ if (t === 'boxplot') return [['分组', '最小', 'Q1', '中位', 'Q3', '最大']].concat((sec.groups || []).map(function (g) { return g.map(String); }));
909
+ if (t === 'network') return [['节点', '名称']].concat((sec.nodes || []).map(function (n) { return [String(n[0]), String(n[1])]; }));
910
+ if (t === 'marimekko') {
911
+ var head = ['列', '总量' + u].concat((sec.legend || []).map(String));
912
+ return [head].concat((sec.cols || []).map(function (c, i) {
913
+ return [String(c[0]), String(c[1])].concat(((sec.cells || [])[i] || []).map(String));
914
+ }));
915
+ }
916
+ if (t === 'streamgraph') {
917
+ return [['系列'].concat((sec.labels || []).map(String))]
918
+ .concat((sec.series || []).map(function (se) {
919
+ return [String(se.name || '')].concat((se.values || []).map(String));
920
+ }));
921
+ }
922
+ return null;
923
+ }
924
+ function infoDataTableMode(sec) {
925
+ var m = String(((sec.chart || {}).dataTable) || 'notes').toLowerCase();
926
+ return m === 'appendix' ? 'inline' : m;
927
+ }
928
+ function infoPlotA(sec, y0, y1) {
929
+ var rows = infoTableRows(sec);
930
+ var inline = (infoDataTableMode(sec) === 'inline') && rows && rows.length;
931
+ if (!inline) return { y0: y0, y1: y1, rows: rows };
932
+ var tH = Math.min((y1 - y0) * 0.4, Math.max(0.44, rows.length * 0.22 + 0.06));
933
+ return { y0: y0, y1: y1 - tH - 0.1, rows: rows, table: { y: y1 - tH, h: tH } };
934
+ }
935
+ function tmSplitA(items, x, y, w, h, out) {
936
+ if (!items.length) return;
937
+ if (items.length === 1) { out.push({ it: items[0], x: x, y: y, w: w, h: h }); return; }
938
+ var total = items.reduce(function (a, i) { return a + i.v; }, 0) || 1;
939
+ var acc = 0, k = 0;
940
+ for (; k < items.length - 1; k++) { acc += items[k].v; if (acc >= total / 2) break; }
941
+ var first = items.slice(0, k + 1), second = items.slice(k + 1);
942
+ var r = first.reduce(function (a, i) { return a + i.v; }, 0) / total;
943
+ if (w >= h) {
944
+ tmSplitA(first, x, y, w * r, h, out);
945
+ tmSplitA(second, x + w * r, y, w * (1 - r), h, out);
946
+ } else {
947
+ tmSplitA(first, x, y, w, h * r, out);
948
+ tmSplitA(second, x, y + h * r, w, h * (1 - r), out);
949
+ }
950
+ }
951
+ /* 信息图近似渲染:按页型画形状 + 落同样的标签文本 */
952
+ function infoApprox(sh, sec, S, y0, y1) {
953
+ var t = sec.type || '';
954
+ var P = infoPlotA(sec, y0, y1);
955
+ if (P.table) {
956
+ var colW = P.rows[0].map(function () { return CW / P.rows[0].length; });
957
+ sh.push(table(MX, P.table.y, CW, colW, P.rows, S, { availH: P.table.h, maxRowH: 0.24, minRowH: 0.15 }));
958
+ }
959
+ var palette = [S.accent, S.faint, S.body, S.line];
960
+ if (t === 'sankey') {
961
+ var flows = (sec.flows || []).filter(function (f) { return f && f.length >= 3; });
962
+ var nodes = [], idx = {};
963
+ flows.forEach(function (f) {
964
+ [f[0], f[1]].forEach(function (n) { if (idx[n] == null) { idx[n] = nodes.length; nodes.push(String(n)); } });
965
+ });
966
+ var level = nodes.map(function () { return 0; });
967
+ for (var it2 = 0; it2 < nodes.length + 1; it2++) {
968
+ var ch = false;
969
+ flows.forEach(function (f) {
970
+ var a = idx[f[0]], b = idx[f[1]];
971
+ if (level[b] < level[a] + 1) { level[b] = level[a] + 1; ch = true; }
972
+ });
973
+ if (!ch) break;
974
+ }
975
+ var maxLv = Math.max.apply(null, level.concat([0]));
976
+ var colW2 = CW / (maxLv + 1);
977
+ var geo = {};
978
+ for (var l = 0; l <= maxLv; l++) {
979
+ var lv = nodes.filter(function (n, i) { return level[i] === l; });
980
+ var rowH2 = Math.max(0.24, (P.y1 - P.y0) / Math.max(1, lv.length));
981
+ lv.forEach(function (n, k2) { geo[n] = { x: MX + l * colW2 + colW2 * 0.5 - 0.16, y: P.y0 + k2 * rowH2, h: rowH2 * 0.7 }; });
982
+ }
983
+ flows.forEach(function (f) {
984
+ var a = geo[f[0]], b = geo[f[1]];
985
+ if (!a || !b) return;
986
+ sh.push(lineSeg(a.x + 0.32, a.y + a.h / 2, b.x, b.y + b.h / 2, S.soft, 8));
987
+ /* 流量标签:与 B 通道同文本(数值 + 单位,由 cross_verify 过滤) */
988
+ sh.push(txSp((a.x + b.x) / 2 - 0.5, (a.y + b.y) / 2 - 0.14, 1.0, 0.28,
989
+ [[{ t: String(f[2]) + (sec.unit || ''), sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
990
+ });
991
+ nodes.forEach(function (n) {
992
+ var g = geo[n];
993
+ sh.push(shape('rect', g.x, g.y, 0.32, g.h, S.accent));
994
+ sh.push(txSp(g.x + 0.38, g.y, Math.min(1.4, colW2 * 0.5), g.h,
995
+ [[{ t: n, sz: 10.5, b: 1, col: S.body, font: S.font }]], { anchor: 'ctr' }));
996
+ });
997
+ return;
998
+ }
999
+ if (t === 'treemap') {
1000
+ var items = (sec.items || []).map(function (it) { return { label: String(it[0]), v: Math.max(0, Number(it[1]) || 0) }; })
1001
+ .filter(function (i) { return i.v > 0; }).sort(function (a, b) { return b.v - a.v; });
1002
+ var out = [];
1003
+ tmSplitA(items, MX, P.y0, CW, P.y1 - P.y0, out);
1004
+ out.forEach(function (o) {
1005
+ sh.push(shape('rect', o.x + 0.03, o.y + 0.03, Math.max(0.05, o.w - 0.06), Math.max(0.05, o.h - 0.06), S.surface, { line: S.line }));
1006
+ sh.push(txSp(o.x + 0.1, o.y + 0.06, Math.max(0.2, o.w - 0.2), 0.28, [[{ t: o.it.label, sz: 10.5, b: 1, col: S.ink, font: S.font }]]));
1007
+ sh.push(txSp(o.x + 0.1, o.y + 0.32, Math.max(0.2, o.w - 0.2), 0.26,
1008
+ [[{ t: String(o.it.v) + (sec.unit || ''), sz: 10, col: S.body, font: S.font }]]));
1009
+ });
1010
+ return;
1011
+ }
1012
+ if (t === 'boxplot') {
1013
+ var groups = (sec.groups || []).filter(function (g) { return g && g.length >= 6; });
1014
+ var step = CW / Math.max(1, groups.length);
1015
+ var plotH = (P.y1 - P.y0) - 0.4;
1016
+ groups.forEach(function (g, i) {
1017
+ var cx = MX + step * i + step / 2;
1018
+ var bw = Math.min(0.9, step * 0.5);
1019
+ sh.push(shape('rect', cx - 0.012, P.y0, 0.024, plotH, S.faint));
1020
+ sh.push(shape('rect', cx - bw / 2, P.y0 + plotH * 0.2, bw, plotH * 0.6, S.soft, { line: S.accent }));
1021
+ sh.push(shape('rect', cx - bw / 2, P.y0 + plotH * 0.48, bw, 0.03, S.accent));
1022
+ sh.push(txSp(cx - step / 2, P.y1 - 0.34, step, 0.28,
1023
+ [[{ t: String(g[0]), sz: 10.5, col: S.body, font: S.font }]], { align: 'ctr' }));
1024
+ });
1025
+ return;
1026
+ }
1027
+ if (t === 'network') {
1028
+ var nds = sec.nodes || [];
1029
+ var n2 = Math.max(1, nds.length);
1030
+ var cx0 = MX + CW / 2, cy0 = (P.y0 + P.y1) / 2;
1031
+ var r = Math.min(CW / 2, (P.y1 - P.y0) / 2) - 1.0;
1032
+ var pos = {};
1033
+ nds.forEach(function (nd, i) {
1034
+ var a = -Math.PI / 2 + i * 2 * Math.PI / n2;
1035
+ pos[String(nd[0])] = { x: cx0 + r * Math.cos(a), y: cy0 + r * Math.sin(a) };
1036
+ });
1037
+ (sec.edges || []).forEach(function (e) {
1038
+ var a = pos[String(e[0])], b = pos[String(e[1])];
1039
+ if (!a || !b) return;
1040
+ sh.push(lineSeg(a.x, a.y, b.x, b.y, S.line, 1.5));
1041
+ });
1042
+ nds.forEach(function (nd) {
1043
+ var p = pos[String(nd[0])];
1044
+ sh.push(shape('ellipse', p.x - 0.26, p.y - 0.26, 0.52, 0.52, S.accent));
1045
+ var outward = p.x >= cx0;
1046
+ sh.push(txSp(outward ? p.x + 0.32 : p.x - 0.32 - 1.4, p.y - 0.14, 1.4, 0.28,
1047
+ [[{ t: String(nd[1] != null ? nd[1] : nd[0]), sz: 10, col: S.body, font: S.font }]],
1048
+ { align: outward ? 'l' : 'r', anchor: 'ctr' }));
1049
+ });
1050
+ return;
1051
+ }
1052
+ if (t === 'marimekko') {
1053
+ var cols = sec.cols || [];
1054
+ var legend = (sec.legend || []).map(String);
1055
+ var total = cols.reduce(function (a, c) { return a + (Number(c[1]) || 0); }, 0) || 1;
1056
+ var plotH2 = (P.y1 - P.y0) - 0.6;
1057
+ var availW = CW - 0.06 * Math.max(0, cols.length - 1);
1058
+ var cx1 = MX;
1059
+ cols.forEach(function (c, i) {
1060
+ var cw = Math.max(0.2, (Number(c[1]) || 0) / total * availW);
1061
+ var shares = ((sec.cells || [])[i] || []).map(Number);
1062
+ var sum = shares.reduce(function (a, b) { return a + b; }, 0) || 1;
1063
+ var cy = P.y0;
1064
+ shares.forEach(function (v, k) {
1065
+ var shh = Math.max(0.02, v / sum * plotH2);
1066
+ sh.push(shape('rect', cx1, cy, cw, shh, palette[k % palette.length]));
1067
+ cy += shh;
1068
+ });
1069
+ sh.push(txSp(cx1, P.y0 + plotH2 + 0.06, cw, 0.28,
1070
+ [[{ t: String(c[0]), sz: 10.5, b: 1, col: S.body, font: S.font }]], { align: 'ctr' }));
1071
+ cx1 += cw + 0.06;
1072
+ });
1073
+ var lx = MX;
1074
+ legend.forEach(function (lg, k) {
1075
+ sh.push(shape('rect', lx, P.y1 - 0.2, 0.12, 0.12, palette[k % palette.length]));
1076
+ sh.push(txSp(lx + 0.16, P.y1 - 0.26, 1.5, 0.26, [[{ t: lg, sz: 10, col: S.body, font: S.font }]], { anchor: 'ctr' }));
1077
+ lx += 1.75;
1078
+ });
1079
+ return;
1080
+ }
1081
+ if (t === 'streamgraph') {
1082
+ var series = sec.series || [];
1083
+ var labels = sec.labels || [];
1084
+ var nPts = labels.length || ((series[0] && series[0].values || []).length);
1085
+ if (nPts >= 2) {
1086
+ var values = series.map(function (se) {
1087
+ return (se.values || []).map(function (v) { return Math.max(0, Number(v) || 0); });
1088
+ });
1089
+ var totals = [];
1090
+ for (var i2 = 0; i2 < nPts; i2++) {
1091
+ var tt = 0;
1092
+ values.forEach(function (v) { tt += (v[i2] || 0); });
1093
+ totals.push(tt);
1094
+ }
1095
+ var maxTot = Math.max.apply(null, totals.concat([1]));
1096
+ var plotH3 = P.y1 - P.y0;
1097
+ var cyMid = (P.y0 + P.y1) / 2;
1098
+ var base = totals.map(function (tv) { return cyMid - tv / maxTot * plotH3 / 2; });
1099
+ series.forEach(function (se, si) {
1100
+ var upper = base.map(function (bv, i3) { return bv + (values[si][i3] || 0) / maxTot * plotH3 / 2; });
1101
+ var stepX = CW / (nPts - 1);
1102
+ for (var i4 = 0; i4 < nPts - 1; i4++) {
1103
+ var hgt = Math.max(0.04, ((upper[i4] - base[i4]) + (upper[i4 + 1] - base[i4 + 1])) / 2);
1104
+ sh.push(shape('rect', MX + i4 * stepX, (base[i4] + base[i4 + 1]) / 2, stepX, hgt, palette[si % palette.length]));
1105
+ }
1106
+ base = upper;
1107
+ sh.push(shape('rect', MX, P.y1 + 0.06 + si * 0.26, 0.12, 0.12, palette[si % palette.length]));
1108
+ sh.push(txSp(MX + 0.16, P.y1 + si * 0.26, 1.5, 0.26,
1109
+ [[{ t: String(se.name || ('系列' + (si + 1))), sz: 10, col: S.body, font: S.font }]], { anchor: 'ctr' }));
1110
+ });
1111
+ labels.forEach(function (lb, i5) {
1112
+ sh.push(txSp(MX + (nPts > 1 ? i5 / (nPts - 1) : 0.5) * CW - 0.4, P.y1 - 0.02, 0.8, 0.24,
1113
+ [[{ t: String(lb), sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
1114
+ });
1115
+ }
1116
+ return;
1117
+ }
1118
+ }
1119
+ /* 统一图表分发(与 scripts/layout-constants.json 的 charts.registry 同分派规则)
1120
+ * A 通道为形状近似:类别标签与数值都落为文本,数值 token 由 cross_verify 过滤;
1121
+ * 复杂信息图(sankey / treemap / boxplot / network / marimekko / streamgraph)请用专属页型承载。 */
1122
+ function chartShapes(sh, ec, S, x, y, w, h, dcols) {
1123
+ var t = (ec.type || 'bar').toLowerCase();
1124
+ var reg = (typeof REG !== 'undefined' && REG && REG[t]) ? REG[t] : null;
1125
+ if (t === 'hbar') { hbarRows(sh, ec, S, y, y + h, dcols, x, w); return; }
1126
+ if (t === 'line' || t === 'dualline' || t === 'area') { lineShapes(sh, ec, S, x, y, w, h, dcols); return; }
1127
+ if (t === 'scatter' || t === 'bubble') { xyDots(sh, ec, S, x, y, w, h); return; }
1128
+ if (t === 'gauge') { gaugeApprox(sh, ec, S, x, y, w, h); return; }
1129
+ if (t === 'pie' || t === 'donut' || t === 'multidonut') { pieApprox(sh, ec, S, x, y, w, h, dcols); return; }
1130
+ /* sparkline / candlestick 的文本集须与 B 通道专属渲染器一致(否则 cross_verify 误报) */
1131
+ if (t === 'sparkline') { sparkApprox(sh, ec, S, x, y, w, h); return; }
1132
+ if (t === 'candlestick') { candleApprox(sh, ec, S, x, y, w, h); return; }
1133
+ if (reg && reg.pptx === 'shape') { shapeProportion(sh, ec, S, x, y, w, h, dcols); return; }
1134
+ vbarShapes(sh, ec, S, x, y, w, h, dcols);
1135
+ }
1136
+
1137
+ /* 区域 IR 取用(与 scripts/lib_layout_regions.js 同源;未注入时回落 null) */
1138
+ function regOf(type, slot, opts) {
1139
+ return (typeof regionOf === 'function') ? regionOf(type, slot, opts) : null;
1140
+ }
1141
+
1142
+ /* ── 幻灯片生成(版式常量同 build_pptx.js · 同一 layout-constants.json) ── */
1143
+ function slidesOf(model, S) {
1144
+ var slides = [];
1145
+ _mode = model.mode || 'presentation';
1146
+ function head(sh, eyebrow, title, lead) {
1147
+ var H = (typeof regionOf === 'function') ? regionOf('head', 'head') : {
1148
+ x: MX, y: PT.common.headEyebrowY, w: CW,
1149
+ titleY: PT.common.headTitleY, ruleY: PT.common.headRuleY, leadY: PT.common.leadY,
1150
+ };
1151
+ if (eyebrow) sh.push(txSp(H.x, H.y, H.w, 0.4, [[{ t: eyebrow, sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1152
+ var titleH = eyebrow ? (H.ruleY - H.titleY - 0.06) : 1.0;
1153
+ var tSize = estTextH(title, CW, 30, 1.25) > titleH ? 24 : 30;
1154
+ sh.push(txSp(H.x, eyebrow ? H.titleY : 0.6, H.w, Math.max(0.6, titleH), [[{ t: title, sz: tSize, b: 1, col: S.ink, font: S.fontDisplay }]]));
1155
+ sh.push(shape('rect', H.x, eyebrow ? H.ruleY : 1.6, 0.9, 0.045, S.accent));
1156
+ if (lead) sh.push(txSp(H.x, H.leadY || PT.common.leadY, H.w, 0.5, [[{ t: lead, sz: 14, col: S.body, font: S.font }]]));
1157
+ }
1158
+
1159
+ /* 封面 */
1160
+ var CV = PT.cover;
1161
+ var c = [shape('rect', 0, 0, PW, PH, S.accent)];
1162
+ c.push(txSp(MX, CV.metaY, CW, 0.4, [[{ t: model.meta || '', sz: 12, col: S.soft, font: S.font }]]));
1163
+ var cTSize = estTextH(model.title, CW - 1, 44, 1.25) > CV.titleH ? 36 : 44;
1164
+ c.push(txSp(MX, CV.titleY, CW - 1, CV.titleH, [[{ t: model.title, sz: cTSize, b: 1, col: S.onAccent, font: S.fontDisplay }]]));
1165
+ c.push(txSp(MX, CV.subtitleY, CW - 1, CV.subtitleH, [[{ t: model.subtitle || '', sz: 18, col: S.soft, font: S.font }]]));
1166
+ slides.push(c);
1167
+
1168
+ /* Agenda(>8 条自动双列,行高自适应防溢出;architecture 极简形态可省略 agenda) */
1169
+ var items = model.agenda || [];
1170
+ if (items.length) {
1171
+ var ag = [];
1172
+ head(ag, 'AGENDA', '报告大纲');
1173
+ var colN = items.length > 8 ? 2 : 1;
1174
+ var perCol = Math.ceil(items.length / colN) || 1;
1175
+ var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
1176
+ var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
1177
+ var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
1178
+ items.forEach(function (it, i) {
1179
+ var col = Math.floor(i / perCol), row = i % perCol;
1180
+ var x = MX + col * (CW / colN), y = CONTENT_TOP + row * rowH;
1181
+ ag.push(txSp(x, y, 0.9, Math.min(0.9, rowH), [[{ t: it[0], sz: numSz, b: 1, col: S.accent, font: S.font }]]));
1182
+ ag.push(txSp(x + 1.0, y + 0.03, CW / colN - 1.1, Math.min(0.95, rowH),
1183
+ [[{ t: it[1], sz: tSz, b: 1, col: S.ink, font: S.font }],
1184
+ [{ t: it[2] || '', sz: dSz, col: S.body, font: S.font }]], { lineSpacing: tSz + 5 }));
1185
+ });
1186
+ slides.push(ag);
1187
+ }
1188
+
1189
+ /* 章节页 */
1190
+ (model.sections || []).forEach(function (sec) {
1191
+ var type = sec.type || 'points';
1192
+ var sh = [];
1193
+ if (type !== 'quote') head(sh, sec.eyebrow, sec.title, sec.lead);
1194
+ /* 非 exhibit 页型也可带 Exhibit 编号(帧头徽标;正文区下移让位,与 B 通道同规则) */
1195
+ var exBadge = (type !== 'exhibit' && sec.exhibitNo) ? String(sec.exhibitNo).toUpperCase() : null;
1196
+ var bodyY0 = sec.lead ? PT.common.bodyYWithLead : CONTENT_TOP;
1197
+ var bodyY = exBadge ? bodyY0 + 0.42 : bodyY0;
1198
+ if (exBadge) {
1199
+ sh.push(txSp(MX, CONTENT_TOP - 0.06, CW, 0.3,
1200
+ [[{ t: exBadge, sz: 11.5, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1201
+ }
1202
+ var flagItems = Array.isArray(sec.flags) ? sec.flags.filter(Boolean) : [];
1203
+ var _F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
1204
+ var flagH = flagItems.length
1205
+ ? (_F.hdH + Math.min(_F.maxRows, flagItems.length) * _F.rowH + 0.06) : 0;
1206
+ var bodyBottom = chartBottom(!!sec.soWhat, !!sec.footnote);
1207
+ var flagY = 0;
1208
+ if (flagH) {
1209
+ flagY = CONTENT_BOTTOM - flagH;
1210
+ if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
1211
+ if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
1212
+ bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
1213
+ }
1214
+
1215
+ if (type === 'metrics') {
1216
+ var M = PT.metrics, ms = sec.metrics || [], n = Math.max(1, ms.length);
1217
+ var mReg = regOf('metrics', 'primary') || { x: MX, y: M.bandY, w: CW, h: M.cardH };
1218
+ var g = cols(mReg.w, n, M.gap);
1219
+ var mw = Math.min(M.maxW, g.w);
1220
+ var gx = mReg.x + (mReg.w - n * mw - (n - 1) * M.gap) / 2;
1221
+ var cardH = Math.min(mReg.h, CONTENT_BOTTOM - mReg.y);
1222
+ ms.forEach(function (m, i) {
1223
+ var x = gx + i * (mw + M.gap);
1224
+ sh.push(shape('roundRect', x, mReg.y, mw, cardH, S.surface, { line: S.line, adj: 4000 }));
1225
+ var vSize = estTextH(m[0], mw - 0.2, 40, 1.1) > cardH * 0.5 ? 30 : 40;
1226
+ sh.push(txSp(x, M.valY, mw, M.valH, [[{ t: m[0], sz: vSize, b: 1, col: S.accent, font: S.fontDisplay }]], { align: 'ctr' }));
1227
+ sh.push(txSp(x + 0.15, M.capY, mw - 0.3, M.capH, [[{ t: m[1], sz: 11.5, col: S.body, font: S.font }]], { align: 'ctr' }));
1228
+ });
1229
+ } else if (type === 'kpi') {
1230
+ var K = PT.kpi, hro = sec.hero || [];
1231
+ var hReg = regOf('kpi', 'hero') || { x: MX, y: K.heroY, w: CW * K.heroW, h: K.heroH };
1232
+ sh.push(txSp(hReg.x, hReg.y, hReg.w, hReg.h, [[{ t: String(hro[0] || ''), sz: 64, b: 1, col: S.accent, font: S.fontDisplay }]]));
1233
+ sh.push(txSp(hReg.x + 0.05, hReg.y + 1.5, hReg.w - 0.4, 0.55, [[{ t: String(hro[1] || ''), sz: 15, b: 1, col: S.ink, font: S.font }]]));
1234
+ if (hro[2]) sh.push(txSp(hReg.x + 0.05, hReg.y + 2.1, hReg.w - 0.4, 0.4, [[{ t: '▲ ' + hro[2], sz: 12, b: 1, col: S.accent, font: S.font }]]));
1235
+ var kdivX = MX + CW * K.dividerX;
1236
+ sh.push(shape('rect', kdivX, K.heroY + 0.15, 0.025, 3.1, S.line));
1237
+ var mRegK = regOf('kpi', 'metrics') || { x: kdivX + 0.35, y: K.metricY0, w: CW * (1 - K.dividerX) - 0.5 };
1238
+ var kmets = sec.metrics || [];
1239
+ var kCap = Math.max(1, Math.floor((CONTENT_BOTTOM - mRegK.y) / K.metricRowH));
1240
+ var kRows = Math.min(kmets.length, kCap);
1241
+ var kRowH = fitRowH(CONTENT_BOTTOM - mRegK.y, kRows, K.metricRowH, 0.42);
1242
+ var kTopH = Math.min(0.55, kRowH * 0.62);
1243
+ kmets.slice(0, kRows).forEach(function (m, i) {
1244
+ var ky = mRegK.y + i * kRowH;
1245
+ sh.push(txSp(mRegK.x, ky, mRegK.w, kTopH, [[{ t: m[0], sz: 22, b: 1, col: S.accent, font: S.fontDisplay }]]));
1246
+ sh.push(txSp(mRegK.x, ky + kTopH, mRegK.w, Math.max(0.24, kRowH - kTopH), [[{ t: m[1], sz: 10.5, col: S.faint, font: S.font }]]));
1247
+ });
1248
+ } else if (type === 'comparison') {
1249
+ var C2 = PT.comparison;
1250
+ var lR = regOf('comparison', 'left') || { x: MX, y: C2.panelY, w: (CW - C2.panelGap) / 2, h: C2.panelH };
1251
+ var rR = regOf('comparison', 'right') || { x: MX + (CW + C2.panelGap) / 2, y: C2.panelY, w: (CW - C2.panelGap) / 2, h: C2.panelH };
1252
+ var cw2 = lR.w, cpy = lR.y, cph = lR.h, rx2 = rR.x;
1253
+ sh.push(shape('roundRect', lR.x, cpy, cw2, cph, S.surface, { adj: 4000, line: S.line }));
1254
+ sh.push(shape('roundRect', rx2, cpy, cw2, cph, S.soft, { adj: 4000 }));
1255
+ [{ x: lR.x, d: (sec.left || {}) }, { x: rx2, d: (sec.right || {}) }].forEach(function (sd) {
1256
+ var isRight = sd.x === rx2;
1257
+ var pts = sd.d.points || [];
1258
+ var avail = cph - C2.titleH - 0.34;
1259
+ var rowH2 = fitRowH(avail, pts.length, C2.rowH, 0.34);
1260
+ var fz = rowH2 < 0.5 ? 11.5 : 13;
1261
+ sh.push(txSp(sd.x + 0.22, cpy + 0.18, cw2 - 0.44, C2.titleH,
1262
+ [[{ t: sd.d.title || '', sz: 15, b: 1, col: isRight ? S.accent : S.ink, font: S.font }]]));
1263
+ pts.forEach(function (p, pi) {
1264
+ var py = cpy + 0.18 + C2.titleH + 0.16 + pi * rowH2;
1265
+ sh.push(shape('rect', sd.x + 0.24, py + rowH2 / 2 - 0.06, 0.12, 0.12, isRight ? S.accent : S.faint));
1266
+ sh.push(txSp(sd.x + 0.5, py, cw2 - 0.75, rowH2 - 0.04,
1267
+ [[{ t: p[0] + ' ', sz: fz, b: 1, col: S.ink, font: S.font },
1268
+ { t: p[1], sz: fz - 1, col: S.body, font: S.font }]]));
1269
+ });
1270
+ });
1271
+ if (sec.verdict) {
1272
+ var vR = regOf('exhibit', 'annotation') || { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
1273
+ sh.push(shape('rect', vR.x, vR.y, vR.w, vR.h, S.accent));
1274
+ sh.push(txSp(vR.x + 0.22, vR.y + 0.06, vR.w - 0.44, vR.h - 0.12,
1275
+ [[{ t: '结论 ', sz: 11, b: 1, col: S.onAccent, font: S.font, spc: 150 },
1276
+ { t: sec.verdict, sz: 12.5, b: 1, col: S.onAccent, font: S.font }]], { anchor: 'ctr' }));
1277
+ }
1278
+ } else if (type === 'quote') {
1279
+ var Q = PT.quote;
1280
+ var qR = regOf('quote', 'primary') || { x: MX + 1.05, y: Q.textY, w: CW - 2.1, h: Q.textH };
1281
+ /* 金句页用主题一致强调带(accent-soft 底)——浅色主题下不再出现深色页 */
1282
+ sh.push(shape('rect', 0, 0, PW, PH, S.soft));
1283
+ if (sec.eyebrow) sh.push(txSp(MX, PT.common.headEyebrowY, CW, 0.4, [[{ t: sec.eyebrow, sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1284
+ sh.push(txSp(MX, PT.common.headTitleY, CW, 1.0, [[{ t: sec.title, sz: 30, b: 1, col: S.ink, font: S.fontDisplay }]]));
1285
+ sh.push(shape('rect', MX, PT.common.headRuleY, 0.9, 0.045, S.accent));
1286
+ sh.push(txSp(MX + 0.05, Q.markY, 1.2, 1.1, [[{ t: '「', sz: 60, b: 1, col: S.accent, font: S.fontDisplay }]]));
1287
+ var qSize = estTextH(sec.quote, qR.w, 20, 1.4) > qR.h ? 16 : 20;
1288
+ sh.push(txSp(qR.x, qR.y, qR.w, qR.h, [[{ t: sec.quote, sz: qSize, b: 1, col: S.ink, font: S.fontDisplay }]], { anchor: 'ctr' }));
1289
+ sh.push(txSp(qR.x, Q.authorY, qR.w, 0.45, [[{ t: '—— ' + sec.author, sz: 13, b: 1, col: S.accent, font: S.font }]], { align: 'r' }));
1290
+ if (sec.context) sh.push(txSp(qR.x, Q.contextY, qR.w, 0.4, [[{ t: sec.context, sz: 11, col: S.faint, font: S.font }]], { align: 'r' }));
1291
+ } else if (type === 'image') {
1292
+ /* 素材图片页:full 版心全宽 / half 左图右注 / bleed 通栏出血 / grid 多图网格 /
1293
+ compare 双图对比 / wall Logo 墙;无图(image.placeholder)走原生占位块 + 标签文本 */
1294
+ var IM = PT.image, img = sec.image || {};
1295
+ var items = Array.isArray(img.items) ? img.items.filter(Boolean) : [];
1296
+ var layout = String(img.layout || (items.length > 1 ? 'grid' : 'full')).toLowerCase();
1297
+ var fit = String(img.fit || ((typeof IMG !== 'undefined' && IMG && IMG.fitDefault) || 'cover')).toLowerCase();
1298
+ var isPh = !!img.placeholder;
1299
+ var iR = regOf('image', 'primary', { top: bodyY, caption: !!img.caption }) ||
1300
+ { x: MX, y: Math.max(IM.y, bodyY), w: CW, h: IM.h };
1301
+ var iy = iR.y;
1302
+ var capH2 = img.caption ? 0.32 : 0;
1303
+ var ih = Math.max(1.2, iR.h);
1304
+ imageLayoutShapes(sh, S, layout, img, items, sec.points || [], isPh, fit, iy, iy + ih);
1305
+ if (img.caption) sh.push(txSp(MX, iy + ih + 0.04, CW, capH2, [[{ t: img.caption, sz: 10.5, col: S.faint, font: S.font }]]));
1306
+ } else if (type === 'donut') {
1307
+ var dcn = sec.chart || {};
1308
+ if (dcn.labels && dcn.values) donutChart(sh, dcn, S, dcn.colors || donutColors(model.style, S, model.theme));
1309
+ if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
1310
+ } else if (type === 'heatmap') {
1311
+ var H = PT.heatmap;
1312
+ var hR = regOf('heatmap', 'primary', { bottom: bodyBottom }) || { x: MX, y: H.y, w: CW, h: bodyBottom - H.y };
1313
+ var rowsH = sec.rowHeads || [], colsH = sec.colHeads || [], cells = sec.cells || [];
1314
+ var nR = Math.max(1, rowsH.length), nC = Math.max(1, colsH.length);
1315
+ var availH = hR.h;
1316
+ var rowH3 = fitRowH(availH - H.headH, nR, H.cellH, 0.24);
1317
+ var gridW = hR.w - H.labelW;
1318
+ var cw3 = (gridW - (nC - 1) * H.cellGap) / nC;
1319
+ var flat = [], k2;
1320
+ for (k2 = 0; k2 < cells.length; k2++) for (var j2 = 0; j2 < (cells[k2] || []).length; j2++) { var nv = Number(cells[k2][j2]); if (!isNaN(nv)) flat.push(nv); }
1321
+ var vmax3 = flat.length ? Math.max.apply(null, flat) : 1;
1322
+ var vmin3 = flat.length ? Math.min.apply(null, flat) : 0;
1323
+ var levels = [S.surface, S.soft, S.line, S.accent];
1324
+ colsH.forEach(function (ch2, ci) {
1325
+ sh.push(txSp(hR.x + H.labelW + ci * (cw3 + H.cellGap), hR.y, cw3, H.headH,
1326
+ [[{ t: ch2, sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1327
+ });
1328
+ var rowsUse = Math.min(nR, cells.length);
1329
+ for (var ri = 0; ri < rowsUse; ri++) {
1330
+ var yy = hR.y + H.headH + 0.08 + ri * (rowH3 + H.cellGap);
1331
+ sh.push(txSp(hR.x, yy, H.labelW - 0.15, rowH3, [[{ t: rowsH[ri] || '', sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
1332
+ for (var ci2 = 0; ci2 < nC; ci2++) {
1333
+ var v2 = Number((cells[ri] || [])[ci2]);
1334
+ var has2 = !isNaN(v2);
1335
+ var p2 = (has2 && vmax3 > vmin3) ? (v2 - vmin3) / (vmax3 - vmin3) : 0.5;
1336
+ var lvl = p2 >= 0.86 ? 3 : (p2 >= 0.55 ? 2 : (p2 >= 0.25 ? 1 : 0));
1337
+ var cx2 = hR.x + H.labelW + ci2 * (cw3 + H.cellGap);
1338
+ sh.push(shape('roundRect', cx2, yy, cw3, rowH3, levels[lvl],
1339
+ { adj: 3000, line: lvl === 0 ? S.line : undefined }));
1340
+ sh.push(txSp(cx2, yy, cw3, rowH3,
1341
+ [[{ t: has2 ? valText(v2, sec.unit) : '—', sz: 11.5, b: lvl >= 2 ? 1 : 0,
1342
+ col: lvl >= 3 ? S.onAccent : S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1343
+ }
1344
+ }
1345
+ var lg = sec.scaleLabel || ['低', '高'];
1346
+ var lgy = hR.y + H.headH + 0.08 + rowsUse * (rowH3 + H.cellGap) + 0.06;
1347
+ sh.push(txSp(hR.x, lgy, 0.6, 0.3, [[{ t: lg[0] || '低', sz: 9.5, col: S.faint, font: S.font }]]));
1348
+ for (var li2 = 0; li2 < 4; li2++) {
1349
+ sh.push(shape('rect', hR.x + 0.45 + li2 * 0.3, lgy + 0.04, 0.28, 0.16, levels[li2],
1350
+ { line: li2 === 0 ? S.line : undefined }));
1351
+ }
1352
+ sh.push(txSp(hR.x + 1.75, lgy, 0.6, 0.3, [[{ t: lg[1] || '高', sz: 9.5, col: S.faint, font: S.font }]]));
1353
+ } else if (type === 'bullet') {
1354
+ var B = PT.bullet, its = sec.items || [], nB = Math.max(1, its.length);
1355
+ var bR = regOf('bullet', 'primary', { bottom: bodyBottom }) || { x: MX, y: B.y, w: CW, h: bodyBottom - B.y };
1356
+ var availB = bR.h;
1357
+ var rowHB = fitRowH(availB, nB, B.rowH, 0.26);
1358
+ var trackX = bR.x + B.labelW + 0.15;
1359
+ var trackW = bR.w - B.labelW - B.valW - 0.3;
1360
+ var nums = [], q2;
1361
+ for (q2 = 0; q2 < its.length; q2++) { var nq = Number(its[q2][1]); if (!isNaN(nq)) nums.push(nq); }
1362
+ var vmaxB = sec.max || (nums.length ? Math.max.apply(null, nums) : 1);
1363
+ its.forEach(function (it, i) {
1364
+ var yy2 = bR.y + i * rowHB + rowHB / 2 - B.barH / 2;
1365
+ var v = Number(it[1]) || 0, tgt = Number(it[2]);
1366
+ var hit = !isNaN(tgt) && v >= tgt;
1367
+ sh.push(txSp(bR.x, bR.y + i * rowHB, B.labelW, rowHB, [[{ t: String(it[0]), sz: 12, b: 1, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
1368
+ sh.push(shape('roundRect', trackX, yy2, trackW, B.barH, S.surface, { adj: 50000, line: S.line }));
1369
+ var fw = Math.max(0.02, Math.min(1, v / (vmaxB || 1)) * trackW);
1370
+ sh.push(shape('roundRect', trackX, yy2, fw, B.barH, hit ? S.accent : S.faint, { adj: 50000 }));
1371
+ if (!isNaN(tgt)) {
1372
+ var tx2 = trackX + Math.min(1, tgt / (vmaxB || 1)) * trackW;
1373
+ sh.push(shape('rect', tx2 - 0.01, yy2 - 0.07, 0.02, B.barH + 0.14, S.body));
1374
+ }
1375
+ sh.push(txSp(trackX + trackW + 0.12, bR.y + i * rowHB, B.valW, rowHB,
1376
+ [[{ t: String(it[1]) + (sec.unit || ''), sz: 12, b: 1, col: hit ? S.accent : S.body, font: S.font },
1377
+ { t: isNaN(tgt) ? '' : ' / ' + tgt + (sec.unit || ''), sz: 10, col: S.faint, font: S.font }]],
1378
+ { align: 'r', anchor: 'ctr' }));
1379
+ });
1380
+ } else if (type === 'pyramid') {
1381
+ var P = PT.pyramid, lv = sec.levels || [], nP = Math.max(1, lv.length);
1382
+ var pR = regOf('pyramid', 'primary', { bottom: bodyBottom }) || { x: MX, y: P.y, w: CW, h: bodyBottom - P.y };
1383
+ var availP = pR.h;
1384
+ var rowHP = fitRowH(availP - (nP - 1) * P.gap, nP, P.rowH, 0.26);
1385
+ lv.forEach(function (l, i) {
1386
+ var frac = P.minW + (1 - P.minW) * (nP === 1 ? 1 : i / (nP - 1));
1387
+ var w2 = pR.w * frac, x2 = pR.x + (pR.w - w2) / 2, y2 = pR.y + i * (rowHP + P.gap);
1388
+ var acc = isRec(l) && l.accent;
1389
+ var lt = isRec(l) ? (l.t || '') : String(l[0] || '');
1390
+ var ld = isRec(l) ? (l.d || '') : String(l[1] || '');
1391
+ sh.push(shape('roundRect', x2, y2, w2, rowHP, acc ? S.soft : S.surface, { adj: 5000, line: acc ? undefined : S.line }));
1392
+ var lwP = Math.min(P.labelW, w2 - 0.44);
1393
+ sh.push(txSp(x2 + 0.22, y2, lwP, rowHP, [[{ t: lt, sz: 13.5, b: 1, col: acc ? S.accent : S.ink, font: S.font }]], { anchor: 'ctr' }));
1394
+ if (ld) sh.push(txSp(x2 + 0.22 + lwP, y2, w2 - 0.44 - lwP, rowHP, [[{ t: ld, sz: 11, col: S.faint, font: S.font }]], { anchor: 'ctr' }));
1395
+ });
1396
+ } else if (type === 'steps') {
1397
+ var S2 = PT.steps, st = sec.steps || [], maxPer = S2.maxPerRow || 6, groups = sec.groups || [];
1398
+ var sR = regOf('steps', 'primary') || { x: MX, y: S2.y, w: CW, h: S2.barH };
1399
+ var chunks = [];
1400
+ if (groups.length) {
1401
+ var kk = 0;
1402
+ groups.forEach(function (gp) { var cnt = Number(gp[1]) || 0; chunks.push({ label: gp[0], items: st.slice(kk, kk + cnt) }); kk += cnt; });
1403
+ if (kk < st.length) chunks.push({ label: '', items: st.slice(kk) });
1404
+ } else {
1405
+ for (var i3 = 0; i3 < st.length; i3 += maxPer) chunks.push({ label: '', items: st.slice(i3, i3 + maxPer) });
1406
+ }
1407
+ var rowsN = Math.max(1, chunks.length);
1408
+ var availS = bodyBottom - sR.y;
1409
+ var rowHS = fitRowH(availS - (rowsN - 1) * 0.3, rowsN, sR.h || S2.barH, 0.3);
1410
+ var cyS = sR.y;
1411
+ chunks.forEach(function (ck) {
1412
+ if (ck.label) {
1413
+ sh.push(txSp(sR.x, cyS - 0.02, sR.w, 0.26, [[{ t: ck.label, sz: 10, b: 1, col: S.faint, font: S.font, spc: 100 }]]));
1414
+ cyS += 0.28;
1415
+ }
1416
+ var n4 = Math.max(1, ck.items.length);
1417
+ var g4 = cols(sR.w, n4, S2.gap + S2.arrowW);
1418
+ ck.items.forEach(function (it, i) {
1419
+ var acc = isRec(it) && it.accent;
1420
+ var t4 = isRec(it) ? (it.t || '') : String(it[0] || '');
1421
+ var d4 = isRec(it) ? (it.d || '') : String(it[1] || '');
1422
+ var x4 = sR.x + i * g4.step;
1423
+ sh.push(shape('roundRect', x4, cyS, g4.w, rowHS, acc ? S.soft : S.surface, { adj: 5000, line: acc ? undefined : S.line }));
1424
+ var num = String(i + 1); if (num.length < 2) num = '0' + num;
1425
+ var pd = S2.pad, numH = S2.numH, ttlH = S2.titleH;
1426
+ sh.push(txSp(x4 + 0.14, cyS + pd, g4.w - 0.28, numH, [[{ t: num, sz: 10, b: 1, col: S.accent, font: S.font }]]));
1427
+ sh.push(txSp(x4 + 0.14, cyS + pd + numH, g4.w - 0.28, ttlH, [[{ t: t4, sz: 13, b: 1, col: acc ? S.accent : S.ink, font: S.font }]]));
1428
+ if (d4) sh.push(txSp(x4 + 0.14, cyS + pd + numH + ttlH, g4.w - 0.28, Math.max(0.2, rowHS - pd * 2 - numH - ttlH), [[{ t: d4, sz: 10, col: S.faint, font: S.font }]]));
1429
+ if (i < n4 - 1) {
1430
+ sh.push(txSp(x4 + g4.w, cyS, S2.arrowW + S2.gap, rowHS, [[{ t: '→', sz: 12, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1431
+ }
1432
+ });
1433
+ cyS += rowHS + 0.3;
1434
+ });
1435
+ } else if (type === 'table') {
1436
+ var tR = regOf('table', 'primary', { bottom: bodyBottom }) || { x: MX, y: CONTENT_TOP, w: CW, h: bodyBottom - CONTENT_TOP };
1437
+ var headRow = sec.table.head;
1438
+ var colW = sec.table.colW || headRow.map(function () { return tR.w / headRow.length; });
1439
+ sh.push(table(tR.x, tR.y, tR.w, colW, [headRow].concat(sec.table.rows), S, { availH: tR.h }));
1440
+ } else if (type === 'timeline') {
1441
+ var T = PT.timeline, ps = sec.phases || [], np = Math.max(1, ps.length);
1442
+ var tLR = regOf('timeline', 'primary') || { x: MX, y: T.labelY, w: CW };
1443
+ var stepW = (tLR.w - 1.0) / np;
1444
+ sh.push(shape('rect', tLR.x + 0.2, T.axisY, tLR.w - 0.4, 0.02, S.line));
1445
+ ps.forEach(function (p, i) {
1446
+ var x5 = tLR.x + 0.2 + i * stepW;
1447
+ var on = p[3] === 'done' || p[3] === 'now';
1448
+ sh.push(shape('ellipse', x5 - T.dotR / 2, T.dotY, T.dotR, T.dotR, on ? S.accent : S.line));
1449
+ sh.push(txSp(x5 - 0.1, T.labelY, stepW - 0.2, 0.4, [[{ t: p[0], sz: 12, b: 1, col: S.accent, font: S.font, spc: 100 }]]));
1450
+ sh.push(txSp(x5 - 0.1, T.nameY, stepW - 0.2, 0.5, [[{ t: p[1], sz: 17, b: 1, col: p[3] === 'now' ? S.accent : S.ink, font: S.fontDisplay }]]));
1451
+ sh.push(txSp(x5 - 0.1, T.descY, stepW - 0.25, T.descH, [[{ t: p[2] || '', sz: 12, col: S.body, font: S.font }]], { lineSpacing: 18 }));
1452
+ });
1453
+ } else if (type === 'bar') {
1454
+ var ch = sec.chart || {};
1455
+ var dcols2 = (ch.colors || dataColors(model.style, model.theme));
1456
+ var botY = chartBottom(!!sec.soWhat, !!sec.footnote) - (sec.note ? 0.42 : 0.05);
1457
+ var isHBar = ch.type === 'hbar';
1458
+ var bRR = regOf('bar', isHBar ? 'hbar' : 'primary', { bottom: botY }) ||
1459
+ { x: isHBar ? MX : MX + PT.bar.chartX, y: isHBar ? PT.bar.hbarY0 : PT.bar.chartY,
1460
+ w: isHBar ? CW : CW - PT.bar.chartW, h: 3 };
1461
+ if (isHBar) {
1462
+ chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, bRR.h, dcols2);
1463
+ } else {
1464
+ chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, Math.max(1.5, bRR.h), dcols2);
1465
+ }
1466
+ if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
1467
+ } else if (type === 'twocol' || type === 'threecol') {
1468
+ var ps2 = sec.paragraphs || [];
1469
+ var nCol = (type === 'threecol') ? 3 : 2;
1470
+ var gapC = (type === 'threecol') ? PT.research.col3Gap : PT.twocol.colGap;
1471
+ var tcR = regOf('twocol', 'primary', { top: bodyY, bottom: bodyBottom }) ||
1472
+ { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1473
+ var gC = cols(tcR.w, nCol, gapC);
1474
+ var perC = Math.ceil(ps2.length / nCol);
1475
+ var availC = tcR.h;
1476
+ /* 探针与 build_pptx.js 同算法:按「各栏字数之和」的最大值估算(保证两通道字号一致) */
1477
+ var longest = 0, gi, pi2;
1478
+ for (gi = 0; gi < nCol; gi++) {
1479
+ var colG = ps2.slice(gi * perC, (gi + 1) * perC), ssum = 0;
1480
+ for (pi2 = 0; pi2 < colG.length; pi2++) ssum += String(colG[pi2][1] || '').length;
1481
+ if (ssum > longest) longest = ssum;
1482
+ }
1483
+ var probe = [], w4;
1484
+ for (w4 = 0; w4 < longest / 120; w4++) probe.push(new Array(121).join('x'));
1485
+ if (!probe.length) probe.push(new Array(1 + longest).join('x'));
1486
+ var fzC = fitFont(probe, gC.w, availC, { max: 13.5, gapFactor: 1.2 });
1487
+ for (var ci3 = 0; ci3 < nCol; ci3++) {
1488
+ var colC = ps2.slice(ci3 * perC, (ci3 + 1) * perC);
1489
+ if (!colC.length) continue;
1490
+ var runsC = [];
1491
+ colC.forEach(function (p) {
1492
+ runsC.push([{ t: p[0] + ' ', sz: fzC, b: 1, col: S.ink, font: S.font },
1493
+ { t: p[1], sz: fzC - 1, col: S.body, font: S.font }]);
1494
+ });
1495
+ sh.push(txSp(tcR.x + ci3 * gC.step, tcR.y, gC.w, availC, runsC,
1496
+ { lineSpacing: fzC * 1.5, spaceBefore: 7 }));
1497
+ }
1498
+ } else if (type === 'halftable') {
1499
+ var yH = sec.lead ? PT.common.bodyYWithLead : PT.research.denseTableY;
1500
+ var hLR = regOf('halftable', 'left', { top: yH, bottom: bodyBottom }) ||
1501
+ { x: MX, y: yH, w: CW * PT.research.halfTableW, h: bodyBottom - yH };
1502
+ var hRR = regOf('halftable', 'right', { top: yH, bottom: bodyBottom }) ||
1503
+ { x: MX + CW * PT.research.halfChartX, y: yH, w: CW * PT.research.halfChartW, h: bodyBottom - yH };
1504
+ var lwH = hLR.w;
1505
+ if (sec.table && sec.table.head) {
1506
+ var colWH = sec.table.colW || sec.table.head.map(function () { return lwH / sec.table.head.length; });
1507
+ sh.push(table(hLR.x, hLR.y, lwH, colWH, [sec.table.head].concat(sec.table.rows || []), S,
1508
+ { availH: hLR.h, maxRowH: PT.research.denseRowH + 0.1, minRowH: 0.28 }));
1509
+ }
1510
+ var ecH = sec.chart || {};
1511
+ if (ecH.labels && ecH.values) {
1512
+ chartBlockShapes(sh, ecH, S, hRR.x, hRR.y, hRR.w, Math.max(1.2, hRR.h),
1513
+ (ecH.colors || dataColors(model.style, model.theme)));
1514
+ }
1515
+ } else if (type === 'matrix') {
1516
+ var rhH = sec.rowHeads || [], chH = sec.colHeads || [];
1517
+ var mxR = regOf('matrix', 'primary', { bottom: bodyBottom }) ||
1518
+ { x: MX, y: PT.research.matrixY, w: CW, h: bodyBottom - PT.research.matrixY };
1519
+ var mY = mxR.y, mLW = PT.research.matrixLabelW;
1520
+ var rowsC = sec.cells || [], nR2 = Math.max(1, rowsC.length);
1521
+ var availM = mxR.h - 0.42;
1522
+ var mCH = fitRowH(availM, nR2, PT.research.matrixCellH, 0.3);
1523
+ var mW = (mxR.w - mLW) / Math.max(1, chH.length);
1524
+ chH.forEach(function (ch3, ci) {
1525
+ sh.push(txSp(mxR.x + mLW + ci * mW, mY, mW, 0.34, [[{ t: ch3, sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1526
+ });
1527
+ rowsC.forEach(function (rowC, ri) {
1528
+ var y6 = mY + 0.42 + ri * mCH;
1529
+ sh.push(txSp(mxR.x, y6 + (mCH - 0.5) / 2, mLW - 0.15, 0.5, [[{ t: rhH[ri] || '', sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
1530
+ rowC.forEach(function (cell, ci) {
1531
+ var acc = isRec(cell) && cell.accent;
1532
+ var txt = isRec(cell) ? (cell.t || '') : String(cell);
1533
+ var cx4 = mxR.x + mLW + ci * mW;
1534
+ sh.push(shape('roundRect', cx4 + 0.04, y6, mW - 0.08, mCH - 0.1, acc ? S.soft : S.surface,
1535
+ { adj: 3000, line: acc ? undefined : S.line }));
1536
+ sh.push(txSp(cx4 + 0.14, y6 + 0.08, mW - 0.28, mCH - 0.26, [[{ t: txt, sz: 11, col: acc ? S.accent : S.body, font: S.font }]], { anchor: 'ctr' }));
1537
+ });
1538
+ });
1539
+ } else if (type === 'lane') {
1540
+ var lnS = sec.lanes || [], nL = Math.max(1, lnS.length);
1541
+ var startL = sec.lead ? PT.common.bodyYWithLead : PT.arch.fullStartY;
1542
+ var lnR = regOf('lane', 'primary', { mode: model.mode, bottom: bodyBottom }) ||
1543
+ { x: MX, y: startL, w: CW, h: bodyBottom - startL };
1544
+ var availL = lnR.h;
1545
+ var lnH = fitRowH(availL - (nL - 1) * PT.arch.laneGap, nL, PT.arch.laneH, 0.3);
1546
+ var lnGap = PT.arch.laneGap, lnHW = PT.arch.laneHeadW;
1547
+ lnS.forEach(function (ln, li) {
1548
+ var y7 = lnR.y + li * (lnH + lnGap);
1549
+ sh.push(shape('roundRect', lnR.x, y7, lnR.w, lnH, S.surface, { adj: 2500, line: S.line }));
1550
+ sh.push(shape('rect', lnR.x, y7, lnHW, lnH, S.surface, { line: S.line }));
1551
+ sh.push(txSp(lnR.x + 0.08, y7, lnHW - 0.16, lnH, [[{ t: ln[0], sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1552
+ var steps = ln[1] || [];
1553
+ var stepW2 = Math.min(PT.arch.stepMaxW, (lnR.w - lnHW - 0.4 - (steps.length - 1) * PT.arch.stepGap) / Math.max(1, steps.length));
1554
+ steps.forEach(function (st, si) {
1555
+ var stAcc = isRec(st) && st.accent;
1556
+ var stT = isRec(st) ? st.t : st;
1557
+ var sx2 = lnR.x + lnHW + 0.2 + si * (stepW2 + PT.arch.stepGap);
1558
+ sh.push(shape('roundRect', sx2, y7 + 0.13, stepW2, lnH - 0.26, stAcc ? S.soft : S.bg,
1559
+ { adj: 4000, line: stAcc ? undefined : S.line }));
1560
+ sh.push(txSp(sx2 + 0.06, y7 + 0.13, stepW2 - 0.12, lnH - 0.26,
1561
+ [[{ t: stT, sz: 11, b: stAcc ? 1 : 0, col: stAcc ? S.accent : S.ink, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1562
+ if (si < steps.length - 1) {
1563
+ sh.push(txSp(sx2 + stepW2, y7 + 0.13, PT.arch.stepGap, lnH - 0.26, [[{ t: '→', sz: 11, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1564
+ }
1565
+ });
1566
+ });
1567
+ } else if (type === 'cards') {
1568
+ var cdC = PT.cards, cds = sec.cards || [];
1569
+ var cR = regOf('cards', 'primary', { bottom: bodyBottom }) ||
1570
+ { x: MX, y: cdC.startY, w: CW, h: bodyBottom - cdC.startY };
1571
+ var cN = Math.min(sec.columns || 3, cds.length || 1);
1572
+ var rN = Math.ceil((cds.length || 1) / cN);
1573
+ var gw2 = (cR.w - (cN - 1) * cdC.gap) / cN;
1574
+ var gh2 = fitRowH(cR.h - (rN - 1) * cdC.gap, rN, cdC.maxH, 0.6);
1575
+ cds.forEach(function (cd, i) {
1576
+ var col2 = i % cN, row2 = Math.floor(i / cN);
1577
+ var x8 = cR.x + col2 * (gw2 + cdC.gap), y8 = cR.y + row2 * (gh2 + cdC.gap);
1578
+ sh.push(shape('roundRect', x8, y8, gw2, gh2, S.surface, { line: S.line, adj: 5000 }));
1579
+ /* 卡片头路标(与 B 通道同源:accent 小方块 = PPTX 侧图标等价物) */
1580
+ var ico2 = 0.18;
1581
+ sh.push(shape('roundRect', x8 + 0.18, y8 + 0.2, ico2, ico2, S.accent, { adj: 2500 }));
1582
+ sh.push(txSp(x8 + 0.18 + ico2 + 0.1, y8 + 0.14, gw2 - 0.36 - ico2 - 0.1, cdC.titleH,
1583
+ [[{ t: cd.title, sz: 15, b: 1, col: S.ink, font: S.font }]]));
1584
+ var paras2 = (cd.points || []).map(function (pt) {
1585
+ var line;
1586
+ if (pt && pt.length != null && typeof pt !== 'string') {
1587
+ var k = pt[0] == null ? '' : String(pt[0]);
1588
+ var v = pt[1] == null ? '' : String(pt[1]);
1589
+ line = (k && v) ? ('· ' + k + ' ' + v) : ('· ' + (k || v));
1590
+ } else if (pt && typeof pt === 'object') {
1591
+ line = '· ' + String(pt.t || '') + (pt.d ? ' ' + String(pt.d) : '');
1592
+ } else {
1593
+ line = '· ' + String(pt == null ? '' : pt);
1594
+ }
1595
+ return [{ t: line, sz: 12, col: S.body, font: S.font }];
1596
+ });
1597
+ sh.push(txSp(x8 + 0.18, y8 + 0.14 + cdC.titleH + 0.06, gw2 - 0.36, Math.max(0.4, gh2 - cdC.titleH - 0.34), paras2,
1598
+ { lineSpacing: 18 }));
1599
+ });
1600
+ } else if (type === 'split') {
1601
+ /* 双区自由组合页:左区与右区各可为 要点 / 图表 / 表格 / 图片(与 B 通道同语义)。 */
1602
+ var SP = PT.split;
1603
+ var lRs = regOf('split', 'left', { bottom: bodyBottom }) ||
1604
+ { x: MX, y: SP.chartY, w: CW * SP.leftW, h: bodyBottom - SP.chartY };
1605
+ var rRs = regOf('split', 'right', { bottom: bodyBottom }) ||
1606
+ { x: MX + CW * SP.rightX, y: SP.chartY, w: CW * SP.rightW, h: bodyBottom - SP.chartY };
1607
+ var lw2 = lRs.w, rw2 = rRs.w, rxL = rRs.x;
1608
+ var lt2 = (sec.left && sec.left.type) || 'points';
1609
+ /* 要点区渲染(左右两区共用;与 B 通道同算法同文本) */
1610
+ var drawPointsL = function (el, x, w) {
1611
+ var ptsP = (el && el.points) || [];
1612
+ var lyP = bodyY, availP = bodyBottom - lyP;
1613
+ var rowHP = fitRowH(availP, ptsP.length, SP.rowH, 0.3);
1614
+ var probeP = [], q5;
1615
+ for (q5 = 0; q5 < ptsP.length; q5++) probeP.push(String(ptsP[q5][0] || '') + String(ptsP[q5][1] || ''));
1616
+ var fzP = fitFont(probeP, w - 0.4, availP, { max: 14, gapFactor: 0.6 });
1617
+ ptsP.forEach(function (pt, i) {
1618
+ var ly2 = lyP + i * rowHP;
1619
+ sh.push(shape('rect', x, ly2 + rowHP / 2 - 0.06, 0.12, 0.12, S.accent));
1620
+ sh.push(txSp(x + 0.28, ly2, w - 0.4, rowHP - 0.04,
1621
+ [[{ t: pt[0] + ' ', sz: fzP, b: 1, col: S.ink, font: S.font },
1622
+ { t: pt[1], sz: fzP - 1, col: S.body, font: S.font }]]));
1623
+ });
1624
+ };
1625
+ if (lt2 === 'table') {
1626
+ var lhL = [sec.left.head].concat(sec.left.rows);
1627
+ var lcwL = sec.left.colW || sec.left.head.map(function () { return lw2 / sec.left.head.length; });
1628
+ sh.push(table(lRs.x, SP.tableY, lw2, lcwL, lhL, S, { availH: bodyBottom - SP.tableY, maxRowH: SP.tableRowH, minRowH: 0.3 }));
1629
+ } else if (lt2 === 'image') {
1630
+ var imL = (sec.left && sec.left.image) || {};
1631
+ addImageEl(sh, imL.src, S, lRs.x, SP.chartY, lw2, Math.max(1.4, bodyBottom - SP.chartY),
1632
+ { placeholder: !!imL.placeholder || !imL.src, layout: 'half', multi: false, fit: imL.fit });
1633
+ if (imL.caption) sh.push(txSp(lRs.x, SP.capY, lw2, 0.35, [[{ t: imL.caption, sz: 11, col: S.faint, font: S.font }]], { align: 'ctr' }));
1634
+ } else if (lt2 !== 'points') {
1635
+ var lc2 = sec.left;
1636
+ chartBlockShapes(sh, lc2, S, lRs.x, SP.chartY, lw2,
1637
+ Math.max(1.4, bodyBottom - SP.chartY - (lc2.cap ? 0.1 : 0)),
1638
+ lc2.colors || dataColors(model.style, model.theme));
1639
+ if (lc2.cap) sh.push(txSp(lRs.x, SP.capY, lw2, 0.35, [[{ t: lc2.cap, sz: 12, col: S.body, font: S.font }]], { align: 'ctr' }));
1640
+ } else {
1641
+ drawPointsL(sec.left, lRs.x, lw2);
1642
+ }
1643
+ var rt = (sec.right && sec.right.type) || 'bar';
1644
+ if (rt === 'table') {
1645
+ var rrL = [sec.right.head].concat(sec.right.rows);
1646
+ var rcwL = sec.right.colW || sec.right.head.map(function () { return rw2 / sec.right.head.length; });
1647
+ sh.push(table(rxL, SP.tableY, rw2, rcwL, rrL, S, { availH: bodyBottom - SP.tableY, maxRowH: SP.tableRowH, minRowH: 0.3 }));
1648
+ } else if (rt === 'image') {
1649
+ var im2 = (sec.right && sec.right.image) || {};
1650
+ addImageEl(sh, im2.src, S, rxL, SP.chartY, rw2, Math.max(1.4, bodyBottom - SP.chartY),
1651
+ { placeholder: !!im2.placeholder || !im2.src, layout: 'half', multi: false });
1652
+ if (im2.caption) sh.push(txSp(rxL, SP.capY, rw2, 0.35, [[{ t: im2.caption, sz: 11, col: S.faint, font: S.font }]], { align: 'ctr' }));
1653
+ } else if (rt === 'points') {
1654
+ drawPointsL(sec.right, rxL, rw2);
1655
+ } else {
1656
+ var bc2 = sec.right;
1657
+ var dcols3 = bc2.colors || dataColors(model.style, model.theme);
1658
+ chartBlockShapes(sh, bc2, S, rxL, SP.chartY, rw2, Math.max(1.4, bodyBottom - SP.chartY - (bc2.cap ? 0.1 : 0)), dcols3);
1659
+ if (bc2.cap) sh.push(txSp(rxL, SP.capY, rw2, 0.35, [[{ t: bc2.cap, sz: 12, col: S.body, font: S.font }]], { align: 'ctr' }));
1660
+ }
1661
+ } else if (type === 'diagram') {
1662
+ var D = PT.diagram, lys = sec.layers || [];
1663
+ var dgR = regOf('diagram', 'primary', { mode: _mode, bottom: bodyBottom }) ||
1664
+ { x: MX, y: D.bodyStartY, w: CW, h: bodyBottom - D.bodyStartY };
1665
+ var dStart = dgR.y;
1666
+ var dEnd = dgR.y + dgR.h;
1667
+ var nLay = Math.max(1, lys.length);
1668
+ var lh2 = Math.max(0.6, Math.min(D.maxLayerH, (dEnd - dStart) / nLay - D.layerGap));
1669
+ lys.forEach(function (lay, li) {
1670
+ var y9 = dStart + li * (lh2 + D.layerGap);
1671
+ var focus = lay[2] === 'focus';
1672
+ sh.push(shape('roundRect', dgR.x, y9, D.layerBarW, lh2, focus ? S.accent : S.surface,
1673
+ { adj: 3000, line: focus ? undefined : S.line }));
1674
+ sh.push(txSp(dgR.x + 0.1, y9 + 0.1, D.layerBarW - 0.2, lh2 - 0.2, [[{ t: lay[0], sz: 12, b: 1, col: focus ? S.onAccent : S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1675
+ var nodes = lay[1] || [], nn2 = Math.max(1, nodes.length);
1676
+ var availW = dgR.w - D.layerBarW - 0.2;
1677
+ var nw2 = Math.min(D.nodeMaxW, (availW - (nn2 - 1) * D.nodeGap) / nn2);
1678
+ nodes.forEach(function (nd, ni) {
1679
+ var x10 = dgR.x + D.layerBarW + 0.2 + ni * (nw2 + D.nodeGap);
1680
+ var ndacc = isRec(nd) ? nd.accent : false;
1681
+ var ntt = isRec(nd) ? nd.t : nd;
1682
+ var nds = isRec(nd) ? (nd.d || '') : '';
1683
+ sh.push(shape('roundRect', x10, y9, nw2, lh2, ndacc ? S.soft : S.surface, { adj: 4000, line: S.line }));
1684
+ sh.push(txSp(x10 + 0.12, y9 + (nds ? 0.12 : lh2 / 2 - 0.2), nw2 - 0.24, 0.38,
1685
+ [[{ t: ntt, sz: 12.5, b: 1, col: ndacc ? S.accent : S.ink, font: S.font }]], { anchor: nds ? 't' : 'ctr' }));
1686
+ if (nds) sh.push(txSp(x10 + 0.12, y9 + lh2 - 0.5, nw2 - 0.24, 0.4, [[{ t: nds, sz: 10, col: S.faint, font: S.font }]]));
1687
+ });
1688
+ if (li < lys.length - 1) {
1689
+ var cy2 = y9 + lh2 + D.layerGap / 2;
1690
+ var cxm2 = dgR.x + D.layerBarW / 2;
1691
+ sh.push(shape('rect', cxm2 - 0.01, cy2 - 0.1, 0.02, 0.14, focus ? S.accent : S.line));
1692
+ sh.push(shape('rect', cxm2 - 0.06, cy2 + 0.04, 0.12, 0.02, focus ? S.accent : S.line));
1693
+ }
1694
+ });
1695
+ if (sec.legend) {
1696
+ var lx2 = MX;
1697
+ sec.legend.forEach(function (lg2) {
1698
+ sh.push(txSp(lx2, PH - 0.62, 3.2, 0.3, [[{ t: '● ' + lg2, sz: 10, col: S.faint, font: S.font }]]));
1699
+ lx2 += 2.0;
1700
+ });
1701
+ }
1702
+ } else if (type === 'exhibit') {
1703
+ var eReg = regOf('exhibit', 'primary')
1704
+ || { x: MX, y: PT.exhibit.chartY, w: CW, badgeY: PT.exhibit.badgeY };
1705
+ if (sec.exhibitNo) {
1706
+ sh.push(txSp(MX, eReg.badgeY != null ? eReg.badgeY : PT.exhibit.badgeY, CW, 0.35, [[{ t: String(sec.exhibitNo).toUpperCase(), sz: 11.5, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1707
+ }
1708
+ var ec2 = sec.chart || {};
1709
+ if (ec2.labels && ec2.values) {
1710
+ var edcols2 = (ec2.colors || dataColors(model.style, model.theme));
1711
+ var hh2 = Math.max(1.2, chartBottom(!!sec.soWhat, !!sec.footnote) - eReg.y - 0.05);
1712
+ if (ec2.type === 'hbar') {
1713
+ chartBlockShapes(sh, ec2, S, eReg.x, eReg.y, eReg.w, hh2, edcols2);
1714
+ } else {
1715
+ chartBlockShapes(sh, ec2, S, eReg.x + 0.4, eReg.y - 0.1, eReg.w - 0.8, hh2 + 0.15, edcols2);
1716
+ }
1717
+ }
1718
+ } else if (type === 'sankey' || type === 'treemap' || type === 'boxplot'
1719
+ || type === 'network' || type === 'marimekko' || type === 'streamgraph') {
1720
+ var infoR = regOf(type, 'primary', { bottom: bodyBottom }) ||
1721
+ { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1722
+ infoApprox(sh, sec, S, infoR.y, infoR.y + infoR.h);
1723
+ } else {
1724
+ /* points(默认):要点列表 + 可选右侧指标列 */
1725
+ var P2 = PT.points, pts3 = sec.points || [];
1726
+ var hasM = !!(sec.metrics && sec.metrics.length);
1727
+ var ptsR = regOf('points', 'primary', { top: bodyY, bottom: bodyBottom }) ||
1728
+ { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1729
+ /* 指标列最多 3 个 + 文本区严格分区(与 build_pptx.js 同算法) */
1730
+ var nm2 = hasM ? Math.min(sec.metrics.length, 3) : 0;
1731
+ var mw2 = hasM ? Math.min(P2.metricW, Math.max(1.6, (CW - 4.8) / nm2)) : 0;
1732
+ var textW = hasM ? Math.max(4.2, (PW - MX - nm2 * mw2) - MX - 0.35) : CW - 0.4;
1733
+ var availPt = ptsR.h;
1734
+ var rowHPt = fitRowH(availPt, pts3.length, P2.rowH, 0.3);
1735
+ var probeP = [], q5;
1736
+ for (q5 = 0; q5 < pts3.length; q5++) probeP.push(String(pts3[q5][0] || '') + String(pts3[q5][1] || ''));
1737
+ var fzP = fitFont(probeP, textW - 0.32, availPt, { max: 15, gapFactor: 0.55 });
1738
+ pts3.forEach(function (p, i) {
1739
+ var y11 = ptsR.y + i * rowHPt;
1740
+ sh.push(shape('rect', ptsR.x, y11 + rowHPt / 2 - P2.markSize / 2, P2.markSize, P2.markSize, S.accent));
1741
+ sh.push(txSp(ptsR.x + 0.32, y11, textW, rowHPt - 0.04,
1742
+ [[{ t: p[0] + ' ', sz: fzP, b: 1, col: S.ink, font: S.font },
1743
+ { t: p[1], sz: fzP - 1, col: S.body, font: S.font }]]));
1744
+ });
1745
+ if (hasM) {
1746
+ var gx2 = PW - MX - nm2 * mw2;
1747
+ var vSz = nm2 >= 3 ? 28 : 34;
1748
+ sec.metrics.slice(0, nm2).forEach(function (m, i) {
1749
+ var x12 = gx2 + i * mw2;
1750
+ sh.push(txSp(x12, P2.metricY, mw2 - 0.2, P2.metricValH, [[{ t: m[0], sz: vSz, b: 1, col: S.accent, font: S.fontDisplay }]]));
1751
+ sh.push(txSp(x12, P2.metricCapY, mw2 - 0.2, 0.9, [[{ t: m[1], sz: 11, col: S.faint, font: S.font }]]));
1752
+ });
1753
+ }
1754
+ }
1755
+ /* research 通用可选件:so-what 结论条 + 页脚来源行 + 待核实条(共用同一槽位) */
1756
+ if (sec.soWhat) soWhatBar(sh, sec.soWhat, S);
1757
+ if (sec.footnote) footnoteLine(sh, sec.footnote, S);
1758
+ if (flagH) flagBar(sh, flagItems, S, flagY);
1759
+ slides.push(sh);
1760
+ });
1761
+
1762
+ /* 收尾(主题一致强调带——accent-soft 底 + 常规文字,浅色模式不再出深色页) */
1763
+ var CL = PT.closing;
1764
+ var cl = [shape('rect', 0, 0, PW, PH, S.soft)];
1765
+ cl.push(shape('rect', MX, CL.eyebrowY, 0.9, 0.045, S.accent));
1766
+ cl.push(txSp(MX, CL.eyebrowY + 0.16, CW, 0.4, [[{ t: '下一步', sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1767
+ cl.push(txSp(MX, CL.titleY, CW - 1, CL.titleH, [[{ t: model.closing.title, sz: 30, b: 1, col: S.ink, font: S.fontDisplay }]]));
1768
+ var clPts = model.closing.points || [], clN = Math.max(1, clPts.length);
1769
+ var clW = (CW - (clN - 1) * 0.3) / clN;
1770
+ clPts.forEach(function (p, i) {
1771
+ var x13 = MX + i * (clW + 0.3);
1772
+ cl.push(shape('roundRect', x13, CL.pointsY - 0.2, clW, CL.pointTitleH + CL.pointBodyH + 0.32, S.surface, { line: S.line, adj: 4000 }));
1773
+ cl.push(txSp(x13 + 0.18, CL.pointsY, clW - 0.36, CL.pointTitleH, [[{ t: p[0], sz: 17, b: 1, col: S.accent, font: S.font }]]));
1774
+ cl.push(txSp(x13 + 0.18, CL.pointsY + CL.pointTitleH, clW - 0.36, CL.pointBodyH, [[{ t: p[1], sz: 12.5, col: S.body, font: S.font }]]));
1775
+ });
1776
+ slides.push(cl);
1777
+
1778
+ /* 页码 */
1779
+ var total = slides.length;
1780
+ slides.forEach(function (sh, i) {
1781
+ sh.push(txSp(PW - 1.4, (LAY.pageNumY != null ? LAY.pageNumY : PH - 0.5), 0.9, 0.3,
1782
+ [[{ t: (i + 1) + ' / ' + total, sz: 10, col: S.faint, font: S.font }]], { align: 'r' }));
1783
+ });
1784
+ return slides;
1785
+ }
1786
+
1787
+ /* ── 打包骨架 ── */
1788
+ var NS = {
1789
+ a: 'http://schemas.openxmlformats.org/drawingml/2006/main',
1790
+ p: 'http://schemas.openxmlformats.org/presentationml/2006/main',
1791
+ r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
1792
+ };
1793
+ function slideXml(shapes) {
1794
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
1795
+ '<p:sld xmlns:a="' + NS.a + '" xmlns:p="' + NS.p + '" xmlns:r="' + NS.r + '">' +
1796
+ '<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
1797
+ '<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
1798
+ shapes.join('') + '</p:spTree></p:cSld><p:clrMapOvr><a:overrideClrMapping bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:clrMapOvr></p:sld>';
1799
+ }
1800
+ function relsXml(pairs) {
1801
+ var rs = pairs.map(function (p) {
1802
+ return '<Relationship Id="' + p[0] + '" Type="' + p[1] + '" Target="' + p[2] + '"/>';
1803
+ }).join('');
1804
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
1805
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rs + '</Relationships>';
1806
+ }
1807
+ var RT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
1808
+
1809
+ /* 每页 slide XML(预览模态用:与导出同一引擎,所见即所得)
1810
+ theme="dark" 时用 PRESETS_DARK(与页面深色主题同源),默认 light */
1811
+ function themePresets(theme) {
1812
+ return (theme === 'dark' && typeof PRESETS_DARK !== 'undefined' &&
1813
+ PRESETS_DARK && Object.keys(PRESETS_DARK).length) ? PRESETS_DARK : PRESETS;
1814
+ }
1815
+ function slidesXml(model) {
1816
+ var S = themePresets(model && model.theme)[model.style] || PRESETS['business-blue'];
1817
+ return slidesOf(model, S).map(slideXml);
1818
+ }
1819
+
1820
+ /* ── 模型自检(schema 单源驱动:scripts/model-schema.json 经 sync_runtime 注入的 MODEL_SCHEMA;
1821
+ 与 scripts/extract_model.py 本地校验同一份定义,双端不漂移) ── */
1822
+ function schemaGet(obj, path) {
1823
+ var cur = obj, ks = path.split('.');
1824
+ for (var i = 0; i < ks.length; i++) {
1825
+ if (cur == null || typeof cur !== 'object') return undefined;
1826
+ cur = cur[ks[i]];
1827
+ }
1828
+ return cur;
1829
+ }
1830
+ function schemaFieldOk(sec, spec) {
1831
+ /* 'anyof:a|b:c'(任一满足,用于 image 页型 src/items/placeholder 三选一);
1832
+ 'a.b:array' / 'a.b:str' / 'a.b'(真值检查)——与 scripts/extract_model.py 同语义。 */
1833
+ if (spec.indexOf('anyof:') === 0) {
1834
+ return spec.slice(6).split('|').some(function (alt) { return alt && schemaFieldOk(sec, alt); });
1835
+ }
1836
+ var m = spec.match(/^(.*?)(?::(array|str))?$/), path = m[1], kind = m[2];
1837
+ var v = schemaGet(sec, path);
1838
+ if (kind === 'array') return Array.isArray(v) && v.length > 0;
1839
+ if (kind === 'str') return typeof v === 'string' && v.trim().length > 0;
1840
+ return !!v;
1841
+ }
1842
+ /* 图片模型软校验:版式 / 裁切策略 / 多图数量 / 外链 src——与 validate_pptx.py 硬门禁同口径,
1843
+ 浏览器预览与 extract_model.py 都能提前看到问题。 */
1844
+ function imageModelWarnings(sec, where, warnings) {
1845
+ var IS = (typeof IMG !== 'undefined' && IMG) || {};
1846
+ var holders = [{ img: sec.image, at: where + '.image' }];
1847
+ if (sec.right && sec.right.image) holders.push({ img: sec.right.image, at: where + '.right.image' });
1848
+ holders.forEach(function (h) {
1849
+ var img = h.img;
1850
+ if (!img || typeof img !== 'object') return;
1851
+ var layouts = IS.layouts || ['full', 'half', 'bleed', 'grid', 'compare', 'wall'];
1852
+ var fitEnum = IS.fitEnum || ['cover', 'contain'];
1853
+ var items = Array.isArray(img.items) ? img.items.filter(Boolean) : [];
1854
+ var layout = String(img.layout || (items.length > 1 ? 'grid' : 'full')).toLowerCase();
1855
+ if (layouts.indexOf(layout) < 0) {
1856
+ warnings.push(h.at + '.layout="' + layout + '" 未知(应为 ' + layouts.join('/') + ')');
1857
+ }
1858
+ if (img.fit && fitEnum.indexOf(String(img.fit).toLowerCase()) < 0) {
1859
+ warnings.push(h.at + '.fit="' + img.fit + '" 未知(应为 ' + fitEnum.join('/') + ')');
1860
+ }
1861
+ if (img.placeholder && (img.src || items.length)) {
1862
+ warnings.push(h.at + ' 同时声明 placeholder 与图片源(placeholder 优先,图片将被忽略)');
1863
+ }
1864
+ if (items.length > (IS.maxPerPage || 6)) {
1865
+ warnings.push(h.at + '.items 共 ' + items.length + ' 张,超出单页上限 ' + (IS.maxPerPage || 6));
1866
+ }
1867
+ if (layout === 'compare' && items.length && items.length !== 2) {
1868
+ warnings.push(h.at + '.items 为 compare 版式时应为 2 张(当前 ' + items.length + ')');
1869
+ }
1870
+ [img].concat(items).forEach(function (it) {
1871
+ if (it && typeof it === 'object' && typeof it.src === 'string' &&
1872
+ /^\s*(?:https?:)?\/\//.test(it.src)) {
1873
+ warnings.push(h.at + ' 含外链 src(只允许 data: 内联或相对路径)');
1874
+ }
1875
+ });
1876
+ });
1877
+ }
1878
+
1879
+ function validateModel(model) {
1880
+ var missing = [], warnings = [];
1881
+ var M = (typeof MODEL_SCHEMA !== 'undefined' && MODEL_SCHEMA) || {};
1882
+ var pageTypes = M.pageTypes || {};
1883
+ if (!model || typeof model !== 'object') {
1884
+ return { ok: false, missing: ['window.REPORT_MODEL(内容模型缺失)'], warnings: warnings, pages: 0 };
1885
+ }
1886
+ (M.model && M.model.required || ['title', 'sections:array', 'closing.title', 'closing.points:array'])
1887
+ .forEach(function (spec) {
1888
+ if (!schemaFieldOk(model, spec)) missing.push(spec.split(':')[0] + '(顶层必填)');
1889
+ });
1890
+ var mode = model.mode || 'presentation';
1891
+ var hasAgenda = Array.isArray(model.agenda) && model.agenda.length > 0;
1892
+ var agMin = (M.model && M.model.agendaMin && M.model.agendaMin[mode]) || 0;
1893
+ if (agMin > 0 && (!hasAgenda || model.agenda.length < agMin)) {
1894
+ missing.push('agenda(大纲 ≥' + agMin + ' 条)');
1895
+ }
1896
+ var secs = model.sections || [];
1897
+ if (!Array.isArray(secs) || secs.length < ((M.model && M.model.sectionsMin) || 1)) {
1898
+ missing.push('sections(章节页 ≥1)');
1899
+ }
1900
+ if (!model.closing || !model.closing.title) missing.push('closing(收尾页)');
1901
+ secs.forEach(function (sec, i) {
1902
+ var t = sec.type || 'points';
1903
+ var def = pageTypes[t];
1904
+ var where = 'sections[' + i + ']';
1905
+ if (!def) { missing.push(where + '.type="' + t + '"(未知页型)'); return; }
1906
+ if (!sec.title) missing.push(where + '.title(第 ' + (i + 1) + ' 章标题)');
1907
+ (def.required || []).forEach(function (spec) {
1908
+ if (!schemaFieldOk(sec, spec)) missing.push(where + '.' + spec + '(' + (def.label || t) + '必填)');
1909
+ });
1910
+ if (def.modes && def.modes.indexOf(mode) < 0) {
1911
+ warnings.push('sections[' + i + '] 页型 "' + t + '" 文档口径适用于 ' + def.modes.join('/') + ',当前 mode="' + mode + '"');
1912
+ }
1913
+ imageModelWarnings(sec, where, warnings);
1914
+ });
1915
+ if (M.modes && model.mode && M.modes.indexOf(model.mode) < 0) {
1916
+ warnings.push('mode="' + model.mode + '" 未知,按 presentation 处理');
1917
+ }
1918
+ if (model.style && !PRESETS[model.style]) warnings.push('style="' + model.style + '" 未知,回落商务蓝');
1919
+ if (model.theme && model.theme !== 'light' && model.theme !== 'dark') {
1920
+ warnings.push('theme="' + model.theme + '" 未知(应为 light/dark),按浅色导出');
1921
+ }
1922
+ if (secs.length < ((M.model && M.model.sectionsRecommended) || 3)) {
1923
+ warnings.push('章节页仅 ' + secs.length + ' 页,正式报告建议 ≥3');
1924
+ }
1925
+ var agMax = (M.model && M.model.agendaComfortMax) || 16;
1926
+ if ((model.agenda || []).length > agMax) {
1927
+ warnings.push('agenda ' + model.agenda.length + ' 条超出单页舒适上限 ' + agMax + ',建议拆分');
1928
+ }
1929
+ var pages = 1 + (hasAgenda ? 1 : 0) + secs.length + 1; /* 封面+大纲(可选)+章节+收尾 */
1930
+ return { ok: missing.length === 0, missing: missing, warnings: warnings, pages: pages };
1931
+ }
1932
+
1933
+ var api = {
1934
+ slidesXml: slidesXml,
1935
+ validateModel: validateModel,
1936
+ /* 以下供 scripts/gen_channel_a.js 在 Node 侧组装完整 PPTX 包(回归双裁判内部质量路径) */
1937
+ slidesOf: slidesOf, slideXml: slideXml, relsXml: relsXml, NS: NS, RT: RT, PRESETS: PRESETS,
1938
+ PRESETS_DARK: (typeof PRESETS_DARK !== 'undefined' ? PRESETS_DARK : null),
1939
+ dataColors: dataColors,
1940
+ esc: esc, solid: solid, E: E
1941
+ };
1942
+ if (typeof module !== 'undefined' && module.exports) module.exports = api; /* Node:gen_channel_a.js 用 */
1943
+ g.TopPptHtml = api;
1944
+ })(typeof window !== 'undefined' ? window : globalThis);