diff --git a/skills/vchart-development-assistant/SKILL.md b/skills/vchart-development-assistant/SKILL.md index e5702cf3fd..378824164b 100644 --- a/skills/vchart-development-assistant/SKILL.md +++ b/skills/vchart-development-assistant/SKILL.md @@ -1,6 +1,6 @@ --- name: vchart-development-assistant -description: VChart图表库专家助手,支持问题诊断、配置生成、图片/Figma设计稿还原等场景,基于结构化知识库提供精确的图表开发解决方案 +description: VChart图表库专家助手,擅长创建、配置和调试VChart图表。当用户需要:生成柱状图/折线图/饼图等图表;修复图表不显示/点击事件不触发等问题;从图片或Figma设计稿还原图表样式;实现点击获取数据/数据动态更新/图表联动/导出图片/主题切换等交互功能;配置图例/坐标轴/标签/tooltip等组件时使用。即使用户没有提到"技能"或"VChart"这个词,只要涉及图表开发就触发。 --- # VChart 图表开发助手 Skill @@ -29,6 +29,7 @@ description: VChart图表库专家助手,支持问题诊断、配置生成、 | **类型详情** | `references/type-details/*.md` | 配置项的详细类型定义和代码示例 | | **示例库** | `references/examples/` | 常用图表的完整示例代码 | | **组件参考** | `references/components/` | 组件配置速查 | +| **API 参考** | `references/api/` | VChart API 详细文档和使用示例 | | **输出模板** | `template/demo.html` | 生成可运行 HTML 示例的标准模板(纯 JS) | | **诊断模板** | `template/diagnosis.html` | 问题诊断 HTML 模板(纯 JS) | | **React 诊断** | `template/diagnosis-react.html` | React-VChart 问题诊断 HTML 模板 | @@ -112,9 +113,39 @@ python3 scripts/generate_diagnosis_react_html.py \ - **普通图片模式**:从截图推断样式,中等精确度 - **Figma 设计稿模式**:提取精确样式值,高精确度 +#### API 交互需求识别(可嵌入任何场景) + +**重要**:API 交互不是独立场景,而是可以嵌入任何场景的**能力模块**。 + +**识别信号**(在场景1/2/3中检测): + +- 用户询问"点击后..."、"hover 时..."、"选中后..." +- 用户询问"动态更新"、"实时刷新"、"定时更新" +- 用户询问"高亮"、"联动"、"外部控制" +- 用户询问"导出"、"下载图片" +- 用户询问"切换主题"、"深色模式" +- 用户需要事件监听、状态管理、交互控制等功能 + +**处理原则**: + +- 在**场景二(配置生成)**中检测到 API 需求 -> 同时生成 Spec + API 代码 +- 在**场景三(视觉还原)**中检测到 API 需求 -> 同时还原样式 + API 代码 +- 在**场景一(问题诊断)**中检测到 API 需求 -> 诊断 Spec + API 代码问题 + +**API 能力分类**: + +| 能力类型 | 典型需求 | API 文档 | +|---------|---------|---------| +| 事件监听 | 点击响应、hover 效果 | `references/api/event-api.md` | +| 数据操作 | 动态更新、实时刷新 | `references/api/data-api.md` | +| 状态管理 | 高亮、选中 | `references/api/state-api.md` | +| 交互控制 | 手动触发 tooltip | `references/api/interaction-api.md` | +| 导出功能 | 下载图片 | `references/api/export-api.md` | +| 主题切换 | 深色模式 | `references/api/theme-api.md` | + --- -### 对话中的场景动态切换 ⚠️ +### 对话中的场景动态切换 **核心原则**:根据最新用户输入重新评估场景,灵活切换。 @@ -128,6 +159,18 @@ python3 scripts/generate_diagnosis_react_html.py \ | 任何 | 提供新图片/截图 | 场景3 | 新的视觉还原 | | 任何 | "重新生成"/全新需求 | 场景2 | 新的完整生成 | +#### API 能力嵌入规则 + +**在当前场景中检测到 API 需求时,不切换场景,而是增强输出**: + +| 当前场景 | 检测到 API 需求 | 增强动作 | +| -------- | ------------------- | ------------------------------ | +| 场景2 | "点击后获取数据" | 输出 Spec + 事件监听代码 | +| 场景2 | "动态更新数据" | 输出 Spec + 数据更新函数 | +| 场景3 | "联动高亮" | 输出 Spec + 状态管理代码 | +| 场景3 | "导出图片" | 输出 Spec + 导出按钮和函数 | +| 场景1 | API 代码报错 | 同时诊断 Spec 和 API 代码 | + #### 切换要点 - 保留之前代码作为上下文基础 @@ -136,7 +179,7 @@ python3 scripts/generate_diagnosis_react_html.py \ --- -## 生成后自检与问题预警 🔍 +## 生成后自检与问题预警 生成代码后立即检查以下高频错误点: @@ -149,15 +192,15 @@ python3 scripts/generate_diagnosis_react_html.py \ ### 主动提示 -发现风险时告知用户: +发现风险��告知用户: ``` -✅ 已生成配置 -⚠️ 请确认数据字段名与 xField/yField 一致,否则图表可能无法显示 -💡 如遇问题请反馈,我会立即诊断 +已生成配置 +请确认数据字段名与 xField/yField 一致,否则图表可能无法显示 +如遇问题请反馈,我会立即诊断 ``` -### 问题反馈关键词 → 立即切换场景1 +### 问题反馈关键词 -> 立即切换场景1 - "报错"/"error"/"不工作"/"失败" - "没显示"/"空白"/"不出来" @@ -178,7 +221,7 @@ python3 scripts/generate_diagnosis_react_html.py \ | **配置生成** | [workflows/scenario-2-generation.md](workflows/scenario-2-generation.md) | 完整生成、增量生成、意图识别 | | **视觉还原** | [workflows/scenario-3-image-replication.md](workflows/scenario-3-image-replication.md) | 图片分析、Figma 精确还原、样式匹配 | -**⚠️ 注意**:场景不是固定的!在对话中随时根据用户最新输入切换场景。参见上文"对话中的场景动态切换"部分。 +**注意**:场景不是固定的!在对话中随时根据用户最新输入切换场景。API 交互作为能力模块嵌入任何场景中。 --- @@ -189,19 +232,19 @@ python3 scripts/generate_diagnosis_react_html.py \ 当需要查找配置项时,按以下顺序查询: ``` -用户意图 → topkey/*.json → type-meta/*.json → type-details/*.md +用户意图 -> topkey/*.json -> type-meta/*.json -> type-details/*.md ``` **查询流程**: -1. **意图识别**:用户说"加个标签" → 查询 `references/topkey/[图表类型].json` → 找到 `label` 配置项 -2. **结构查询**:需要 label 的属性 → 查询 `references/type-meta/[图表类型].json` → 找到 `label` 的类型定义 -3. **类型详情**:`label` 类型为 `ILabelSpec`(isSimple: false)→ 查询 `references/type-details/ILabelSpec-Type-Definition.md` +1. **意图识别**:用户说"加个标签" -> 查询 `references/topkey/[图表类型].json` -> 找到 `label` 配置项 +2. **结构查询**:需要 label 的属性 -> 查询 `references/type-meta/[图表类型].json` -> 找到 `label` 的类型定义 +3. **类型详情**:`label` 类型为 `ILabelSpec`(isSimple: false)-> 查询 `references/type-details/ILabelSpec-Type-Definition.md` > **常用配置项索引**: > -> - 通用配置(标题、图例、tooltip等)→ `references/topkey/all_common.json` -> - 图表专属配置 → `references/topkey/[图表类型].json` +> - 通用配置(标题、图例、tooltip等)-> `references/topkey/all_common.json` +> - 图表专属配置 -> `references/topkey/[图表类型].json` ### 类型定义查询 @@ -213,6 +256,31 @@ python3 scripts/generate_diagnosis_react_html.py \ | `false` | `ILabelSpec`, `IData` | 查询 `references/type-details/[类型名]-Type-Definition.md` | | 函数类型 | 回调函数 | 查询 `references/type-details/FunctionType-Type-Definition.md` | +### API 查询 + +当用户需要实现交互功能时,查询 API 文档: + +**快速索引**:`references/api/API_INDEX.md` + +| 用户需求 | 查询文档 | +| ---------------- | -------------------------------------- | +| 更新图表数据 | `references/api/data-api.md` | +| 响应点击/hover | `references/api/event-api.md` | +| 高亮/选中图元 | `references/api/state-api.md` | +| 手动触发tooltip | `references/api/interaction-api.md` | +| 切换主题 | `references/api/theme-api.md` | +| 导出图片 | `references/api/export-api.md` | +| 控制动画 | `references/api/animation-api.md` | +| 坐标位置转换 | `references/api/coordinate-api.md` | +| 控制图例 | `references/api/legend-api.md` | +| 调整尺寸 | `references/api/layout-api.md` | + +**API 查询流程**: + +1. **需求识别**:用户说"点击图表获取数据" -> 识别为事件监听需求 +2. **文档查找**:查询 `references/api/event-api.md` +3. **代码生成**:根据文档示例生成完整代码 + --- ## 通用查询策略 @@ -220,7 +288,10 @@ python3 scripts/generate_diagnosis_react_html.py \ ### 查询优先级 ``` -1. 本地知识库(references/topkey/ → references/type-meta/ → references/type-details/ → references/examples/ → references/faq) +1. 本地知识库 + - 配置相关:references/topkey/ -> references/type-meta/ -> references/type-details/ + - 交互相关:references/api/ + - 示例参考:references/examples/ 2. 在线文档(仅当本地不足时) ``` @@ -240,7 +311,7 @@ python3 scripts/generate_diagnosis_react_html.py \ 生成的代码应: 1. **类型正确**:属性值符合 `type-details` 中的类型定义 -2. **字段匹配**:数据字段名与 xField/yField 等严格对应(⚠️ 最常见错误源) +2. **字段匹配**:数据字段名与 xField/yField 等严格对应(最常见错误源) 3. **必填完整**:包含 `type-meta` 中 `required: true` 的所有字段 4. **注释清晰**:关键配置项添加注释说明 5. **版本兼容**:使用 VChart 2.0.0+ 的 API @@ -259,6 +330,7 @@ python3 scripts/generate_diagnosis_react_html.py \ | 配置生成 / 视觉还原 | `template/demo.html` | 替换标题/描述占位;替换 `{{SPEC_CODE}}` 为完整 spec | | 问题诊断(纯 JS) | `template/diagnosis.html` | 嵌入用户代码/问题点,并给出修复后 spec | | 问题诊断(React) | `template/diagnosis-react.html` | React 场景输出 React 诊断 HTML | +| API 交互增强 | `template/demo.html` | 包含 spec + 事件监听 + 交互函数 + 控制按钮 | **输出校验清单**(回答时提醒用户可直接保存为 .html 打开): diff --git a/skills/vchart-development-assistant/references/api/API_INDEX.md b/skills/vchart-development-assistant/references/api/API_INDEX.md new file mode 100644 index 0000000000..b747650e56 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/API_INDEX.md @@ -0,0 +1,287 @@ +# VChart API 索引 + +## API 分类概览 + +VChart API 分为以下几大类别: + +| 分类 | 用途 | 常见场景 | +|------|------|---------| +| **数据操作** | 动态更新图表数据 | 数据刷新、实时数据、数据筛选 | +| **图表更新** | 动态修改图表配置 | 切换图表类型、修改样式、更新标题 | +| **状态管理** | 控制图元状态 | 高亮、选中、hover 效果 | +| **事件监听** | 响应用户交互 | 点击、hover、图例筛选 | +| **主题切换** | 动态切换主题 | 深色模式、品牌主题 | +| **交互控制** | 手动触发交互 | 显示 tooltip、crosshair、dimension | +| **导出功能** | 导出图表 | 图片导出、获取 dataURL | +| **尺寸调整** | 响应式布局 | 窗口 resize、容器尺寸变化 | +| **图例操作** | 控制图例状态 | 图例选中、获取图例数据 | +| **动画控制** | 控制动画播放 | 暂停、恢复、停止动画 | +| **坐标转换** | 数据与坐标转换 | 自定义标注、点击位置转换 | +| **实例获取** | 获取内部实例 | 获取 chart、stage、canvas | + +--- + +## 快速查找 API + +### 按用户需求查找 + +| 用户需求 | 推荐 API | 文档链接 | +|---------|---------|---------| +| 更新图表数据 | `updateData` / `updateDataSync` | [data-api.md](data-api.md) | +| 切换图表类型 | `updateSpec` / `updateSpecSync` | [spec-api.md](spec-api.md) | +| 高亮某个数据 | `setHovered` / `setSelected` | [state-api.md](state-api.md) | +| 响应点击事件 | `on('click')` | [event-api.md](event-api.md) | +| 切换深色模式 | `setCurrentTheme` | [theme-api.md](theme-api.md) | +| 手动显示 tooltip | `showTooltip` / `hideTooltip` | [interaction-api.md](interaction-api.md) | +| 导出图片 | `exportImg` / `getDataURL` | [export-api.md](export-api.md) | +| 窗口 resize | `resize` | [layout-api.md](layout-api.md) | +| 控制图例选中 | `setLegendSelectedDataById` | [legend-api.md](legend-api.md) | +| 暂停动画 | `pauseAnimation` / `resumeAnimation` | [animation-api.md](animation-api.md) | +| 数据坐标转换 | `convertDatumToPosition` | [coordinate-api.md](coordinate-api.md) | + +--- + +## 快速查找 API + +### 按用户需求查找 + +| 用户需求 | 推荐 API | 文档链接 | +|---------|---------|---------| +| 更新图表数据 | `updateData` / `updateDataSync` | [data-api.md](data-api.md) | +| 切换图表类型 | `updateSpec` / `updateSpecSync` | [spec-api.md](spec-api.md) | +| 高亮某个数据 | `setHovered` / `setSelected` | [state-api.md](state-api.md) | +| 响应点击事件 | `on('click')` | [event-api.md](event-api.md) | +| 切换深色模式 | `setCurrentTheme` | [theme-api.md](theme-api.md) | +| 手动显示 tooltip | `showTooltip` / `hideTooltip` | [tooltip-api.md](tooltip-api.md) | +| 导出图片 | `exportImg` / `getDataURL` | [export-api.md](export-api.md) | +| 窗口 resize | `resize` | [layout-api.md](layout-api.md) | +| 控制图例选中 | `setLegendSelectedDataById` | [legend-api.md](legend-api.md) | +| 暂停动画 | `pauseAnimation` / `resumeAnimation` | [animation-api.md](animation-api.md) | +| 数据坐标转换 | `convertDatumToPosition` | [coordinate-api.md](coordinate-api.md) | + +--- + +## API 详细文档 + +### 核心数据操作 API + +**文档**: [data-api.md](data-api.md) + +| API | 同步/异步 | 用途 | +|-----|----------|------| +| `updateData` | 异步 | 更新指定数据集 | +| `updateDataSync` | 同步 | 同步更新数据集 | +| `updateDataInBatches` | 异步 | 批量更新多个数据集 | +| `updateFullData` | 异步 | 更新完整数据对象 | +| `updateFullDataSync` | 同步 | 同步更新完整数据 | + +### Spec 配置更新 API + +**文档**: [spec-api.md](spec-api.md) + +| API | 同步/异步 | 用途 | +|-----|----------|------| +| `updateSpec` | 异步 | 更新完整 spec | +| `updateSpecSync` | 同步 | 同步更新 spec | +| `updateModelSpec` | 异步 | 更新模块 spec | +| `updateModelSpecSync` | 同步 | 同步更新模块 spec | + +### 状态管理 API + +**文档**: [state-api.md](state-api.md) + +| API | 用途 | +|-----|------| +| `setHovered` | 设置 hover 状态 | +| `setSelected` | 设置选中状态 | +| `updateState` | 更新自定义状态 | +| `clearHovered` | 清除 hover 状态 | +| `clearSelected` | 清除选中状态 | +| `clearState` | 清除指定状态 | + +### 事件监听 API + +**文档**: [event-api.md](event-api.md) + +| API | 用途 | +|-----|------| +| `on` | 添加事件监听 | +| `off` | 移除事件监听 | + +### 主题切换 API + +**文档**: [theme-api.md](theme-api.md) + +| API | 用途 | +|-----|------| +| `setCurrentTheme` | 设置当前主题 | +| `getCurrentTheme` | 获取当前主题 | +| `getCurrentThemeName` | 获取当前主题名称 | + +### 交互控制 API + +**文档**: [interaction-api.md](interaction-api.md) + +| API | 用途 | +|-----|------| +| `showTooltip` | 手动显示 tooltip | +| `hideTooltip` | 隐藏 tooltip | +| `setDimensionIndex` | 触发 dimension 交互 | +| `disableTooltip` | 禁用/启用 tooltip | +| `disableCrossHair` | 禁用/启用 crosshair | +| `disableDimensionHoverEvent` | 禁用/启用 dimension hover | + +### 导出功能 API + +**文档**: [export-api.md](export-api.md) + +| API | 用途 | +|-----|------| +| `exportImg` | 导出图片文件 | +| `getDataURL` | 获取 dataURL | +| `exportCanvas` | 导出 canvas 元素 | +| `getImageBuffer` | 获取图片 buffer(Node.js) | + +### 图例操作 API + +**文档**: [legend-api.md](legend-api.md) + +| API | 用途 | +|-----|------| +| `setLegendSelectedDataById` | 设置图例选中项 | +| `setLegendSelectedDataByIndex` | 按索引设置图例选中项 | +| `getLegendSelectedDataById` | 获取图例选中项 | +| `getLegendDataById` | 获取图例数据 | + +### 动画控制 API + +**文档**: [animation-api.md](animation-api.md) + +| API | 用途 | +|-----|------| +| `pauseAnimation` | 暂停动画 | +| `resumeAnimation` | 恢复动画 | +| `stopAnimation` | 停止动画 | +| `isAnimationEnable` | 检查动画是否启用 | + +### 坐标转换 API + +**文档**: [coordinate-api.md](coordinate-api.md) + +| API | 用途 | +|-----|------| +| `convertDatumToPosition` | 数据转换为坐标位置 | +| `convertValueToPosition` | 数值转换为坐标位置 | + +### 布局与尺寸 API + +**文档**: [layout-api.md](layout-api.md) + +| API | 用途 | +|-----|------| +| `resize` | 调整图表尺寸 | +| `updateViewBox` | 更新绘制区域 | +| `setLayout` | 设置自定义布局 | +| `reLayout` | 强制重新布局 | + +--- + +## API 使用模式 + +### 同步 vs 异步选择 + +**推荐使用异步 API**(默认): +```javascript +// ✅ 推荐:异步 API +await vchart.updateData('id', newData); +await vchart.updateSpec(newSpec); +await vchart.resize(800, 600); +``` + +**使用同步 API 的场景**: +```javascript +// 适用于需要立即获取结果的场景 +vchart.updateDataSync('id', newData); +const position = vchart.convertDatumToPosition(datum); +``` + +### 链式调用 + +大多数 API 返回 VChart 实例,支持链式调用: +```javascript +vchart + .updateDataSync('data', newData) + .setHovered(datum) + .showTooltip(datum, options); +``` + +### 事件监听模式 + +```javascript +// 添加事件监听 +vchart.on('click', (params) => { + console.log('点击了图表', params); +}); + +// 带查询条件的事件监听 +vchart.on('click', { seriesId: 'series0' }, (params) => { + console.log('点击了指定系列', params); +}); +``` + +--- + +## API 最佳实践 + +### 1. 数据更新场景选择 + +| 场景 | 推荐 API | 原因 | +|------|---------|------| +| 单个数据集更新 | `updateData` | 简单高效 | +| 多个数据集同时更新 | `updateDataInBatches` | 减少渲染次数 | +| 完整数据替换 | `updateFullData` | 更清晰的数据结构 | +| 实时数据刷新 | `updateDataSync` | 同步更新避免闪烁 | + +### 2. Spec 更新 vs 数据更新 + +- **仅数据变化**:使用 `updateData` 系列 API +- **配置变化**:使用 `updateSpec` 系列 API +- **频繁更新**:优先考虑数据更新,性能更好 + +### 3. 状态管理时机 + +- **用户交互触发**:在事件回调中使用 `setHovered`/`setSelected` +- **程序控制**:在数据更新后手动设置状态 +- **清除状态**:数据变化前调用 `clearHovered`/`clearSelected` + +--- + +## 常见问题 + +### Q1: updateData 和 updateSpec 的区别? + +**updateData**:仅更新数据,保留其他配置,性能更好。 +**updateSpec**:更新完整配置,包括数据、样式、组件等。 + +### Q2: 何时使用同步 vs 异步 API? + +- **异步 API**:大多数场景,不阻塞主线程 +- **同步 API**:需要立即获取结果、调试场景 + +### Q3: 如何实现图表的响应式布局? + +```javascript +window.addEventListener('resize', () => { + const width = container.clientWidth; + const height = container.clientHeight; + vchart.resize(width, height); +}); +``` + +--- + +## 参考资源 + +- **官方 API 文档**: https://visactor.com/vchart/api/API/vchart +- **事件文档**: [event-api.md](event-api.md) +- **示例代码**: GitHub `docs/assets/examples/` \ No newline at end of file diff --git a/skills/vchart-development-assistant/references/api/animation-api.md b/skills/vchart-development-assistant/references/api/animation-api.md new file mode 100644 index 0000000000..226f3c0710 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/animation-api.md @@ -0,0 +1,221 @@ +# 动画控制 API + +## 概述 + +动画控制 API 用于控制图表动画的播放、暂停和停止。 + +--- + +## API 列表 + +### isAnimationEnable + +**检查动画是否启用** + +```typescript +isAnimationEnable(): boolean; +``` + +**返回值**:动画是否启用 + +**使用示例**: + +```javascript +if (vchart.isAnimationEnable()) { + console.log('动画已启用'); +} +``` + +--- + +### pauseAnimation + +**暂停动画** + +```typescript +pauseAnimation(): void; +``` + +**使用示例**: + +```javascript +vchart.pauseAnimation(); +``` + +--- + +### resumeAnimation + +**恢复动画** + +```typescript +resumeAnimation(): void; +``` + +**使用示例**: + +```javascript +vchart.resumeAnimation(); +``` + +--- + +### stopAnimation + +**停止动画** + +```typescript +stopAnimation(): void; +``` + +**使用示例**: + +```javascript +vchart.stopAnimation(); +``` + +--- + +## 使用场景 + +### 场景 1: 数据更新时跳过动画 + +```javascript +async function quickUpdate() { + // 暂停动画 + vchart.pauseAnimation(); + + // 更新数据 + await vchart.updateData('data', newData); + + // 停止动画(不播放剩余动画) + vchart.stopAnimation(); +} +``` + +### 场景 2: 用户控制动画播放 + +```javascript +let isPaused = false; + +document.getElementById('toggle-animation').addEventListener('click', () => { + if (isPaused) { + vchart.resumeAnimation(); + } else { + vchart.pauseAnimation(); + } + isPaused = !isPaused; +}); +``` + +### 场景 3: 动画完成后执行操作 + +```javascript +vchart.on('animationFinished', () => { + console.log('动画播放完成'); + // 显示操作提示 + showTip('可以点击图表进行交互'); +}); +``` + +--- + +## 动画配置 + +### Spec 中配置动画 + +```javascript +const spec = { + type: 'bar', + data: [{ id: 'data', values: [...] }], + xField: 'category', + yField: 'value', + + // 动画配置 + animation: true, // 启用动画 + animationAppear: { + duration: 1000, + easing: 'cubicOut' + }, + animationEnter: { + duration: 500, + easing: 'linear' + }, + animationExit: { + duration: 300 + }, + animationUpdate: { + duration: 500 + } +}; +``` + +### 关闭动画 + +```javascript +// 方式 1: spec 中关闭 +const spec = { + type: 'bar', + animation: false, + // ... +}; + +// 方式 2: 动态停止 +vchart.stopAnimation(); +``` + +--- + +## 完整示例 + +```html + + + + + + +
+ +
+ + + + +
+ + + + +``` diff --git a/skills/vchart-development-assistant/references/api/coordinate-api.md b/skills/vchart-development-assistant/references/api/coordinate-api.md new file mode 100644 index 0000000000..0e8be121a3 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/coordinate-api.md @@ -0,0 +1,288 @@ +# 坐标转换 API + +## 概述 + +坐标转换 API 用于在数据值和画布坐标之间进行转换,常用于自定义标注、点击位置转换等场景。 + +--- + +## API 列表 + +### convertDatumToPosition + +**将数据转换为坐标位置** + +```typescript +convertDatumToPosition( + datum: Datum, + dataLinkInfo?: DataLinkSeries, + isRelativeToCanvas?: boolean, + checkInViewData?: boolean +): IPoint | null; +``` + +**参数**: +- `datum`: 数据对象 +- `dataLinkInfo`: 关联的系列信息(seriesId 或 seriesIndex),默认 { seriesIndex: 0 } +- `isRelativeToCanvas`: 是否相对画布坐标,默认 false +- `checkInViewData`: 是否检查数据是否在视图中 + +**返回值**:坐标点 `{ x: number, y: number }` 或 null + +**使用示例**: + +```javascript +// 获取数据点在图表中的位置 +const position = vchart.convertDatumToPosition({ + category: 'A', + value: 20 +}); +console.log(position); // { x: 100, y: 200 } +``` + +--- + +### convertValueToPosition + +**将数值转换为坐标位置** + +```typescript +// 单值模式 - 用于轴转换 +convertValueToPosition( + value: StringOrNumber, + dataLinkInfo: DataLinkAxis, + isRelativeToCanvas?: boolean +): number | null; + +// 双值模式 - 用于坐标系转换 +convertValueToPosition( + value: [StringOrNumber, StringOrNumber], + dataLinkInfo: DataLinkSeries, + isRelativeToCanvas?: boolean +): IPoint | null; +``` + +**参数**: +- `value`: 数值或数值对 +- `dataLinkInfo`: 关联的轴或系列信息 +- `isRelativeToCanvas`: 是否相对画布坐标 + +**使用示例**: + +```javascript +// 将 X 轴值转换为坐标 +const x = vchart.convertValueToPosition('A', { axisIndex: 0 }); + +// 将 Y 轴值转换为坐标 +const y = vchart.convertValueToPosition(50, { axisIndex: 1 }); + +// 将坐标点值转换为位置 +const point = vchart.convertValueToPosition(['A', 50], { seriesIndex: 0 }); +``` + +--- + +## 类型定义 + +### DataLinkSeries + +```typescript +type DataLinkSeries = { + seriesId?: StringOrNumber; + seriesIndex?: number; +}; +``` + +### DataLinkAxis + +```typescript +type DataLinkAxis = { + axisId?: StringOrNumber; + axisIndex?: number; +}; +``` + +--- + +## 使用场景 + +### 场景 1: 在数据点位置添加自定义标注 + +```javascript +// 获取数据点位置并添加自定义元素 +const data = { category: 'A', value: 30 }; +const position = vchart.convertDatumToPosition(data, { seriesIndex: 0 }, true); + +if (position) { + // 创建自定义标注元素 + const marker = document.createElement('div'); + marker.className = 'custom-marker'; + marker.style.left = `${position.x}px`; + marker.style.top = `${position.y}px`; + container.appendChild(marker); +} +``` + +### 场景 2: 点击位置转换为数据值 + +```javascript +vchart.on('click', (params) => { + const { x, y } = params.event; + + // 根据点击位置获取数据 + // 需要反向计算(根据具体需求实现) +}); +``` + +### 场景 3: 多图表联动定位 + +```javascript +// 图表1点击时,在图表2对应位置显示标记 +chart1.on('click', { level: 'mark' }, (params) => { + if (params.datum) { + // 获取图表1中的位置 + const pos1 = chart1.convertDatumToPosition(params.datum, {}, true); + + // 获取图表2中的对应数据位置 + const pos2 = chart2.convertDatumToPosition(params.datum, {}, true); + + // 在两个图表中显示联动标记 + showLinkMarker(pos1, pos2); + } +}); +``` + +### 场景 4: 绘制自定义连线 + +```javascript +// 连接两个数据点 +function drawConnection(datum1, datum2) { + const pos1 = vchart.convertDatumToPosition(datum1, {}, true); + const pos2 = vchart.convertDatumToPosition(datum2, {}, true); + + if (pos1 && pos2) { + const canvas = vchart.getCanvas(); + const ctx = canvas.getContext('2d'); + ctx.beginPath(); + ctx.moveTo(pos1.x, pos1.y); + ctx.lineTo(pos2.x, pos2.y); + ctx.stroke(); + } +} +``` + +--- + +## 注意事项 + +### 1. 坐标系类型 + +不同图表类型的坐标系不同: +- **直角坐标系**(柱状图、折线图等):返回 { x, y } +- **极坐标系**(饼图、雷达图等):返回极坐标位置 +- **地理坐标系**(地图):返回投影后的坐标 + +### 2. 数据匹配 + +传入的 datum 需要包含足够的字段来匹配数据: + +```javascript +// ❌ 可能失败 - 字段不完整 +vchart.convertDatumToPosition({ category: 'A' }); + +// ✅ 推荐 - 包含完整字段 +vchart.convertDatumToPosition({ category: 'A', value: 20, group: 'X' }); +``` + +### 3. 渲染时机 + +必须在图表渲染完成后调用: + +```javascript +// ❌ 错误 - 渲染前调用 +const vchart = new VChart(spec, { dom }); +const pos = vchart.convertDatumToPosition(data); // null + +// ✅ 正确 - 渲染后调用 +await vchart.renderAsync(); +const pos = vchart.convertDatumToPosition(data); // 正确 +``` + +--- + +## 完整示例 + +```html + + + + + + + +
+
+
+ +
+ + + +
+ + + + +``` diff --git a/skills/vchart-development-assistant/references/api/data-api.md b/skills/vchart-development-assistant/references/api/data-api.md new file mode 100644 index 0000000000..f82bf801d4 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/data-api.md @@ -0,0 +1,368 @@ +# 数据操作 API + +## 概述 + +数据操作 API 用于动态更新图表数据,支持同步和异步两种模式。 + +--- + +## API 列表 + +### updateData + +**异步更新指定数据集** + +```typescript +updateData(id: StringOrNumber, data: Datum[] | string, options?: IParserOptions): Promise +``` + +**参数**: +- `id`: 数据集 ID(spec 中定义的 data.id) +- `data`: 新数据数组或数据 URL +- `options`: 数据解析选项(可选) + +**返回值**:Promise + +**使用示例**: + +```javascript +const vchart = new VChart(spec, { dom: 'container' }); +await vchart.renderAsync(); + +// 更新数据 +await vchart.updateData('myData', [ + { category: 'A', value: 20 }, + { category: 'B', value: 30 }, + { category: 'C', value: 25 } +]); +``` + +**适用场景**: +- ✅ 单个数据集更新 +- ✅ 从 API 获取数据后更新 +- ✅ 数据筛选后更新 + +--- + +### updateDataSync + +**同步更新指定数据集** + +```typescript +updateDataSync(id: StringOrNumber, data: Datum[], options?: IParserOptions): IVChart +``` + +**参数**: +- `id`: 数据集 ID +- `data`: 新数据数组 +- `options`: 数据解析选项(可选) + +**返回值**:IVChart(支持链式调用) + +**使用示例**: + +```javascript +// 同步更新数据 +vchart.updateDataSync('myData', newData); + +// 链式调用 +vchart + .updateDataSync('data1', data1) + .updateDataSync('data2', data2); +``` + +**适用场景**: +- ✅ 需要立即获取更新结果 +- ✅ 实时数据刷新(避免闪烁) +- ✅ 调试场景 + +--- + +### updateDataInBatches + +**批量更新多个数据集** + +```typescript +updateDataInBatches(list: { id: string; data: Datum[]; options?: IParserOptions }[]): Promise +``` + +**参数**: +- `list`: 数据更新列表,每个元素包含 id、data、options + +**返回值**:Promise + +**使用示例**: + +```javascript +// 批量更新多个数据集 +await vchart.updateDataInBatches([ + { id: 'sales', data: salesData }, + { id: 'profit', data: profitData }, + { id: 'cost', data: costData } +]); +``` + +**适用场景**: +- ✅ 多个数据集同时更新 +- ✅ 减少渲染次数,提升性能 +- ✅ 组合图数据更新 + +--- + +### updateFullData + +**异步更新完整数据对象** + +```typescript +updateFullData(data: IDataValues | IDataValues[], reRender?: boolean): Promise +``` + +**参数**: +- `data`: 完整数据对象(包含 id、values 等) +- `reRender`: 是否重新渲染,默认 true + +**返回值**:Promise + +**使用示例**: + +```javascript +// 更新完整数据对象 +await vchart.updateFullData({ + id: 'myData', + values: [ + { category: 'A', value: 20 }, + { category: 'B', value: 30 } + ] +}); + +// 更新多个数据对象 +await vchart.updateFullData([ + { id: 'data1', values: data1Values }, + { id: 'data2', values: data2Values } +]); +``` + +**适用场景**: +- ✅ 完整替换数据结构 +- ✅ 数据格式变化较大 +- ✅ 需要更新数据的元信息 + +--- + +### updateFullDataSync + +**同步更新完整数据对象** + +```typescript +updateFullDataSync(data: IDataValues | IDataValues[], reRender?: boolean): IVChart +``` + +**参数**: +- `data`: 完整数据对象 +- `reRender`: 是否重新渲染,默认 true + +**返回值**:IVChart + +**使用示例**: + +```javascript +vchart.updateFullDataSync({ + id: 'myData', + values: newData +}); +``` + +--- + +## 使用场景 + +### 场景 1: 实时数据刷新 + +**需求**:每秒更新图表数据 + +```javascript +// WebSocket 实时数据 +const ws = new WebSocket('wss://api.example.com/realtime'); +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + vchart.updateDataSync('realtimeData', data); +}; +``` + +### 场景 2: 数据筛选 + +**需求**:根据用户选择筛选数据 + +```javascript +function filterData(category) { + const filteredData = allData.filter(d => d.category === category); + vchart.updateData('myData', filteredData); +} +``` + +### 场景 3: 组合图数据更新 + +**需求**:双 Y 轴图表,同时更新两个系列的数据 + +```javascript +// 批量更新多个数据集 +async function updateComboChart(sales, profit) { + await vchart.updateDataInBatches([ + { id: 'salesData', data: sales }, + { id: 'profitData', data: profit } + ]); +} +``` + +### 场景 4: API 数据加载 + +**需求**:从后端 API 获取数据并更新 + +```javascript +async function loadDataFromAPI() { + const response = await fetch('/api/chart-data'); + const data = await response.json(); + await vchart.updateData('chartData', data); +} +``` + +--- + +## 性能优化建议 + +### 1. 选择合适的更新方式 + +| 数据量 | 更新频率 | 推荐 API | +|--------|---------|---------| +| 小(< 100 条) | 低频 | `updateData` | +| 大(> 100 条) | 低频 | `updateData` + 数据预处理 | +| 小(< 100 条) | 高频 | `updateDataSync` | +| 大(> 100 条) | 高频 | `updateDataSync` + 增量更新 | + +### 2. 批量更新优化 + +**❌ 不推荐**:多次单独更新 +```javascript +await vchart.updateData('data1', data1); +await vchart.updateData('data2', data2); +await vchart.updateData('data3', data3); +// 渲染 3 次 +``` + +**✅ 推荐**:批量更新 +```javascript +await vchart.updateDataInBatches([ + { id: 'data1', data: data1 }, + { id: 'data2', data: data2 }, + { id: 'data3', data: data3 } +]); +// 只渲染 1 次 +``` + +### 3. 数据预处理 + +在更新数据前进行预处理,减少 VChart 内部的数据处理时间: + +```javascript +// 预先排序数据 +const sortedData = data.sort((a, b) => b.value - a.value); +await vchart.updateData('myData', sortedData); +``` + +--- + +## 常见问题 + +### Q1: updateData 后图表不更新? + +**原因**:数据 ID 不匹配或数据格式错误 + +**解决方案**: +```javascript +// 检查 spec 中的 data.id +const spec = { + data: [{ id: 'myData', values: [...] }] // 注意这个 id +}; + +// 更新时使用相同的 id +await vchart.updateData('myData', newData); +``` + +### Q2: 高频更新导致性能问题? + +**解决方案**: +1. 使用 `updateDataSync` 减少异步开销 +2. 实现数据节流/防抖 +3. 使用增量更新(仅更新变化的数据) + +```javascript +// 节流更新 +let updateTimer; +function throttledUpdate(data) { + if (updateTimer) clearTimeout(updateTimer); + updateTimer = setTimeout(() => { + vchart.updateDataSync('myData', data); + }, 100); +} +``` + +### Q3: 如何判断数据更新完成? + +**异步 API**:使用 await 或 Promise.then() +```javascript +await vchart.updateData('myData', newData); +console.log('数据更新完成'); +``` + +**同步 API**:立即完成,无需等待 +```javascript +vchart.updateDataSync('myData', newData); +console.log('数据已更新'); +``` + +--- + +## 完整示例 + +```html + + + + + + +
+ + + + + +``` \ No newline at end of file diff --git a/skills/vchart-development-assistant/references/api/event-api.md b/skills/vchart-development-assistant/references/api/event-api.md new file mode 100644 index 0000000000..590169eaec --- /dev/null +++ b/skills/vchart-development-assistant/references/api/event-api.md @@ -0,0 +1,431 @@ +# 事件监听 API + +## 概述 + +VChart 提供了完整的事件监听机制,支持基础 DOM 事件、组件事件、图元事件和生命周期事件。 + +--- + +## API 列表 + +### on + +**注册事件监听器** + +```typescript +on(event: string, callback: (params: EventParams) => void): void; +on(event: string, query: EventQuery, callback: (params: EventParams) => void): void; +``` + +**参数**: +- `event`: 事件名称 +- `query`: 事件过滤条件(可选) +- `callback`: 事件回调函数 + +**使用示例**: + +```javascript +// 基础事件监听 +vchart.on('click', (params) => { + console.log('点击了图表', params); +}); + +// 带过滤条件的事件监听 +vchart.on('click', { level: 'mark', type: 'bar' }, (params) => { + console.log('点击了柱子', params.datum); +}); +``` + +--- + +### off + +**移除事件监听器** + +```typescript +off(event: string, callback?: (params: EventParams) => void): void; +``` + +**参数**: +- `event`: 事件名称 +- `callback`: 要移除的回调函数(可选,不传则移除该事件所有监听) + +**使用示例**: + +```javascript +const handler = (params) => console.log(params); + +// 添加监听 +vchart.on('click', handler); + +// 移除特定监听 +vchart.off('click', handler); + +// 移除所有 click 监听 +vchart.off('click'); +``` + +--- + +## 事件参数结构 + +所有事件回调都接收统一的事件参数: + +```typescript +type EventParams = { + /** 原始事件对象 */ + event: SuperEvent; + /** 事件携带的数据 */ + value?: any; + /** 事件源的 mark */ + mark?: IMark; + /** 事件源的 model */ + model?: IModel; + /** 事件源的 chart */ + chart?: IChart; + /** 拾取到的图元数据 */ + datum?: Datum; + /** 拾取到的图形节点 */ + node?: INode; +}; +``` + +--- + +## 事件分类 + +### 1. 基础 DOM 事件 + +#### 指针事件 +```javascript +vchart.on('pointerdown', handler); +vchart.on('pointerup', handler); +vchart.on('pointermove', handler); +vchart.on('pointerover', handler); +vchart.on('pointerout', handler); +vchart.on('pointertap', handler); +``` + +#### 鼠标事件 +```javascript +vchart.on('click', handler); +vchart.on('dblclick', handler); +vchart.on('mousedown', handler); +vchart.on('mouseup', handler); +vchart.on('mousemove', handler); +vchart.on('mouseover', handler); +vchart.on('mouseout', handler); +vchart.on('wheel', handler); +``` + +#### 触摸事件 +```javascript +vchart.on('touchstart', handler); +vchart.on('touchend', handler); +vchart.on('touchmove', handler); +vchart.on('tap', handler); +``` + +#### 拖拽事件 +```javascript +vchart.on('dragstart', handler); +vchart.on('drag', handler); +vchart.on('dragend', handler); +vchart.on('drop', handler); +``` + +### 2. 组件事件 + +#### 图例事件 +```javascript +// 图例点击 +vchart.on('legendItemClick', (params) => { + console.log('选中的图例项', params.value); +}); + +// 图例 hover +vchart.on('legendItemHover', (params) => { + console.log('hover 的图例项', params.value); +}); +``` + +#### DataZoom 事件 +```javascript +vchart.on('dataZoomChange', (params) => { + console.log('缩放范围', params.value.start, params.value.end); +}); +``` + +#### Brush 框选事件 +```javascript +vchart.on('brushEnd', (params) => { + console.log('框选的数据', params.value.inBrushData); +}); +``` + +### 3. 生命周期事件 + +```javascript +// 图表初始化完成 +vchart.on('initialized', (params) => { + console.log('图表初始化完成'); +}); + +// 渲染完成(只触发一次) +vchart.on('rendered', (params) => { + console.log('图表首次渲染完成'); +}); + +// 每次渲染完成 +vchart.on('renderFinished', (params) => { + console.log('图表渲染完成'); +}); + +// 动画结束 +vchart.on('animationFinished', (params) => { + console.log('动画播放完成'); +}); + +// 尺寸变化 +vchart.on('afterResize', (params) => { + console.log('图表尺寸变化'); +}); +``` + +--- + +## 事件过滤 + +### 过滤规则 + +| 过滤方式 | 说明 | 示例 | +|---------|------|------| +| `source` | 按事件源过滤 | `{ source: 'window' }` | +| `level` | 按冒泡层级过滤 | `{ level: 'mark' }` | +| `type` | 按组件/mark 类型过滤 | `{ type: 'axis' }` | +| `nodeName` | 按图形节点名过滤 | `{ nodeName: 'axis-label' }` | +| `markName` | 按 mark 名称过滤 | `{ markName: 'bar' }` | +| `id` | 按 spec 中的 id 过滤 | `{ id: 'axis-left' }` | +| `filter` | 自定义过滤函数 | `{ filter: (e) => e.model.id === 1 }` | + +### 常用过滤示例 + +```javascript +// 监听特定系列的点击 +vchart.on('click', { seriesId: 'sales' }, (params) => { + console.log('点击了 sales 系列'); +}); + +// 监听坐标轴点击 +vchart.on('click', { level: 'model', type: 'axis' }, (params) => { + console.log('点击了坐标轴'); +}); + +// 监听柱子点击 +vchart.on('click', { level: 'mark', type: 'bar' }, (params) => { + console.log('点击了柱子', params.datum); +}); + +// 监听标签点击(需要先开启标签交互) +// spec: { label: { interactive: true } } +vchart.on('click', { nodeName: 'label' }, (params) => { + console.log('点击了标签'); +}); +``` + +--- + +## 使用场景 + +### 场景 1: 点击图元获取数据 + +```javascript +vchart.on('click', { level: 'mark' }, (params) => { + if (params.datum) { + console.log('点击的数据:', params.datum); + // 跳转到详情页 + window.location.href = `/detail?id=${params.datum.id}`; + } +}); +``` + +### 场景 2: 实现 Tooltip 外部控制 + +```javascript +// 鼠标悬停时更新外部信息面板 +vchart.on('pointermove', { level: 'mark' }, (params) => { + if (params.datum) { + updateInfoPanel(params.datum); + } +}); + +vchart.on('pointerout', () => { + hideInfoPanel(); +}); +``` + +### 场景 3: 图例联动 + +```javascript +// 监听图例点击,联动其他图表 +vchart.on('legendItemClick', (params) => { + const selectedItems = params.value; + // 更新其他图表的显示 + otherChart.updateData('mainData', filterByCategories(selectedItems)); +}); +``` + +### 场景 4: 数据下钻 + +```javascript +vchart.on('dblclick', { level: 'mark' }, async (params) => { + if (params.datum) { + // 双击下钻 + const drillData = await fetchDetailData(params.datum.category); + vchart.updateData('mainData', drillData); + } +}); +``` + +### 场景 5: 框选数据导出 + +```javascript +vchart.on('brushEnd', (params) => { + const selectedData = params.value.inBrushData; + // 导出选中数据 + exportToExcel(selectedData); +}); +``` + +--- + +## 性能优化 + +### 节流与防抖 + +事件过滤配置支持内置的节流和防抖: + +```javascript +// 节流(每 200ms 最多触发一次) +vchart.on('pointermove', { throttle: 200 }, handler); + +// 防抖(停止 200ms 后触发) +vchart.on('pointermove', { debounce: 200 }, handler); +``` + +### 事件销毁 + +组件销毁时记得移除事件监听: + +```javascript +// 保存监听器引用 +const handlers = { + click: handleClick, + hover: handleHover +}; + +// 添加监听 +Object.entries(handlers).forEach(([event, handler]) => { + vchart.on(event, handler); +}); + +// 销毁时移除 +function destroy() { + Object.entries(handlers).forEach(([event, handler]) => { + vchart.off(event, handler); + }); + vchart.release(); +} +``` + +--- + +## 常见问题 + +### Q1: 点击图元没有反应? + +**检查**: +1. 是否正确使用了过滤条件 +2. 图元是否可交互(某些组件默认关闭交互) + +```javascript +// 确保图元可交互 +vchart.on('click', { level: 'mark' }, handler); +``` + +### Q2: 如何获取点击的坐标? + +```javascript +vchart.on('click', (params) => { + // 原始事件坐标 + const clientX = params.event.client.x; + const clientY = params.event.client.y; + + // 图表内坐标 + const canvasX = params.event.offsetX; + const canvasY = params.event.offsetY; +}); +``` + +### Q3: 如何阻止事件冒泡? + +```javascript +vchart.on('click', (params) => { + params.event.stopPropagation(); +}); +``` + +--- + +## 完整示例 + +```html + + + + + + +
+
+ + + + +``` diff --git a/skills/vchart-development-assistant/references/api/export-api.md b/skills/vchart-development-assistant/references/api/export-api.md new file mode 100644 index 0000000000..27c76711d1 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/export-api.md @@ -0,0 +1,316 @@ +# 导出功能 API + +## 概述 + +导出功能 API 用于将图表导出为图片或其他格式。 + +--- + +## API 列表 + +### exportImg + +**导出图片文件(仅浏览器端)** + +```typescript +exportImg(name?: string): Promise; +``` + +**参数**: +- `name`: 保存的文件名(可选) + +**使用示例**: + +```javascript +// 导出图片(会触发浏览器下载) +await vchart.exportImg('my-chart'); +``` + +--- + +### getDataURL + +**获取图片 Data URL** + +```typescript +getDataURL(): Promise; +``` + +**返回值**:Promise - base64 格式的图片数据 + +**使用示例**: + +```javascript +const dataURL = await vchart.getDataURL(); +console.log(dataURL); // data:image/png;base64,... + +// 用于 img 标签 +document.getElementById('preview').src = dataURL; +``` + +--- + +### exportCanvas + +**导出 Canvas 元素** + +```typescript +exportCanvas(): HTMLCanvasElement | undefined; +``` + +**返回值**:Canvas 元素或 undefined + +**使用示例**: + +```javascript +const canvas = vchart.exportCanvas(); +if (canvas) { + // 进行自定义处理 + const ctx = canvas.getContext('2d'); + ctx.fillStyle = 'red'; + ctx.fillRect(0, 0, 10, 10); +} +``` + +--- + +### getImageBuffer + +**获取图片 Buffer(仅 Node.js 环境)** + +```typescript +getImageBuffer(): void; +``` + +**使用示例**: + +```javascript +// Node.js 环境 +const buffer = vchart.getImageBuffer(); +require('fs').writeFileSync('chart.png', buffer); +``` + +--- + +## 使用场景 + +### 场景 1: 下载图表为图片 + +```javascript +async function downloadChart() { + await vchart.exportImg('chart-' + Date.now()); +} +``` + +### 场景 2: 预览图表缩略图 + +```javascript +async function showThumbnail() { + const dataURL = await vchart.getDataURL(); + document.getElementById('thumbnail').src = dataURL; +} +``` + +### 场景 3: 上传图表到服务器 + +```javascript +async function uploadChart() { + const dataURL = await vchart.getDataURL(); + + // 将 base64 转为 Blob + const response = await fetch(dataURL); + const blob = await response.blob(); + + // 上传 + const formData = new FormData(); + formData.append('chart', blob, 'chart.png'); + + await fetch('/api/upload', { + method: 'POST', + body: formData + }); +} +``` + +### 场景 4: 打印图表 + +```javascript +async function printChart() { + const dataURL = await vchart.getDataURL(); + + const printWindow = window.open('', '_blank'); + printWindow.document.write(` + + + + + + `); + printWindow.document.close(); + printWindow.print(); +} +``` + +### 场景 5: 生成报告 + +```javascript +async function generateReport() { + const chartImage = await vchart.getDataURL(); + + const report = { + title: '销售报告', + date: new Date().toISOString(), + chart: chartImage, + data: chartData + }; + + // 发送到报告生成服务 + await fetch('/api/report', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(report) + }); +} +``` + +--- + +## 导出选项 + +### 设置导出尺寸 + +在 spec 中配置: + +```javascript +const spec = { + type: 'bar', + width: 800, // 导出宽度 + height: 600, // 导出高度 + // ... +}; +``` + +### 设置背景色 + +```javascript +const spec = { + type: 'bar', + background: 'white', // 导出背景色 + // ... +}; +``` + +### 设置设备像素比 + +```javascript +const vchart = new VChart(spec, { + dom: 'chart', + mode: 'desktop-browser', + modeParams: { + devicePixelRatio: 2 // 高清导出 + } +}); +``` + +--- + +## 注意事项 + +### 1. 渲染完成后导出 + +```javascript +// ❌ 错误 - 渲染前导出 +const vchart = new VChart(spec, { dom }); +await vchart.exportImg(); // 可能失败 + +// ✅ 正确 - 渲染后导出 +await vchart.renderAsync(); +await vchart.exportImg(); +``` + +### 2. 动画完成后再导出 + +```javascript +// 等待动画完成 +vchart.on('animationFinished', async () => { + await vchart.exportImg('chart'); +}); +``` + +### 3. 跨域问题 + +如果图表中包含跨域图片,导出可能会失败: + +```javascript +// 解决方案:使用代理或转存图片 +``` + +--- + +## 完整示例 + +```html + + + + + + +
+ +
+ + + +
+ +
+ + + + +``` diff --git a/skills/vchart-development-assistant/references/api/interaction-api.md b/skills/vchart-development-assistant/references/api/interaction-api.md new file mode 100644 index 0000000000..6d9844c0d5 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/interaction-api.md @@ -0,0 +1,301 @@ +# 交互控制 API + +## 概述 + +交互控制 API 用于手动触发或禁用图表的交互效果,包括 Tooltip、Crosshair、Dimension 等。 + +--- + +## API 列表 + +### showTooltip + +**手动显示 Tooltip** + +```typescript +showTooltip(datum: Datum, options: IShowTooltipOption): boolean; +``` + +**参数**: +- `datum`: 要显示 tooltip 的数据 +- `options`: 显示选项 + +**返回值**:是否成功显示 + +**使用示例**: + +```javascript +// 显示指定数据的 tooltip +vchart.showTooltip( + { category: 'A', value: 20 }, + { position: { x: 100, y: 100 } } +); +``` + +--- + +### hideTooltip + +**隐藏 Tooltip** + +```typescript +hideTooltip(): boolean; +``` + +**返回值**:是否成功隐藏 + +**使用示例**: + +```javascript +vchart.hideTooltip(); +``` + +--- + +### setDimensionIndex + +**手动触发 dimension 交互效果** + +```typescript +setDimensionIndex(value: StringOrNumber, options?: DimensionIndexOption): void; +``` + +**参数**: +- `value`: 维度值 +- `options`: 触发配置 + +**使用示例**: + +```javascript +// 触发指定维度的交互效果 +vchart.setDimensionIndex('A'); // 显示 'A' 类别的 crosshair 和 tooltip +``` + +--- + +### disableTooltip + +**禁用/启用 Tooltip** + +```typescript +disableTooltip(disabled: boolean): void; +``` + +**参数**: +- `disabled`: true 禁用,false 启用 + +**使用示例**: + +```javascript +// 禁用 tooltip +vchart.disableTooltip(true); + +// 重新启用 +vchart.disableTooltip(false); +``` + +--- + +### disableCrossHair + +**禁用/启用 Crosshair** + +```typescript +disableCrossHair(disabled: boolean): void; +``` + +**参数**: +- `disabled`: true 禁用,false 启用 + +**使用示例**: + +```javascript +vchart.disableCrossHair(true); +``` + +--- + +### disableDimensionHoverEvent + +**禁用/启用 dimension hover 事件** + +```typescript +disableDimensionHoverEvent(disabled?: boolean): void; +``` + +**参数**: +- `disabled`: true 禁用,false 启用 + +**使用示例**: + +```javascript +vchart.disableDimensionHoverEvent(true); +``` + +--- + +## 使用场景 + +### 场景 1: 外部触发 Tooltip + +```javascript +// 通过外部按钮显示 tooltip +function showTooltipForCategory(category) { + const data = chartData.find(d => d.category === category); + if (data) { + const position = vchart.convertDatumToPosition(data); + vchart.showTooltip(data, { position }); + } +} +``` + +### 场景 2: 联动 Crosshair + +```javascript +// 两个图表联动 crosshair +chart1.on('pointermove', (params) => { + if (params.value?.dimension) { + chart2.setDimensionIndex(params.value.dimension); + } +}); + +chart1.on('pointerout', () => { + chart2.setDimensionIndex(null); +}); +``` + +### 场景 3: 条件禁用交互 + +```javascript +// 数据加载时禁用交互 +async function reloadData() { + vchart.disableTooltip(true); + vchart.disableCrossHair(true); + + await vchart.updateData('data', newData); + + vchart.disableTooltip(false); + vchart.disableCrossHair(false); +} +``` + +### 场景 4: 自定义 Tooltip 触发 + +```javascript +// 点击外部元素显示 tooltip +document.getElementById('info-panel').addEventListener('mouseenter', (e) => { + const category = e.target.dataset.category; + const data = chartData.find(d => d.category === category); + vchart.showTooltip(data, {}); +}); + +document.getElementById('info-panel').addEventListener('mouseleave', () => { + vchart.hideTooltip(); +}); +``` + +--- + +## TooltipHandler 自定义 + +### setTooltipHandler + +**设置自定义 Tooltip 处理器** + +```typescript +setTooltipHandler(tooltipHandler: ITooltipHandler): void; +``` + +**使用示例**: + +```javascript +const customHandler = { + showTooltip: (activeTooltip, params) => { + // 自定义显示逻辑 + console.log('显示 tooltip', activeTooltip); + return true; + }, + hideTooltip: () => { + // 自定义隐藏逻辑 + console.log('隐藏 tooltip'); + return true; + }, + release: () => { + // 清理资源 + } +}; + +vchart.setTooltipHandler(customHandler); +``` + +### getTooltipHandler + +**获取当前 Tooltip 处理器** + +```typescript +getTooltipHandler(): ITooltipHandler | undefined; +``` + +--- + +## 完整示例 + +```html + + + + + + +
+ +
+ + + + +
+ + + + +``` diff --git a/skills/vchart-development-assistant/references/api/layout-api.md b/skills/vchart-development-assistant/references/api/layout-api.md new file mode 100644 index 0000000000..78916344fb --- /dev/null +++ b/skills/vchart-development-assistant/references/api/layout-api.md @@ -0,0 +1,176 @@ +# 布局与尺寸 API + +## 概述 + +布局与尺寸 API 用于控制图表的尺寸和布局。 + +--- + +## API 列表 + +### resize + +**调整图表尺寸** + +```typescript +resize(width: number, height: number): Promise; +``` + +**参数**: +- `width`: 新宽度 +- `height`: 新高度 + +**使用示例**: + +```javascript +await vchart.resize(800, 600); +``` + +--- + +### updateViewBox + +**更新绘制区域** + +```typescript +updateViewBox(viewBox: IBoundsLike, reRender?: boolean): IVChart; +``` + +**参数**: +- `viewBox`: 绘制区域 { x1, y1, x2, y2 } +- `reRender`: 是否重新渲染,默认 true + +--- + +### setLayout + +**设置自定义布局** + +```typescript +setLayout(layout: LayoutCallBack): void; +``` + +--- + +### reLayout + +**强制重新布局** + +```typescript +reLayout(): void; +``` + +--- + +### getCurrentSize + +**获取当前容器尺寸** + +```typescript +getCurrentSize(): IContainerSize; +``` + +**返回值**:`{ width: number, height: number }` + +--- + +## 使用场景 + +### 场景 1: 响应式布局 + +```javascript +// 监听容器尺寸变化 +const resizeObserver = new ResizeObserver((entries) => { + const { width, height } = entries[0].contentRect; + vchart.resize(width, height); +}); + +resizeObserver.observe(document.getElementById('chart-container')); +``` + +### 场景 2: 窗口 resize + +```javascript +window.addEventListener('resize', () => { + const container = document.getElementById('chart'); + vchart.resize(container.clientWidth, container.clientHeight); +}); +``` + +### 场景 3: 全屏切换 + +```javascript +function toggleFullscreen() { + if (document.fullscreenElement) { + document.exitFullscreen(); + vchart.resize(600, 400); + } else { + document.getElementById('chart').requestFullscreen(); + vchart.resize(window.innerWidth, window.innerHeight); + } +} +``` + +--- + +## 完整示例 + +```html + + + + + + + +
+
+
+ +
+ + + +
+ + + + +``` diff --git a/skills/vchart-development-assistant/references/api/legend-api.md b/skills/vchart-development-assistant/references/api/legend-api.md new file mode 100644 index 0000000000..0b11d77c1d --- /dev/null +++ b/skills/vchart-development-assistant/references/api/legend-api.md @@ -0,0 +1,186 @@ +# 图例操作 API + +## 概述 + +图例操作 API 用于获取图例数据和设置图例选中状态。 + +--- + +## API 列表 + +### getLegendDataById + +**根据 ID 获取图例数据** + +```typescript +getLegendDataById(id: string): Datum[]; +``` + +**参数**: +- `id`: 图例组件 ID + +**返回值**:图例数据数组 + +--- + +### getLegendDataByIndex + +**根据索引获取图例数据** + +```typescript +getLegendDataByIndex(index?: number): Datum[]; +``` + +**参数**: +- `index`: 图例索引,默认 0 + +--- + +### getLegendSelectedDataById + +**根据 ID 获取图例选中项** + +```typescript +getLegendSelectedDataById(id: string): StringOrNumber[]; +``` + +**返回值**:选中的图例项值数组 + +--- + +### getLegendSelectedDataByIndex + +**根据索引获取图例选中项** + +```typescript +getLegendSelectedDataByIndex(index?: number): StringOrNumber[]; +``` + +--- + +### setLegendSelectedDataById + +**根据 ID 设置图例选中项** + +```typescript +setLegendSelectedDataById(id: string, selectedData: StringOrNumber[]): void; +``` + +**参数**: +- `id`: 图例组件 ID +- `selectedData`: 要选中的图例项值数组 + +--- + +### setLegendSelectedDataByIndex + +**根据索引设置图例选中项** + +```typescript +setLegendSelectedDataByIndex(index: number, selectedData: StringOrNumber[]): void; +``` + +--- + +## 使用场景 + +### 场景 1: 获取当前图例选中状态 + +```javascript +const selected = vchart.getLegendSelectedDataByIndex(0); +console.log('当前选中:', selected); // ['A', 'B'] +``` + +### 场景 2: 程序控制图例选中 + +```javascript +// 只显示指定类别 +vchart.setLegendSelectedDataByIndex(0, ['A', 'B']); + +// 全选 +const allData = vchart.getLegendDataByIndex(0); +const allValues = allData.map(d => d.value); +vchart.setLegendSelectedDataByIndex(0, allValues); +``` + +### 场景 3: 图例状态保存与恢复 + +```javascript +// 保存图例状态 +function saveLegendState() { + return vchart.getLegendSelectedDataByIndex(0); +} + +// 恢复图例状态 +function restoreLegendState(selected) { + vchart.setLegendSelectedDataByIndex(0, selected); +} +``` + +### 场景 4: 多图表图例联动 + +```javascript +// 图表1图例变化时,同步到图表2 +chart1.on('legendItemClick', (params) => { + chart2.setLegendSelectedDataByIndex(0, params.value); +}); +``` + +--- + +## 完整示例 + +```html + + + + + + +
+ +
+ + + +
+ + + + +``` diff --git a/skills/vchart-development-assistant/references/api/state-api.md b/skills/vchart-development-assistant/references/api/state-api.md new file mode 100644 index 0000000000..37e1d61398 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/state-api.md @@ -0,0 +1,376 @@ +# 状态管理 API + +## 概述 + +状态管理 API 用于控制图元的高亮、选中等交互状态,支持自定义状态配置。 + +--- + +## API 列表 + +### setHovered + +**设置图元 hover 状态** + +```typescript +setHovered( + datum: MaybeArray | null, + filter?: (series: ISeries, mark: IMark) => boolean, + region?: IRegionQuerier +): void; +``` + +**参数**: +- `datum`: 要 hover 的数据(单条或数组),传 null 清除 +- `filter`: 过滤函数,指定哪些 series/mark 受影响 +- `region`: 区域过滤 + +**使用示例**: + +```javascript +// 设置单个数据 hover +vchart.setHovered({ category: 'A', value: 20 }); + +// 设置多个数据 hover +vchart.setHovered([ + { category: 'A', value: 20 }, + { category: 'B', value: 30 } +]); + +// 清除 hover 状态 +vchart.setHovered(null); +``` + +--- + +### setSelected + +**设置图元选中状态** + +```typescript +setSelected( + datum: MaybeArray | null, + filter?: (series: ISeries, mark: IMark) => boolean, + region?: IRegionQuerier +): void; +``` + +**参数**: +- `datum`: 要选中的数据,传 null 清除 +- `filter`: 过滤函数 +- `region`: 区域过滤 + +**使用示例**: + +```javascript +// 选中单个数据 +vchart.setSelected({ category: 'A', value: 20 }); + +// 选中多个数据 +vchart.setSelected([ + { category: 'A', value: 20 }, + { category: 'B', value: 30 } +]); + +// 清除选中状态 +vchart.setSelected(null); +``` + +--- + +### clearHovered + +**清除所有 hover 状态** + +```typescript +clearHovered(): void; +``` + +**使用示例**: + +```javascript +vchart.clearHovered(); +``` + +--- + +### clearSelected + +**清除所有选中状态** + +```typescript +clearSelected(): void; +``` + +**使用示例**: + +```javascript +vchart.clearSelected(); +``` + +--- + +### clearState + +**清除指定状态** + +```typescript +clearState(state: string): void; +``` + +**参数**: +- `state`: 状态名称 + +**使用示例**: + +```javascript +// 清除自定义状态 +vchart.clearState('customState'); +``` + +--- + +### updateState + +**更新自定义状态配置** + +```typescript +updateState( + state: Record, 'style'>>, + filter?: (series: ISeries, mark: IMark, stateKey: string) => boolean +): void; +``` + +**参数**: +- `state`: 状态配置对象 +- `filter`: 过滤函数 + +**使用示例**: + +```javascript +// 配置自定义状态 +vchart.updateState({ + highlight: { + filter: (datum) => datum.value > 50 + } +}); +``` + +--- + +## 使用场景 + +### 场景 1: 外部控制图元高亮 + +```javascript +// 根据外部表格 hover 高亮图表 +table.on('rowHover', (row) => { + vchart.setHovered({ category: row.category }); +}); + +table.on('rowLeave', () => { + vchart.clearHovered(); +}); +``` + +### 场景 2: 多图表联动高亮 + +```javascript +// 图表1 hover 联动图表2 +chart1.on('pointerover', { level: 'mark' }, (params) => { + if (params.datum) { + chart2.setHovered(params.datum); + } +}); + +chart1.on('pointerout', () => { + chart2.clearHovered(); +}); +``` + +### 场景 3: 点击选中并保持状态 + +```javascript +let selectedData = null; + +vchart.on('click', { level: 'mark' }, (params) => { + if (params.datum) { + // 切换选中状态 + if (selectedData === params.datum) { + vchart.clearSelected(); + selectedData = null; + } else { + vchart.setSelected(params.datum); + selectedData = params.datum; + } + } +}); +``` + +### 场景 4: 条件筛选高亮 + +```javascript +// 高亮数值大于 50 的数据 +const highValues = data.filter(d => d.value > 50); +vchart.setHovered(highValues); + +// 或使用 filter 函数 +vchart.setHovered( + data, + (series, mark) => mark.name === 'bar' +); +``` + +--- + +## 状态样式配置 + +### Spec 中配置状态样式 + +```javascript +const spec = { + type: 'bar', + data: [{ id: 'data', values: [...] }], + xField: 'category', + yField: 'value', + + // hover 状态样式 + hover: { + enable: true, + style: { + fillOpacity: 0.8, + stroke: '#000', + lineWidth: 2 + } + }, + + // 选中状态样式 + select: { + enable: true, + style: { + fill: '#ff6b6b', + stroke: '#333', + lineWidth: 3 + } + }, + + // 自定义状态 + state: { + highlight: { + style: { + fill: '#ffd43b' + } + } + } +}; +``` + +### 通过 updateState 动态配置 + +```javascript +vchart.updateState({ + highlight: { + filter: (datum) => datum.value > 100 + }, + dim: { + filter: (datum) => datum.value < 20 + } +}); +``` + +--- + +## 注意事项 + +### 1. 数据匹配 + +`setHovered`/`setSelected` 中的 datum 需要与原始数据结构匹配: + +```javascript +// 原始数据 +const data = [ + { category: 'A', value: 20, year: 2024 } +]; + +// ✅ 正确 - 匹配数据结构 +vchart.setHovered({ category: 'A', value: 20, year: 2024 }); + +// ❌ 可能不匹配 - 字段不完整 +vchart.setHovered({ category: 'A' }); +``` + +### 2. 状态优先级 + +当同时设置多个状态时,优先级为: +1. selected +2. hovered +3. 自定义状态 + +### 3. 性能考虑 + +频繁设置状态会影响性能,建议: + +```javascript +// ❌ 不推荐 - 频繁调用 +data.forEach(d => vchart.setHovered(d)); + +// ✅ 推荐 - 批量设置 +vchart.setHovered(data); +``` + +--- + +## 完整示例 + +```html + + + + + + +
+ + + + + + + +``` diff --git a/skills/vchart-development-assistant/references/api/theme-api.md b/skills/vchart-development-assistant/references/api/theme-api.md new file mode 100644 index 0000000000..cedc6a60d3 --- /dev/null +++ b/skills/vchart-development-assistant/references/api/theme-api.md @@ -0,0 +1,250 @@ +# 主题切换 API + +## 潜述 + +主题切换 API 用于动态切换图表主题,支持内置主题和自定义主题。 + +--- + +## 实例 API + +### setCurrentTheme + +**设置当前图表的主题** + +```typescript +setCurrentTheme(name: string): Promise; +``` + +**参数**: +- `name`: 主题名称 + +**使用示例**: + +```javascript +// 切换到深色主题 +await vchart.setCurrentTheme('dark'); + +// 切换回默认主题 +await vchart.setCurrentTheme('default'); +``` + +--- + +### getCurrentTheme + +**获取当前主题配置** + +```typescript +getCurrentTheme(): ITheme; +``` + +**返回值**:当前主题配置对象 + +**使用示例**: + +```javascript +const theme = vchart.getCurrentTheme(); +console.log(theme.colors); // 主题色板 +``` + +--- + +### getCurrentThemeName + +**获取当前主题名称** + +```typescript +getCurrentThemeName(): string; +``` + +**返回值**:主题名称 + +**使用示例**: + +```javascript +const themeName = vchart.getCurrentThemeName(); +console.log(themeName); // 'dark' 或 'default' 等 +``` + +--- + +## 静态 API(VChart.ThemeManager) + +### registerTheme + +**注册自定义主题** + +```typescript +VChart.ThemeManager.registerTheme(name: string, theme: Partial): void; +``` + +**使用示例**: + +```javascript +// 注册自定义主题 +VChart.ThemeManager.registerTheme('myTheme', { + colors: ['#5B8FF9', '#5AD8A6', '#5D7092'], + background: '#f5f5f5', + title: { + textStyle: { + fill: '#333' + } + } +}); + +// 使用自定义主题 +await vchart.setCurrentTheme('myTheme'); +``` + +--- + +### getTheme + +**获取指定主题** + +```typescript +VChart.ThemeManager.getTheme(name: string): ITheme; +``` + +--- + +### removeTheme + +**移除指定主题** + +```typescript +VChart.ThemeManager.removeTheme(name: string): boolean; +``` + +--- + +### themeExist + +**检查主题是否存在** + +```typescript +VChart.ThemeManager.themeExist(name: string): boolean; +``` + +--- + +### setCurrentTheme(全局) + +**设置全局默认主题** + +```typescript +VChart.ThemeManager.setCurrentTheme(name: string): void; +``` + +--- + +## 内置主题 + +VChart 提供以下内置主题: + +| 主题名 | 说明 | +|-------|------| +| `default` | 默认主题 | +| `dark` | 深色主题 | +| `cloud` | 云端主题 | + +--- + +## 使用场景 + +### 场景 1: 深色模式切换 + +```javascript +let isDarkMode = false; + +function toggleDarkMode() { + isDarkMode = !isDarkMode; + vchart.setCurrentTheme(isDarkMode ? 'dark' : 'default'); +} + +// 监听系统主题变化 +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + vchart.setCurrentTheme(e.matches ? 'dark' : 'default'); +}); +``` + +### 场景 2: 品牌主题 + +```javascript +// 注册品牌主题 +VChart.ThemeManager.registerTheme('brand', { + colors: ['#FF6B6B', '#4ECDC4', '#45B7D1'], + background: '#FAFAFA' +}); + +// 应用品牌主题 +await vchart.setCurrentTheme('brand'); +``` + +### 场景 3: 用户偏好主题 + +```javascript +// 保存用户主题偏好 +function saveThemePreference(themeName) { + localStorage.setItem('chart-theme', themeName); +} + +// 加载用户主题偏好 +function loadThemePreference() { + const theme = localStorage.getItem('chart-theme') || 'default'; + vchart.setCurrentTheme(theme); +} +``` + +--- + +## 完整示例 + +```html + + + + + + +
+ +
+ + + +
+ + + + +``` diff --git a/skills/vchart-development-assistant/scripts/generate_demo_html.py b/skills/vchart-development-assistant/scripts/generate_demo_html.py index 7194104799..2cfcf763e5 100644 --- a/skills/vchart-development-assistant/scripts/generate_demo_html.py +++ b/skills/vchart-development-assistant/scripts/generate_demo_html.py @@ -16,6 +16,7 @@ from pathlib import Path import argparse +import sys def escape_js_string(s: str) -> str: """转义 JavaScript 字符串中的特殊字符""" @@ -24,33 +25,71 @@ def escape_js_string(s: str) -> str: s = s.replace("\n", "\\n") return s +def validate_spec_code(spec_code: str) -> bool: + """验证 spec 代码基本格式""" + # 检查是否包含基本的 spec 结构 + if "const spec" not in spec_code and "let spec" not in spec_code and "var spec" not in spec_code: + return False + if "type:" not in spec_code: + return False + return True + def main(): - parser = argparse.ArgumentParser(description="Generate demo HTML from template/demo.html") - parser.add_argument("--title", default="VChart 图表示例", help="页面标题") - parser.add_argument("--desc", default="基于需求生成的可运行图表配置", help="页面描述") - parser.add_argument("--feature", default="补充主要功能说明", help="主要功能说明") - parser.add_argument("--tips", default="补充编辑提示", help="编辑提示") - parser.add_argument("--spec-file", help="包含完整 spec 代码的文件路径") - parser.add_argument("--output", default="output/demo.html", help="输出 HTML 文件路径") - args = parser.parse_args() - - template_path = Path("template/demo.html") - if not template_path.exists(): - raise FileNotFoundError(f"❌ 模板不存在: {template_path}\n💡 请确保在项目根目录运行脚本") - - html = template_path.read_text(encoding="utf-8") - html = html.replace("{{REPORT_TITLE}}", args.title) - html = html.replace("{{REPORT_DESC}}", args.desc) - html = html.replace("{{FEATURE_DESC}}", args.feature) - html = html.replace("{{EDIT_TIPS}}", args.tips) - - if args.spec_file: - spec_path = Path(args.spec_file) - if not spec_path.exists(): - raise FileNotFoundError(f"❌ Spec 文件不存在: {spec_path}") - spec_code = spec_path.read_text(encoding="utf-8") - else: - spec_code = """const spec = { + try: + parser = argparse.ArgumentParser( + description="Generate demo HTML from template/demo.html", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + # 使用自定义 spec 文件 + python3 scripts/generate_demo_html.py --spec-file my-spec.js --output demo.html + + # 完整参数 + python3 scripts/generate_demo_html.py \\ + --title "柱状图示例" \\ + --desc "基础柱状图配置" \\ + --spec-file spec.js \\ + --output output/demo.html + """ + ) + parser.add_argument("--title", default="VChart 图表示例", help="页面标题") + parser.add_argument("--desc", default="基于需求生成的可运行图表配置", help="页面描述") + parser.add_argument("--feature", default="补充主要功能说明", help="主要功能说明") + parser.add_argument("--tips", default="补充编辑提示", help="编辑提示") + parser.add_argument("--spec-file", help="包含完整 spec 代码的文件路径") + parser.add_argument("--output", default="output/demo.html", help="输出 HTML 文件路径") + parser.add_argument("--validate", action="store_true", help="验证 spec 代码格式") + args = parser.parse_args() + + # 检查模板文件 + template_path = Path("template/demo.html") + if not template_path.exists(): + print(f"❌ 错误: 模板文件不存在: {template_path}", file=sys.stderr) + print(f"💡 提示: 请确保在 skills/vchart-development-assistant 目录运行脚本", file=sys.stderr) + sys.exit(1) + + html = template_path.read_text(encoding="utf-8") + html = html.replace("{{REPORT_TITLE}}", args.title) + html = html.replace("{{REPORT_DESC}}", args.desc) + html = html.replace("{{FEATURE_DESC}}", args.feature) + html = html.replace("{{EDIT_TIPS}}", args.tips) + + # 读取或使用默认 spec + if args.spec_file: + spec_path = Path(args.spec_file) + if not spec_path.exists(): + print(f"❌ 错误: Spec 文件不存在: {spec_path}", file=sys.stderr) + sys.exit(1) + + spec_code = spec_path.read_text(encoding="utf-8") + + # 验证 spec 代码格式 + if args.validate and not validate_spec_code(spec_code): + print(f"⚠️ 警告: Spec 代码格式可能不正确", file=sys.stderr) + print(f" 请确保包含 'const spec = {{...}}' 和 'type: \"...\"'", file=sys.stderr) + else: + # 使用内置示例 + spec_code = """const spec = { type: "line", data: { values: [ @@ -64,21 +103,37 @@ def main(): xField: "time", yField: "value" };""" + print(f"ℹ️ 使用内置示例 spec", file=sys.stderr) + + # 转义 spec 代码供 JavaScript 字符串使用 + initial_code_escaped = escape_js_string(spec_code.strip()) + + # 填充 spec 和 initialCode 模板变量 + html = html.replace("{{SPEC_CODE}}", spec_code) + html = html.replace("{{INITIAL_CODE}}", initial_code_escaped) - # 转义 spec 代码供 JavaScript 字符串使用 - initial_code_escaped = escape_js_string(spec_code.strip()) + # 检查模板占位符是否都被替换 + if "{{SPEC_CODE}}" in html or "{{INITIAL_CODE}}" in html: + print(f"❌ 错误: 模板占位符替换失败", file=sys.stderr) + print(f"💡 提示: 请检查模板文件: {template_path}", file=sys.stderr) + sys.exit(1) - # 填充 spec 和 initialCode 模板变量 - html = html.replace("{{SPEC_CODE}}", spec_code) - html = html.replace("{{INITIAL_CODE}}", initial_code_escaped) - filled = html + # 创建输出目录并写入文件 + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(html, encoding="utf-8") - output_path = Path(args.output) - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(filled, encoding="utf-8") + print(f"✅ 示例 HTML 已生成: {output_path.resolve()}") + print(f"📖 请在浏览器中打开以查看交互式图表") - print(f"✅ 示例 HTML 已生成: {output_path.resolve()}") - print(f"📊 请在浏览器中打开以查看可运行示例") + except FileNotFoundError as e: + print(f"❌ 文件错误: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"❌ 未知错误: {e}", file=sys.stderr) + import traceback + traceback.print_exc() + sys.exit(1) if __name__ == "__main__": - main() + main() \ No newline at end of file