@guandata/guanvis 0.1.16

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 (55) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +85 -0
  3. package/bin/run.js +87 -0
  4. package/binaries/guanvis-darwin-arm64 +0 -0
  5. package/binaries/guanvis-darwin-x64 +0 -0
  6. package/binaries/guanvis-linux-arm64 +0 -0
  7. package/binaries/guanvis-linux-x64 +0 -0
  8. package/binaries/guanvis-win32-x64.exe +0 -0
  9. package/package.json +41 -0
  10. package/skills/guanvis/SKILL.md +334 -0
  11. package/skills/guanvis/evals/china_map/card_01_basic_map.js +13 -0
  12. package/skills/guanvis/evals/china_map/card_02_world_map.js +10 -0
  13. package/skills/guanvis/evals/china_map/card_03_point_map.js +12 -0
  14. package/skills/guanvis/evals/china_map/page.js +10 -0
  15. package/skills/guanvis/evals/china_map/schema.js +10 -0
  16. package/skills/guanvis/evals/comparative_mvp_roundtrip/README.md +30 -0
  17. package/skills/guanvis/evals/comparative_mvp_roundtrip/card_01_comparative_mvp.js +72 -0
  18. package/skills/guanvis/evals/comparative_mvp_roundtrip/page.js +6 -0
  19. package/skills/guanvis/evals/comparative_mvp_roundtrip/schema.js +22 -0
  20. package/skills/guanvis/evals/comparative_mvp_roundtrip/verify_default_roundtrip.sh +92 -0
  21. package/skills/guanvis/evals/custom_chart_echarts/card_01_bar.js +13 -0
  22. package/skills/guanvis/evals/custom_chart_echarts/card_02_pie.js +13 -0
  23. package/skills/guanvis/evals/custom_chart_echarts/charts/bar.js +11 -0
  24. package/skills/guanvis/evals/custom_chart_echarts/charts/pie.js +15 -0
  25. package/skills/guanvis/evals/custom_chart_echarts/page.js +5 -0
  26. package/skills/guanvis/evals/custom_chart_echarts/schema.js +9 -0
  27. package/skills/guanvis/evals/percentage_advcalc_roundtrip/README.md +26 -0
  28. package/skills/guanvis/evals/percentage_advcalc_roundtrip/card_01_percentage_advcalc.js +44 -0
  29. package/skills/guanvis/evals/percentage_advcalc_roundtrip/card_02_scroll_percentage.js +12 -0
  30. package/skills/guanvis/evals/percentage_advcalc_roundtrip/page.js +6 -0
  31. package/skills/guanvis/evals/percentage_advcalc_roundtrip/schema.js +22 -0
  32. package/skills/guanvis/evals/rank_advcalc_roundtrip/README.md +28 -0
  33. package/skills/guanvis/evals/rank_advcalc_roundtrip/card_01_rank_advcalc.js +59 -0
  34. package/skills/guanvis/evals/rank_advcalc_roundtrip/page.js +6 -0
  35. package/skills/guanvis/evals/rank_advcalc_roundtrip/schema.js +22 -0
  36. package/skills/guanvis/evals/rank_advcalc_roundtrip/verify_default_roundtrip.sh +135 -0
  37. package/skills/guanvis/evals/running_total_advcalc_roundtrip/README.md +15 -0
  38. package/skills/guanvis/evals/running_total_advcalc_roundtrip/card_01_running_total_advcalc.js +74 -0
  39. package/skills/guanvis/evals/running_total_advcalc_roundtrip/page.js +6 -0
  40. package/skills/guanvis/evals/running_total_advcalc_roundtrip/schema.js +22 -0
  41. package/skills/guanvis/evals/sales_dashboard/card_01_revenue_column.js +10 -0
  42. package/skills/guanvis/evals/sales_dashboard/card_02_trend_line.js +9 -0
  43. package/skills/guanvis/evals/sales_dashboard/card_03_kpi.js +6 -0
  44. package/skills/guanvis/evals/sales_dashboard/card_04_pie.js +9 -0
  45. package/skills/guanvis/evals/sales_dashboard/card_05_calc_field_test.js +12 -0
  46. package/skills/guanvis/evals/sales_dashboard/page.js +11 -0
  47. package/skills/guanvis/evals/sales_dashboard/schema.js +14 -0
  48. package/skills/guanvis/evals/sales_dashboard/selector_01_region.js +10 -0
  49. package/skills/guanvis/references/api-reference.md +408 -0
  50. package/skills/guanvis/references/builder-reference.md +690 -0
  51. package/skills/guanvis/references/publish-and-constraints.md +46 -0
  52. package/skills/guanvis/references/tableau-migration.md +73 -0
  53. package/skills/guanvis/references/theme.md +115 -0
  54. package/skills/guanvis/references/troubleshooting.md +20 -0
  55. package/skills/guanvis/references/validation-and-chart-patterns.md +205 -0
@@ -0,0 +1,46 @@
1
+ ## 在线同步机制
2
+
3
+ `publish` 和 `upload` 命令通过 BI 的 transfer API 直接将资源包上传到目标系统:
4
+
5
+ - **接口**:`POST /api/manual/template/transfer`(标准 multipart/form-data,表单字段名 `new-file`)
6
+ - **认证**:`X-Auth-Token` header(JWT token)
7
+ - **关键 header**:`raw-backend-response: TRUE`(绕过前端代理层,直达后端)
8
+ - **ID 策略**:`needIdMapping=false`,保持资源 ID 不变。同 ID 资源会被覆盖更新
9
+ - **通用性**:不需要目标系统开启"一键迁移"开关,所有客户环境可用
10
+ - **异步执行**:上传成功后返回 `taskId`,后端异步完成导入
11
+
12
+ 修改或重新发布已存在的仪表板资源时,默认按“新版本仪表板”处理:先从目标 BI 线上环境同步最新资源状态,再基于同步后的内容生成新的 Page/Card/Selector ID,并给仪表板名称追加版本号后发布。用户可能已经在 BI 上手工改过页面布局、卡片配置、筛选器或说明文本;如果只按本地旧文件 `publish`,同 ID 资源会被覆盖,导致线上改动丢失。只有用户明确要求覆盖原资源时,才允许保留原 ID 并同 ID 发布。
13
+
14
+ ## 认证与 CLI 集成
15
+
16
+ - 所有 API 调用通过底层 CLI 代理执行
17
+ - 认证由底层 CLI 负责:guancli 使用 `guancli auth use <profile>` 设置的活跃环境;guancli-lite 使用 `GUANCLI_BASE_URL`/`GUANCLI_TOKEN` 环境变量
18
+ - CLI 二进制路径通过 `GUANCLI_PATH` 环境变量指定,或自动在 PATH 中查找 `guancli-lite`/`guancli`
19
+
20
+ ## 硬约束
21
+
22
+ - `schema.js` 由 `init` 命令自动生成,不允许 AI 或人工修改。
23
+ - card 脚本必须调用 `registerCard(card.build())`(目录模式)或返回 `card.build()`(单文件模式)。
24
+ - text card 脚本必须调用 `registerTextCard(textCard.build())`。
25
+ - image card 脚本必须调用 `registerImageCard(imageCard.build())`。
26
+ - custom chart 脚本必须调用 `registerCustomChart(chart.build())`。
27
+ - selector 脚本必须调用 `registerSelector(selector.build())`。
28
+ - `fdId` 和 `dsId` 必须匹配目标 BI 系统的真实值(通过 schema.js 保证)。
29
+ - card 文件名建议 `card_NN_xxx.js` 控制执行顺序。
30
+ - selector 文件名建议 `selector_NN_xxx.js`,在 card 之后执行。
31
+ - page 文件必须命名为 `page.js`。
32
+ - 筛选器的 `linkTo(cardIndex)` 中 cardIndex 基于 `registerCard()` 的调用顺序(0-based)。
33
+ - **文件访问边界**:只允许访问以下路径,不要主动探索或读取用户未明确授权的其他目录:
34
+ - 当前工作目录(`-d` 指定的目录或 cwd)
35
+ - `guancli` / `guanvis` CLI 工具的输出
36
+ - 用户在对话中明确提到或授权的路径
37
+ - 禁止为了"查找参考代码""寻找类似实现"而主动扫描用户机器上的其他项目目录,这会引起安全顾虑。
38
+
39
+ ### ID 管理
40
+
41
+ 所有 Card、Selector 和 Page **必须**调用 `.setId(id)` 设置显式 ID。未设置 ID 会在 JS 校验和 Go 校验两层报错,阻止生成。
42
+
43
+ - `.setId(id)` 的 ID **必须**为严格 24 位字母数字字符串(正则:`^[a-zA-Z0-9]{24}$`),与后端 `RandUtil.uuid` 格式一致。
44
+ - **生成 ID**:先运行 `guanvis genid <数量>` 生成足够的 ID,在编写脚本时直接填入每个 card/selector/page 的 `.setId()` 调用中。
45
+ - **线上更新默认策略**:已发布过或线上正在使用的仪表板,后续修改默认生成新的 Page/Card/Selector ID,并给 Page 名称追加版本号后发布,保留旧版本不覆盖。
46
+ - **显式覆盖场景**:只有用户明确要求覆盖原仪表板时,才保持 `.setId()` 不变;多次 `publish` 会覆盖同 ID 资源(因为 transfer API 的 `needIdMapping=false`)。
@@ -0,0 +1,73 @@
1
+ ## Tableau 迁移 Checklist
2
+
3
+ 从 Tableau 迁移仪表板到观远BI时,必须按以下清单逐项检查:
4
+
5
+ ### 迁移前
6
+
7
+ 1. **解析 Tableau 仪表板结构**:用 `tableau-skill db <file> <dashboard_name> -f json` 获取完整结构
8
+ 2. **识别所有交互元素**:Dashboard JSON 中 `IsFilter=true` 的 zone 是筛选器,`IsParam=true` 的 zone 是参数控件
9
+ 3. **识别计算字段**:用 `tableau-skill calc <file> -f json` 获取所有计算字段,判断迁移策略
10
+
11
+ ### 筛选器翻译(必做)
12
+
13
+ Tableau 仪表板上的 filter zone 必须翻译为观远的 `createSelector()`:
14
+
15
+ | Tableau FilterMode | 观远 Selector 配置 |
16
+ |---|---|
17
+ | `typeinlist` / `typeinmulti` | `setMultiSelect(true)` + `setDisplayType(SelectorDisplay.SEARCH_BOX)` |
18
+ | `typeinsingle` | `setMultiSelect(false)` + `setDisplayType(SelectorDisplay.SEARCH_LIST)` |
19
+ | 日期范围筛选 | `.setSelectorType(SelectorType.CALENDAR)` 或 `.setSelectorType(SelectorType.DS_INTERVAL)` |
20
+ | 相对日期筛选(Relative date) | `.setTimeMacroOptions([{ name: "最近7天", expr: ["LAST_7_DAY"] }, ...])` |
21
+ | 数值范围筛选(利润率、金额等连续值) | `.setSelectorType(SelectorType.DS_INTERVAL)`(默认 BT 区间,起始值-结束值输入框) |
22
+ | 数值精确筛选 | `.setSelectorType(SelectorType.DS_INTERVAL).setFilterType("EQ")` |
23
+
24
+ **关键**:连续数值字段(如利润率、折扣率、金额等)**必须**使用 `DS_INTERVAL`,不要用 `DS_ELEMENTS`(列表选择会列出所有离散值,对连续数值没有意义)。
25
+
26
+ 所有 selector 必须调用 `.linkToAll()` 实现联动。
27
+
28
+ ### 计算字段迁移决策树
29
+
30
+ | Tableau 计算类型 | 观远实现方式 | 说明 |
31
+ |---|---|---|
32
+ | 行级计算(如 `利润/销售额`) | **数据集计算字段**(`guands dataset calc-field add`) | 所有卡片和筛选器都能引用,推荐 |
33
+ | 聚合计算(如 `SUM(利润)/SUM(销售额)`) | **卡片级 calcField** | 仅在单个卡片内有效 |
34
+ | LOD 表达式(FIXED/INCLUDE/EXCLUDE) | **ETL** 或 **卡片级 window calcField** | 需要根据具体语义判断 |
35
+ | 原生高级计算(Running Total、Rank) | **原生 advCalc builder** | 优先用 `runningTotal.*` / `rank.*`,不要手写 window calcField |
36
+ | 其他表计算 | **卡片级 window calcField** | 当前无原生 builder 时再用 `calculationType: "window"` |
37
+ | 多表 JOIN + 计算 | **ETL**(guanetl) | 需要先合表再计算 |
38
+
39
+ **重要**:如果需要在筛选器中引用计算字段(如"利润率"筛选器),必须使用**数据集计算字段**而非卡片级 calcField,因为 selector 只能绑定数据集上的物理字段。
40
+
41
+ ### calcField 命名规则
42
+
43
+ - calcField 的 name **不能与数据集已有字段同名**,否则观远BI会报 warning 并默认取数据集字段
44
+ - 如果数据集已有"利润率"字段,卡片级 calcField 应命名为"利润率_agg"或"利润率_calc"等
45
+ - `pack`/`publish` 时的验证会自动检查重名并输出 warning
46
+
47
+ ### 无法迁移内容的处理(必做)
48
+
49
+ 如果有无法 1:1 迁移的 Tableau 元素,**必须**在页面最底部添加一个全宽文本卡片说明:
50
+
51
+ ```javascript
52
+ var note = createTextCard("迁移说明")
53
+ .setId("<genid>")
54
+ .addMarkdown(
55
+ "---\n" +
56
+ "### Tableau 迁移说明\n\n" +
57
+ "本页面从 Tableau 工作簿 `<workbook_name>` 的 `<dashboard_name>` 迁移而来。\n\n" +
58
+ "**未迁移的内容:**\n" +
59
+ "- <列出未迁移项及原因>\n\n" +
60
+ "**与原始 Tableau 的差异:**\n" +
61
+ "- <列出差异>\n"
62
+ );
63
+ registerTextCard(note.build());
64
+
65
+ // 在 page.js 中将说明卡片放在最底部
66
+ page.addFullWidthCard(<noteCardIndex>, 3);
67
+ ```
68
+
69
+ 需要说明的典型内容:
70
+ - Tableau 参数控件(观远用筛选器替代)
71
+ - LOD 表达式的近似实现
72
+ - 颜色/格式的差异
73
+ - 交互行为的差异(如 Tableau 的 highlight action)
@@ -0,0 +1,115 @@
1
+ ## 仪表板主题
2
+
3
+ `preview` / `pack` / `publish` 会自动给页面与卡片注入主题,决策顺序如下:
4
+
5
+ 1. **`<dir>/themes/.preference.json`** — 用户/AI 主动写入的意图(`themeId` 优先,缺失再用 `keywords` 在 `.index.json` 上匹配;命中后 `themeId` 会被回写到 `.preference.json`)
6
+ 2. **`<dir>/themes/.applied.json`** — 上一次成功 `pack`/`publish` 选用的线上主题(用于改版自动继承;本次回退到 skill 自带 `simple.json` 时**不写**该文件)
7
+ 3. **skill 自带 `simple.json`** — 兜底(embed 在二进制里,**不联网、不会失败**)
8
+
9
+ AI 选择主题时不要把租户主题列表里的默认“浅色”/“深色”当作候选:这两套主题没有特殊样式。若除了“浅色”/“深色”外没有合适主题,不写主题偏好或执行 `theme preference --clear`,让解析落到 skill 内置“简约”(`simple.json`),不要为了命中而从列表里随便挑一个。
10
+
11
+ 兜底覆盖的失败场景(任意一种发生都会平滑退到 simple.json,命令不会因主题问题中断):
12
+ - 工程目录下没有 `themes/`、或 `.preference.json` 缺失
13
+ - `.preference.json` 里的 `themeId` 在租户线上 list 中不存在 / `Sync` 失败 / 当前命令本来就不联网
14
+ - `keywords` 在 `.index.json` 上没有任何命中
15
+ - `.applied.json` 引用的 `themeId` 本地快照已被删除(applied 步骤**只读本地、不再发起 sync**)
16
+
17
+ 每次执行命令时 stderr 会打印决策结果。命中线上主题时形如 `Theme: <name> [<id>] (source=preference|applied)`;落到自带兜底时形如 `Theme: 简约 (source=fallback, built-in simple.json)`,特意不打印 themeId 是因为那只是 skill 与 BI 默认值对齐的实现细节,不是租户主题列表里能查到的 ID。
18
+
19
+ ### 何时主动操作
20
+
21
+ > 所有 `theme *` 子命令的 **`[dir]` 可省**,缺省为当前工作目录;下面的示例假设你已经 `cd` 到 dashboard 工程目录。
22
+
23
+ - **用户描述风格**(如"深色科技风" "蓝色简约"):
24
+
25
+ ```bash
26
+ guanvis theme preference --keywords "深色 科技" --sync
27
+ ```
28
+
29
+ - `--keywords` 按 **空格 / 英文逗号 `,` / 中文逗号 `,`** 切分;命中分数最高者,平局取 `.index.json` 中**靠前**的条目(不区分 `themeType`)。AI 不应把默认“浅色”/“深色”作为关键词目标或手动指定的候选;命中后会把 `themeId` 写回 `.preference.json`,让 preview/pack/publish 后续都稳定指向同一主题。
30
+ - `--sync` 写入偏好后立刻拉一次 `/api/theme-setting/list` 落盘到 `themes/<id>.json` + `.index.json` + `.sync-meta.json`,让随后的 preview/pack 在离线状态也能看到真实主题。
31
+
32
+ - **用户给出明确的 themeId**:
33
+
34
+ ```bash
35
+ guanvis theme preference --theme-id custom_blue
36
+ ```
37
+
38
+ themeId 需为单个「安全文件名」片段:`[A-Za-z0-9_-]`、长度 ≤32,且不能含 `/`、`\`、子串 `..`;否则 CLI 会拒绝,sync 也会跳过并打 stderr 警告。
39
+
40
+ - **改版(不重提风格)**:什么都不用做,`.applied.json` 会让本次 `pack`/`publish` 沿用上次主题。
41
+ - **换风格**:再跑一次 `theme preference` 即可;下一次 `pack`/`publish` 实际使用新主题时 `.applied.json` 会自动覆盖。
42
+ - **没有合适主题**:不要选择默认“浅色”/“深色”,也不要从列表里随便挑;保持无偏好或执行 `theme preference --clear`,使用内置“简约”兜底。
43
+ - **回到 skill 自带兜底**:`guanvis theme preference --clear` 会**同时**删除 `.preference.json` 和 `.applied.json`,下次 `preview`/`pack`/`publish` 必然落到内置 `simple.json`(只删 `.preference.json` 而保留 `.applied.json` 的话,解析仍会通过 applied 继承上次主题,所以 `--clear` 一并清掉)。
44
+
45
+ ### 离线 vs 联网
46
+
47
+ | 命令 | 是否联网 | 说明 |
48
+ |---|---|---|
49
+ | `preview` / `pack` | **不联网** | 只读 `themes/`,缺主题文件直接退到 skill 自带 `simple.json`(不会因弱网失败) |
50
+ | `publish` | 按需 | 选定的 `themeId` 缺主题文件时调一次 list;list 失败则退到 `simple.json` + 警告,不阻塞发布 |
51
+ | `theme preference --sync` / `theme sync` | 是 | 强制刷新;网络失败仅打 warning,不会损坏既有 `themes/` 内容 |
52
+
53
+ > 想让 `preview` 看到与 `publish` 一致的主题,**先**跑 `theme preference --sync` 或 `theme sync`,再 `preview`。
54
+
55
+ ### `themes/` 目录文件说明
56
+
57
+ | 文件 | 作用 | 是否入库 |
58
+ |---|---|---|
59
+ | `<themeId>.json` | 单主题快照(list 单条原文,含 `themeSettings`) | 推荐入库(diff 友好) |
60
+ | `.preference.json` | 用户/AI 偏好 | 入库 |
61
+ | `.applied.json` | 上次记录的线上主题 | 入库 |
62
+ | `.index.json` | `themeId/themeName/themeType` 索引,供 keywords 匹配 | 入库 |
63
+ | `.sync-meta.json` | `syncedAt`、条目数 | 可选 |
64
+
65
+ 不要直接手改 `<themeId>.json`——每次 `sync` 会原子覆盖;要改就改 `.preference.json`。
66
+
67
+ ### 排错
68
+
69
+ - `theme show` 打印当前决策(`source` / `themeId` / `themeName` / `themeType`),只读。
70
+ - `theme list` 列出 `.index.json` 中候选;`.index.json` 缺失时提示先 `sync`。
71
+ - 看到 `Theme: 简约 (source=fallback, built-in simple.json)` 但用户期望非简约:检查 `.preference.json` 是否写好、对应 `themes/<id>.json` 是否存在;如果是 keywords 路径,跑 `theme list` 看候选名字是否真的包含关键词;联网命令还要看 stderr 是否打印了 `theme: sync failed: ...`。
72
+ - 看到 `multi-subdir project under <root> contains per-subdir themes/ in [...]`:你在多子目录工程的**根目录**直接跑了 preview/pack/publish,但子目录里有自己的 `themes/`。按提示 `cd` 进每个子目录单独执行;或如果不想用各子目录的主题,删除子目录下的 `themes/` 后再回到根目录运行。
73
+
74
+ ## 设计规则
75
+
76
+ `preview` / `pack` / `publish` 会自动应用内置设计规则。需要覆盖默认视觉规则时,在工程目录新建 `design-rule.json`,不要手改 `themes/<themeId>.json`。
77
+
78
+ `design-rule.json` 只用于两类配置:
79
+
80
+ - `componentDefaults`:卡片静态默认规则,例如 `SINGLE_VALUE` 单指标卡、`KPI_CARD` 对比指标卡的默认布局与样式。指标卡/对比指标卡布局类配置只通过这里统一配置,不放进 `themePatch`,也不提供单张卡片 props 透出。
81
+ - `themePatch`:仅用于主题已开放的主题配置,例如页面背景、标题样式、主题色等;不要把指标卡布局类配置放进 `themePatch`。
82
+
83
+ 数据格式不放在 `design-rule.json` 里,数值/百分比格式由字段类型与图表区域规则自动处理。
84
+
85
+ 示例:
86
+
87
+ ```json
88
+ {
89
+ "schemaVer": 1,
90
+ "componentDefaults": {
91
+ "SINGLE_VALUE": {
92
+ "singleValueSetting": {
93
+ "alignment": "left",
94
+ "fontStyle": {
95
+ "size": 28
96
+ }
97
+ }
98
+ },
99
+ "KPI_CARD": {
100
+ "secondKpiSetting": {
101
+ "valueAlign": "right"
102
+ }
103
+ }
104
+ },
105
+ "themePatch": {
106
+ "cardTitle": {
107
+ "style": {
108
+ "fontSize": 14
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ 非法字段会被忽略并打印 warning,不要依赖未支持的配置项。
@@ -0,0 +1,20 @@
1
+ ## Troubleshooting(常见错误 → 修复方法)
2
+
3
+ | 错误信息 | 原因 | 修复方法 |
4
+ |---|---|---|
5
+ | `zone metric: field count N exceeds max 1` | BASIC_COLUMN/BAR/LINE 只支持 1 个 metric | 改用 GROUPED_COLUMN/MULTI_LINE(支持多 metric),或拆成多个卡片 |
6
+ | `zone row: field count N exceeds max 1` | 组合图(*_WITH_LINE)row maxCount=1 | 减少 row 字段到 1 个,或改用非组合图类型 |
7
+ | `missing required zone: metric` | 卡片没有添加 metric 字段 | 调用 `.addMetric(f("字段名"))` |
8
+ | `calcField name 'X' conflicts with dataset physical field` | calcField 与数据集字段同名 | 重命名 calcField,如加 `_calc` 后缀 |
9
+ | `Calculated field missing name` | calcField 没有指定 name | 使用 `calcField("名称", "公式")` 格式 |
10
+ | `Field 'X' not found in dataset` | 字段名拼写错误或数据集中不存在 | 检查 schema.js 中的字段名,确保完全匹配 |
11
+ | `unknown chart type 'X'` | ChartType 枚举值错误 | 使用 `ChartType.XXX` 常量,不要手写字符串 |
12
+ | `auth: ...` / `401` | Token 过期或 profile 错误 | 运行 `guancli auth status`,必要时重新 `auth login` |
13
+ | `run scripts: ...ReferenceError` | JS 脚本中引用了未定义的变量 | 检查是否使用了 `new XxxBuilder()` 而非工厂函数 `createXxx()` |
14
+ | `placeCard index out of range` | placeCard 的 cardIndex 超出已注册卡片数量 | 检查 registerCard/registerTextCard 的调用顺序和总数 |
15
+ | `page has no cards` | page.js 中没有 placeCard | 确保 page.js 中为每个已注册的卡片调用了 placeCard |
16
+ | `selector not linked to any card` | selector 没有调用 linkTo/linkToAll | 添加 `.linkToAll()` 或 `.linkTo(cardIndex)` |
17
+ | `Theme: 简约 (source=fallback, built-in simple.json)` 但用户要求别的风格 | 偏好缺失 / themeId 在环境中不存在 / sync 失败 / keywords 没命中 | `theme list` 看候选 → `theme preference --theme-id ...` 或 `--keywords ... --sync`;联网命令注意 stderr 是否有 `theme: sync failed: ...` |
18
+ | publish 后 `.applied.json` 没更新 | 本次实际用的就是 skill 自带 `simple.json`(按设计 fallback 不写 applied) | 检查 stderr 的 `theme: ... falling back ...` 警告;确保对应 `themes/<id>.json` 存在或先 `theme sync` |
19
+ | `invalid themeId "..." (...)` | themeId 不是合法的单段文件名(见 `publish-and-constraints.md` 中的约束) | 使用 `theme list` 里出现的 id;避免 `/`、`\`、`..`;或用 `theme preference --keywords` |
20
+ | `theme: keywords "..." matched no theme` | 关键词与候选 themeName 没有公共子串 | `theme list` 核对候选名字;调整 keywords,或换成 `--theme-id` |
@@ -0,0 +1,205 @@
1
+ ## 验证规则
2
+
3
+ pack/publish 时分两层自动检查:
4
+
5
+ ### JS 层(脚本执行时,面向 AI 生成的脚本质量)
6
+
7
+ - **数据集绑定**:是否调用了 `.bindDataset()`,dsId 是否在 `defineDataset()` 中注册
8
+ - **Zone 配置**:必填 zone 是否有字段、字段数量是否超过 maxCount、是否使用了不支持的 zone
9
+ - **字段完整性**:fdId/name 非空、fdType 合法(FieldType 枚举)、field() 引用的字段名是否存在
10
+ - **聚合类型**:metric/compare/target zone 中非明细表图表必须有 aggrType;aggrType 值必须合法(AggrType 枚举)
11
+ - **明细表计算字段**:`DETAIL_TABLE` / `SCROLL_TABLE` 禁止 `calculationType: "aggregation"` / `"window"` 的 `calcField()`,并拦截明显的 `OVER(...)` 窗口子句;明细表只做行级展示,行级公式使用 `{ calculationType: "normal" }`,聚合需求优先改用非明细图表的原生指标 `aggrType`,复杂聚合改用非明细图表的 aggregation calcField 或 ETL 预计算。跨方言 SQL 聚合函数由 skill 生成规范约束,不在本地校验中维护函数名白/黑名单
12
+ - **排序**:sorting zone 中 sortType 必须是 ASC/DESC(使用 SortOrder 枚举)
13
+ - **重复检测**:同一 zone 中相同 fdId+alias+aggrType 组合会产生 warning
14
+ - **空卡片**:没有任何字段的卡片会报错
15
+ - **Page 布局**:cardIndex 越界、宽度超限(非精细 1-12,精细导出后 1-60)、溢出检测
16
+
17
+ ### Go 层(payload 生成后,面向后端导入兼容性)
18
+
19
+ - **结构完整性**:chartType 非空、dsId 非空、dsInfo 非空且含 dsId/columns、meta JSON 合法
20
+ - **meta 结构**:chartMain/summary 存在、zoneData/zoneInfo 中 zoneId 合法
21
+ - **字段合法性**:fdType/aggrType 值合法、metric zone 字段有 aggrType
22
+ - **Page 关联**:layout 中 card ID 引用合法、pageCardRels 引用合法
23
+ - **筛选器**:cdType=6 的卡片跳过 chartType/chartMain 检查,验证 source.field 存在
24
+
25
+ ### column zone 与 colorBy zone 的区别
26
+
27
+ **column zone**(`.addColumn()`):接受**维度字段**,用于按维度分组着色。例如"按邮寄方式分组堆叠":
28
+
29
+ ```javascript
30
+ createCard(ChartType.STACKED_COLUMN, "类别销售额")
31
+ .addRow(f("类别"))
32
+ .addColumn(f("邮寄方式")) // 维度分组 → 每种邮寄方式一个颜色
33
+ .addMetric(f("销售额", { aggrType: AggrType.SUM }))
34
+ ```
35
+
36
+ **colorBy zone**(`.addColorBy()`):接受**指标字段**,用于按指标值渐变着色。例如"按利润值深浅着色":
37
+
38
+ ```javascript
39
+ createCard(ChartType.BASIC_COLUMN, "类别销售额")
40
+ .addRow(f("类别"))
41
+ .addMetric(f("销售额", { aggrType: AggrType.SUM }))
42
+ .addColorBy(f("利润", { aggrType: AggrType.SUM })) // 指标值 → 颜色深浅
43
+ ```
44
+
45
+ 支持 column zone 的图表:`STACKED_COLUMN`、`GROUPED_COLUMN`、`GROUPED_BAR`、`STACKED_BAR`、`MULTI_LINE`、`STACKED_AREA` 等多指标图表。`BASIC_COLUMN`、`BASIC_BAR`、`BASIC_LINE`、`PIE` 不支持 column zone。
46
+
47
+ ### 特殊图表行为
48
+
49
+ - **DETAIL_TABLE / SCROLL_TABLE**:`.addColumn()` 自动重定向到 metric zone;metric/sorting zone 的 `needAggregation` 设为 false;不自动添加 aggrType
50
+ - **PIVOT_TABLE**:支持 row(行维度)+ column(列维度)+ metric(值)交叉分析
51
+ - **BASIC_MAP / WORLD_MAP**:地理维度字段放 `row` zone(1 个),指标放 `colorBy`(渐变着色)或 `metric` zone。后端验证规则:`rowSize == 1 && (colorBySize == 1 || metricSize == 1)`。**不要用** `addLocation()`(已废弃)
52
+ - **POINT_MAP / BUBBLE_MAP**:`row` zone 放地名维度(1 个),`metric` zone 放数值指标(POINT_MAP 最多 20 个,BUBBLE_MAP 无限制)。后端验证规则:`rowSize == 1 && metricSize >= 1`
53
+
54
+ ### 图表类型 Zone 配置速查表
55
+
56
+ `-1` 或 `∞` 表示不限数量。**必填** zone 标记 `*`。省略的 zone(filters/sorting/tooltip)所有图表都支持。
57
+
58
+ | ChartType | metric | row | column | colorBy | 其他 | 典型场景 |
59
+ |---|---|---|---|---|---|---|
60
+ | BASIC_COLUMN/BAR/LINE | 1* | ∞ | - | 1 | split(1) | 单指标柱/条/折线 |
61
+ | GROUPED_COLUMN/BAR | ∞* | ∞ | 1 | 1 | split(1) | 多指标分组柱/条 |
62
+ | STACKED_COLUMN/BAR | ∞* | ∞ | 1 | 1 | split(1) | 堆叠柱/条 |
63
+ | MULTI_LINE | ∞* | ∞ | 1 | 1 | split(1) | 多指标折线 |
64
+ | *_WITH_LINE (组合图) | ∞* | **1** | 1 | 1 | metricAdditional(20), split(1) | 柱+线组合。metric=柱形(主轴),metricAdditional=折线(副轴) |
65
+ | PIE / TREE_MAP | 1* | ∞ | - | 1 | - | 饼图/矩形树图 |
66
+ | KPI_CARD | 1* | - | - | - | compare(∞) | 单指标 KPI |
67
+ | PIVOT_TABLE | 124* | ∞ | ∞ | - | - | 交叉表 |
68
+ | DETAIL_TABLE | ∞* | - | - | - | - | 明细表 |
69
+ | SCROLL_TABLE | ∞* | ∞ | - | - | - | 滚动表 |
70
+ | BASIC_SCATTER_PLOT | 2* | 1 | - | 1 | sizeBy(1) | 散点图(X/Y 两个 metric) |
71
+ | BASIC_MAP | 1 | 1* | - | 1 | - | 中国地图(row 放地理维度) |
72
+ | FUNNEL | ∞* | 1 | - | - | - | 漏斗 |
73
+ | PROGRESS_BAR | 2* | 0-1 | - | - | - | 进度条,默认用两个 metric(当前值/目标值) |
74
+ | HEAT_MAP | 1* | 1* | 1* | - | - | 热力图(row+column+metric 都必填) |
75
+ | RADAR_LINE | ∞* | 1 | - | 1 | - | 雷达图 |
76
+ | SANKEY | 1 | ∞* | - | - | - | 桑基图 |
77
+
78
+ ### 图表选型补充(非通识部分)
79
+
80
+ 以下仅列出观远 BI 特有的、容易选错的图表区分规则:
81
+
82
+ | 场景 | 正确选择 | 避免混淆 |
83
+ |------|---------|---------|
84
+ | KPI 只展示一个数字 | `SINGLE_VALUE` | 不要用 `KPI_CARD` |
85
+ | KPI 需要同环比对比 | `KPI_CARD`(有 compare zone) | `SINGLE_VALUE` 无 compare zone |
86
+ | 折线图只有 1 个指标 | `BASIC_LINE` | 不要用 `MULTI_LINE` |
87
+ | 折线图有 2+ 指标 | `MULTI_LINE` | 不要用 `BASIC_LINE`(仅支持 1 metric + colorBy 拆分) |
88
+ | 明细逐行展示(不聚合) | `DETAIL_TABLE` | `PIVOT_TABLE` 会聚合 |
89
+ | 行列交叉汇总 | `PIVOT_TABLE`(有 row + column + metric 三个 zone) | `DETAIL_TABLE` 无 column zone |
90
+ | 按维度分组着色(如按地区着色) | 使用 `.addColumn(f("地区"))` | **不要用** `.addColorBy()`,colorBy 是按指标值渐变着色 |
91
+ | 分组+组成双维度对比 | `STACKED_SPLIT_COLUMN` | `GROUPED_COLUMN` 无堆积、`STACKED_COLUMN` 无分组 |
92
+ | 柱线组合图(主轴柱形+副轴折线) | `GROUPED_COLUMN_WITH_LINE`(metric=柱, metricAdditional=线) | 不要把所有指标都放 metric,副轴指标用 `.addMetricAdditional()` |
93
+ | 目标达成率 | `BULLET_COLUMN` / `BULLET_BAR` | 不要用普通柱形图叠加辅助线 |
94
+ | 占比分析类别 >5 | `TREE_MAP` 或 `RISING_SUN` | `PIE` 限 2-5 个分类,否则难以辨读 |
95
+ | 完成度/进度展示 | `KPI_CARD` + 数值格式 | `SOLID_GAUGE` 占空间大但信息密度低,优先用指标卡 |
96
+ | 中国省/市级地图 | `BASIC_MAP`(内置 Highmaps) | **不要用自定义图表**,内置地图自带 GeoJSON、地名编码匹配 |
97
+ | 全球国家级地图 | `WORLD_MAP`(内置 Highmaps) | 同上 |
98
+ | 标记地图(地名+多指标) | `POINT_MAP`(标记)或 `BUBBLE_MAP`(气泡) | row zone 放 1 个地名维度,metric zone 放多个指标 |
99
+ | 美国各州/加拿大等非中国区域详细地图 | 自定义图表(Vega-Lite geoshape) | 内置 `WORLD_MAP` 只精确到国家级,州/省级需自定义 |
100
+
101
+ ### 拆分图(Trellis/Facet)
102
+
103
+ 将一个图表按维度值拆分成多个小图表并排展示。使用 `addSplit(field)` 指定拆分维度,可选 `setSplitSetting` 控制行列数:
104
+
105
+ ```javascript
106
+ var card = createCard(ChartType.STACKED_AREA, "各类别月销售趋势")
107
+ .bindDataset(DS)
108
+ .addRow(f("订单日期", { granularity: "MONTH" }))
109
+ .addMetric(f("销售额", { aggrType: AggrType.SUM }))
110
+ .addSplit(f("产品类别"))
111
+ .setSplitSetting({ rows: 2, columns: 3 });
112
+ ```
113
+
114
+ 不调用 `setSplitSetting` 时默认 3 行 3 列。支持拆分的图表类型:`BASIC_COLUMN`、`BASIC_BAR`、`BASIC_LINE`、`STACKED_AREA`、`GROUPED_COLUMN`、`STACKED_COLUMN`、`MULTI_LINE` 等所有带 split zone 的图表。
115
+
116
+ ### 地图图表用法
117
+
118
+ **BASIC_MAP(中国行政地图)**:内置 Highmaps,自动加载中国省市 GeoJSON。地理维度字段放 `row` zone,指标放 `colorBy`(渐变着色)或 `metric` zone。BI 后端通过 `GeoCodeUtil` 自动将地名(如"北京市""浙江省")映射为行政区编码。
119
+
120
+ ```javascript
121
+ var card = createCard(ChartType.BASIC_MAP, "各省利润率")
122
+ .bindDataset(DS)
123
+ .addRow(f("省/自治区"))
124
+ .addColorBy(calcField("利润率", "SUM([利润])/SUM([销售额])", {
125
+ numberFormat: NumberFormat.percentage(1)
126
+ }));
127
+
128
+ registerCard(card.build());
129
+ ```
130
+
131
+ 如果数据集中已有利润率字段,可直接引用(无需 calcField):
132
+ ```javascript
133
+ .addColorBy(f("利润率", { aggrType: AggrType.AVG }));
134
+ ```
135
+
136
+ **自定义 colorBy 渐变色**:默认为主题色单色渐变。要设置自定义渐变色,使用 `setColorByColors`,支持两种方式:
137
+
138
+ 1. **预设色板名称**(推荐):`setColorByColors(ColorByPreset.RedGreen)`
139
+ 2. **自定义 hex 颜色**:`setColorByColors({ startColor: "#hex", endColor: "#hex", middleColor?: "#hex", steps?: 5 })`
140
+
141
+ 可用 `ColorByPreset` 值:
142
+
143
+ | 预设名称 | 类型 | 颜色 |
144
+ |---|---|---|
145
+ | `TrafficLight` | 发散 | `#F54F2A` → `#F1DB8B` → `#00A376` |
146
+ | `RedGreen` | 发散 | `#EC4F4F` → `#E0ECF0` → `#009688` |
147
+ | `HeatCold` | 发散 | `#0E64A8` → `#F7F6F6` → `#C53D18` |
148
+ | `Blue` | 顺序 | `#A3CCF8` → `#547CCE` |
149
+ | `Golden` | 顺序 | `#FADA61` → `#F76B1C` |
150
+ | `Green` | 顺序 | `#E0E6B7` → `#0D9347` |
151
+ | `Grey` | 顺序 | `#CBD3DA` → `#5B6A7C` |
152
+ | `Pink` | 顺序 | `#FFA9A9` → `#EF6065` |
153
+ | `Purple` | 顺序 | `#D3C3DB` → `#7260AF` |
154
+
155
+ ```javascript
156
+ // 预设色板
157
+ var card = createCard(ChartType.BASIC_MAP, "利润率地图")
158
+ .bindDataset(DS)
159
+ .addRow(f("省/自治区"))
160
+ .addColorBy(calcField("利润率", "SUM([利润])/SUM([销售额])", {
161
+ numberFormat: NumberFormat.percentage(1)
162
+ }))
163
+ .setColorByColors(ColorByPreset.RedGreen);
164
+
165
+ // 自定义 hex 颜色
166
+ .setColorByColors({ startColor: "#EC4F4F", middleColor: "#E0ECF0", endColor: "#009688", steps: 5 });
167
+ ```
168
+
169
+ **WORLD_MAP(世界地图)**:
170
+
171
+ ```javascript
172
+ var card = createCard(ChartType.WORLD_MAP, "全球销售分布")
173
+ .bindDataset(DS)
174
+ .addRow(f("国家"))
175
+ .addColorBy(f("销售额", { aggrType: AggrType.SUM }));
176
+
177
+ registerCard(card.build());
178
+ ```
179
+
180
+ **POINT_MAP(标记地图)**:row zone 放地名维度(1 个),metric zone 放指标(支持多个,最多 20 个)。
181
+
182
+ ```javascript
183
+ var card = createCard(ChartType.POINT_MAP, "各省门店数")
184
+ .bindDataset(DS)
185
+ .addRow(f("省/自治区"))
186
+ .addMetric(f("门店数", { aggrType: AggrType.SUM }))
187
+ .addMetric(f("销售额", { aggrType: AggrType.SUM }));
188
+
189
+ registerCard(card.build());
190
+ ```
191
+
192
+ **仪表板编排**:KPI 指标卡放最前,趋势/对比图表居中,明细表放最后。每页主 KPI 不超过 5-7 个。
193
+
194
+ ### Field 对象
195
+
196
+ 通过 `f("字段名")` 或 `field(DS, "字段名")` 从 schema 自动构造,无需手写 fdId/fdType。可通过 overrides 叠加属性:
197
+
198
+ ```javascript
199
+ f("营收", {
200
+ aggrType: AggrType.SUM,
201
+ alias: "总营收",
202
+ numberFormat: NumberFormat.currency("¥"),
203
+ sortType: SortOrder.DESC
204
+ })
205
+ ```