diff --git a/.cursor/rules/specify-rules.mdc b/.cursor/rules/specify-rules.mdc index f120f3e934..0687c8a289 100644 --- a/.cursor/rules/specify-rules.mdc +++ b/.cursor/rules/specify-rules.mdc @@ -3,6 +3,7 @@ Auto-generated from all feature plans. Last updated: 2026-01-15 ## Active Technologies + - TypeScript/React 18 + @visactor/react-vchart, @visactor/vchar (001-react-vchart-demo) - TypeScript 4.9.5 + @visactor/vchart, @visactor/vrender-components (~1.0.37), @visactor/vutils (001-scrollbar-wheel-step) @@ -23,6 +24,7 @@ npm test && npm run lint TypeScript 4.9.5: Follow standard conventions ## Recent Changes + - 001-react-vchart-demo: Added TypeScript/React 18 + @visactor/react-vchart, @visactor/vchar - 001-react-vchart-demo: Added [if applicable, e.g., PostgreSQL, CoreData, files or N/A] diff --git a/docs/assets/examples/en/extension-chart/timeline-arrow.md b/docs/assets/examples/en/extension-chart/timeline-arrow.md new file mode 100644 index 0000000000..51854e8797 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/timeline-arrow.md @@ -0,0 +1,146 @@ +--- +category: examples +group: extension chart +title: Timeline Chart - With Arrows +keywords: extension, timeline, arrow +order: 5 +cover: /vchart/preview/timeline-arrow_2.0.jpeg +option: extensionChart +--- + +# Timeline Chart - With Arrows + +Timeline charts support displaying arrows between event nodes, providing a more intuitive representation of time flow and continuity between events. + +## Key Configurations + +- `arrow.visible: true` Enable arrow display +- `arrow.thickness` Set the thickness of arrows +- `arrow.style` Configure arrow style + +## Demo Code + +```javascript livedemo +/** --Please add the following code when using in business-- */ +// When using in business, please additionally depend on @visactor/vchart-extension, keeping the package version consistent with vchart +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --Please add the above code when using in business-- */ + +/** --Please delete the following code when using in business-- */ +const { registerTimelineChart } = VChartExtension; +/** --Please delete the above code when using in business-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + title: 'Requirements', + detail: 'Collect and analyze user needs', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + title: 'Design', + detail: 'Create technical solution', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + title: 'Development', + detail: 'Implement features', + time: 3, + color: '#F5A623' + }, + { + id: '4', + title: 'Testing', + detail: 'Quality assurance', + time: 4, + color: '#9B59B6' + }, + { + id: '5', + title: 'Release', + detail: 'Go live', + time: 5, + color: '#2ECC71' + } + ] + } + ], + title: { + visible: true, + text: 'Project Development Process', + subtext: 'Complete workflow from requirements to release', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + }, + subtextStyle: { + fontSize: 14, + fill: '#666' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + arrow: { + visible: true, + thickness: 16, + style: { + fill: datum => datum.color, + fillOpacity: 0.3 + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12 + } + }, + line: { + visible: false + } + } + ] +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## Related Tutorials + +[Extension Chart: Timeline Chart](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/en/extension-chart/timeline-basic.md b/docs/assets/examples/en/extension-chart/timeline-basic.md new file mode 100644 index 0000000000..d1ef061824 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/timeline-basic.md @@ -0,0 +1,141 @@ +--- +category: examples +group: extension chart +title: Timeline Chart - Basic +keywords: extension, timeline +order: 1 +cover: /vchart/preview/timeline-basic_2.0.jpeg +option: extensionChart +--- + +# Timeline Chart - Basic + +Timeline charts are used to display events in chronological order, suitable for project milestones, corporate development history, product iterations, and other scenarios. + +## Key Configurations + +- `type: 'timeline'` Specifies the chart type as timeline chart +- `direction: 'horizontal' | 'vertical'` Specifies the direction of the timeline, horizontal or vertical +- `timeField` Specifies the time field +- `eventField` Specifies the event name field +- `subTitleField` Specifies the event detail field + +## Demo Code + +```javascript livedemo +/** --Please add the following code when using in business-- */ +// When using in business, please additionally depend on @visactor/vchart-extension, keeping the package version consistent with vchart +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --Please add the above code when using in business-- */ + +/** --Please delete the following code when using in business-- */ +const { registerTimelineChart } = VChartExtension; +/** --Please delete the above code when using in business-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + padding: { + left: 60, + right: 60, + top: 150, + bottom: 150 + }, + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + year: '2021', + title: 'Product Launch', + detail: 'Released first generation product with market recognition', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2022', + title: 'Tech Breakthrough', + detail: 'Achieved major breakthrough in core technology', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2023', + title: 'Market Expansion', + detail: 'Business coverage extended to major cities nationwide', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2024', + title: 'Globalization', + detail: 'Entered international market, opening new chapter', + time: 4, + color: '#9B59B6' + } + ] + } + ], + title: { + visible: true, + text: 'Corporate Development History', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## Related Tutorials + +[Extension Chart: Timeline Chart](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/en/extension-chart/timeline-group.md b/docs/assets/examples/en/extension-chart/timeline-group.md new file mode 100644 index 0000000000..f10ffee573 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/timeline-group.md @@ -0,0 +1,155 @@ +--- +category: examples +group: extension chart +title: Timeline Chart - Grouped Display +keywords: extension, timeline, group +order: 4 +cover: /vchart/preview/timeline-group_2.0.jpeg +option: extensionChart +--- + +# Timeline Chart - Grouped Display + +By configuring seriesField, multiple timelines can be displayed in the same chart, suitable for comparing timelines of different themes or categories. + +## Key Configurations + +- `seriesField` Specifies the grouping field +- Multiple timelines are displayed in parallel, each timeline displays independently + +## Demo Code + +```javascript livedemo +/** --Please add the following code when using in business-- */ +// When using in business, please additionally depend on @visactor/vchart-extension, keeping the package version consistent with vchart +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --Please add the above code when using in business-- */ + +/** --Please delete the following code when using in business-- */ +const { registerTimelineChart } = VChartExtension; +/** --Please delete the above code when using in business-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + padding: { + left: 60, + right: 60, + top: 100, + bottom: 100 + }, + data: [ + { + id: 'timeline-data', + values: [ + { + category: 'Product Line A', + title: 'V1.0', + detail: 'Initial release', + time: 1, + color: '#4A90E2' + }, + { + category: 'Product Line A', + title: 'V2.0', + detail: 'Feature enhancement', + time: 3, + color: '#4A90E2' + }, + { + category: 'Product Line A', + title: 'V3.0', + detail: 'Performance optimization', + time: 5, + color: '#4A90E2' + }, + { + category: 'Product Line B', + title: 'Beta', + detail: 'Beta version', + time: 2, + color: '#50C8C8' + }, + { + category: 'Product Line B', + title: 'V1.0', + detail: 'Official release', + time: 4, + color: '#50C8C8' + }, + { + category: 'Product Line B', + title: 'V2.0', + detail: 'Major update', + time: 6, + color: '#50C8C8' + } + ] + } + ], + title: { + visible: true, + text: 'Multi-Product Line Comparison', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + axes: [ + { + orient: 'bottom', + type: 'linear', + min: 0, + max: 7 + }, + { + orient: 'left', + type: 'band' + } + ], + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + seriesField: 'category', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 13, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 11 + } + }, + line: { + style: { + stroke: datum => datum.color, + lineWidth: 2 + } + } +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## Related Tutorials + +[Extension Chart: Timeline Chart](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/en/extension-chart/timeline-vertical.md b/docs/assets/examples/en/extension-chart/timeline-vertical.md new file mode 100644 index 0000000000..081512126a --- /dev/null +++ b/docs/assets/examples/en/extension-chart/timeline-vertical.md @@ -0,0 +1,147 @@ +--- +category: examples +group: extension chart +title: Timeline Chart - Vertical Layout +keywords: extension, timeline, vertical +order: 3 +cover: /vchart/preview/timeline-vertical_2.0.jpeg +option: extensionChart +--- + +# Timeline Chart - Vertical Layout + +Timeline charts support vertical layout with time flowing from top to bottom, suitable when there is sufficient horizontal space on the page. + +## Key Configurations + +- `direction: 'vertical'` Specifies vertical layout +- `labelPosition: 'left-right' | 'right-left'` Controls alternating display of labels on left and right sides + +## Demo Code + +```javascript livedemo +/** --Please add the following code when using in business-- */ +// When using in business, please additionally depend on @visactor/vchart-extension, keeping the package version consistent with vchart +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --Please add the above code when using in business-- */ + +/** --Please delete the following code when using in business-- */ +const { registerTimelineChart } = VChartExtension; +/** --Please delete the above code when using in business-- */ + +const spec = { + type: 'timeline', + direction: 'vertical', + padding: { + left: 200, + right: 200, + top: 60, + bottom: 60 + }, + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + year: '2021 Q1', + title: 'V1.0 Release', + detail: 'First official version launched', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2021 Q3', + title: 'V2.0 Upgrade', + detail: '50% performance improvement', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2022 Q1', + title: 'V3.0 Refactor', + detail: 'Complete architecture upgrade', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2022 Q3', + title: 'V4.0 Internationalization', + detail: 'Multi-language support', + time: 4, + color: '#9B59B6' + }, + { + id: '5', + year: '2023 Q1', + title: 'V5.0 AI-Powered', + detail: 'AI capabilities introduced', + time: 5, + color: '#E74C3C' + } + ] + } + ], + title: { + visible: true, + text: 'Product Version Iteration History', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'left-right', + dotLabelGap: 10, + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## Related Tutorials + +[Extension Chart: Timeline Chart](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/en/extension-chart/timeline-with-icon.md b/docs/assets/examples/en/extension-chart/timeline-with-icon.md new file mode 100644 index 0000000000..9d5166a81c --- /dev/null +++ b/docs/assets/examples/en/extension-chart/timeline-with-icon.md @@ -0,0 +1,152 @@ +--- +category: examples +group: extension chart +title: Timeline Chart - With Icons +keywords: extension, timeline, icon +order: 2 +cover: /vchart/preview/timeline-icon_2.0.jpeg +option: extensionChart +--- + +# Timeline Chart - With Icons + +Timeline charts support adding icons to event nodes, making information display more intuitive and rich. Icons can be displayed symmetrically with titles relative to the timeline. + +## Key Configurations + +- `iconField` Specifies the icon field +- `icon.style` Configures icon style +- Icons are symmetric with titles relative to the timeline: when title is above, icon is below; when title is on left, icon is on right + +## Demo Code + +```javascript livedemo +/** --Please add the following code when using in business-- */ +// When using in business, please additionally depend on @visactor/vchart-extension, keeping the package version consistent with vchart +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --Please add the above code when using in business-- */ + +/** --Please delete the following code when using in business-- */ +const { registerTimelineChart } = VChartExtension; +/** --Please delete the above code when using in business-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + padding: { + left: 60, + right: 60, + top: 120, + bottom: 120 + }, + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + year: '2021', + title: 'Product Launch', + detail: 'Released first generation product with market recognition', + icon: 'star', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2022', + title: 'Tech Breakthrough', + detail: 'Achieved major breakthrough in core technology', + icon: 'triangleUp', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2023', + title: 'Market Expansion', + detail: 'Business coverage extended to major cities nationwide', + icon: 'diamond', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2024', + title: 'Globalization', + detail: 'Entered international market, opening new chapter', + icon: 'cross', + time: 4, + color: '#9B59B6' + } + ] + } + ], + title: { + visible: true, + text: 'Corporate Development History - With Icons', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + iconField: 'icon', + labelPosition: 'top-bottom', + dot: { + style: { + size: 10, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + icon: { + visible: true, + style: { + size: 24, + fill: datum => datum.color + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12, + lineHeight: 18 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## Related Tutorials + +[Extension Chart: Timeline Chart](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/menu.json b/docs/assets/examples/menu.json index 3e719af8f5..3167ad7df5 100644 --- a/docs/assets/examples/menu.json +++ b/docs/assets/examples/menu.json @@ -2027,6 +2027,41 @@ "en": "extension-chart" }, "children": [ + { + "path": "timeline-basic", + "title": { + "zh": "基础时间轴图", + "en": "Basic Timeline Chart" + } + }, + { + "path": "timeline-with-icon", + "title": { + "zh": "带图标的时间轴图", + "en": "Timeline Chart with Icons" + } + }, + { + "path": "timeline-vertical", + "title": { + "zh": "垂直布局时间轴图", + "en": "Vertical Timeline Chart" + } + }, + { + "path": "timeline-group", + "title": { + "zh": "分组时间轴图", + "en": "Grouped Timeline Chart" + } + }, + { + "path": "timeline-arrow", + "title": { + "zh": "带箭头的时间轴图", + "en": "Timeline Chart with Arrows" + } + }, { "path": "sequence-scatter-link-classification", "title": { diff --git a/docs/assets/examples/zh/extension-chart/timeline-arrow.md b/docs/assets/examples/zh/extension-chart/timeline-arrow.md new file mode 100644 index 0000000000..30f8bba46a --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/timeline-arrow.md @@ -0,0 +1,146 @@ +--- +category: examples +group: extension chart +title: 时间轴图-带箭头连接 +keywords: extension, timeline, arrow +order: 5 +cover: /vchart/preview/timeline-arrow_2.0.jpeg +option: extensionChart +--- + +# 时间轴图-带箭头连接 + +时间轴图支持在事件节点之间显示箭头,更直观地展示时间流向和事件之间的连贯性。 + +## 关键配置 + +- `arrow.visible: true` 启用箭头显示 +- `arrow.thickness` 设置箭头的粗细 +- `arrow.style` 配置箭头样式 + +## 代码演示 + +```javascript livedemo +/** --在业务中使用时请添加以下代码-- */ +// 在业务中使用时, 请额外依赖 @visactor/vchart-extension,包版本保持和vchart一致 +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --在业务中使用时请添加以上代码-- */ + +/** --在业务中使用时请删除以下代码-- */ +const { registerTimelineChart } = VChartExtension; +/** --在业务中使用时请删除以上代码-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + title: '需求分析', + detail: '收集并分析用户需求', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + title: '方案设计', + detail: '制定技术方案', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + title: '开发实现', + detail: '编码开发功能', + time: 3, + color: '#F5A623' + }, + { + id: '4', + title: '测试验收', + detail: '质量保证与验收', + time: 4, + color: '#9B59B6' + }, + { + id: '5', + title: '上线发布', + detail: '正式发布上线', + time: 5, + color: '#2ECC71' + } + ] + } + ], + title: { + visible: true, + text: '项目开发流程', + subtext: '从需求到上线的完整流程', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + }, + subtextStyle: { + fontSize: 14, + fill: '#666' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + arrow: { + visible: true, + thickness: 16, + style: { + fill: datum => datum.color, + fillOpacity: 0.3 + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12 + } + }, + line: { + visible: false + } + } + ] +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## 相关教程 + +[扩展图表:时间轴图](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/zh/extension-chart/timeline-basic.md b/docs/assets/examples/zh/extension-chart/timeline-basic.md new file mode 100644 index 0000000000..d223c4fa8c --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/timeline-basic.md @@ -0,0 +1,141 @@ +--- +category: examples +group: extension chart +title: 时间轴图-基础 +keywords: extension, timeline +order: 1 +cover: /vchart/preview/timeline-basic_2.0.jpeg +option: extensionChart +--- + +# 时间轴图-基础 + +时间轴图用于展示事件按时间顺序发生的过程,适合用于项目里程碑、企业发展历程、产品迭代等场景。 + +## 关键配置 + +- `type: 'timeline'` 指定图表类型为时间轴图 +- `direction: 'horizontal' | 'vertical'` 指定时间轴的方向,水平或垂直 +- `timeField` 指定时间字段 +- `eventField` 指定事件名称字段 +- `subTitleField` 指定事件详情字段 + +## 代码演示 + +```javascript livedemo +/** --在业务中使用时请添加以下代码-- */ +// 在业务中使用时, 请额外依赖 @visactor/vchart-extension,包版本保持和vchart一致 +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --在业务中使用时请添加以上代码-- */ + +/** --在业务中使用时请删除以下代码-- */ +const { registerTimelineChart } = VChartExtension; +/** --在业务中使用时请删除以上代码-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + padding: { + left: 60, + right: 60, + top: 150, + bottom: 150 + }, + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + year: '2021', + title: '产品发布', + detail: '发布第一代产品,获得市场认可', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2022', + title: '技术突破', + detail: '核心技术获得重大突破', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2023', + title: '市场扩展', + detail: '业务覆盖全国主要城市', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2024', + title: '国际化', + detail: '进军国际市场,开启新篇章', + time: 4, + color: '#9B59B6' + } + ] + } + ], + title: { + visible: true, + text: '企业发展历程', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## 相关教程 + +[扩展图表:时间轴图](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/zh/extension-chart/timeline-group.md b/docs/assets/examples/zh/extension-chart/timeline-group.md new file mode 100644 index 0000000000..f848af0832 --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/timeline-group.md @@ -0,0 +1,144 @@ +--- +category: examples +group: extension chart +title: 时间轴图-分组展示 +keywords: extension, timeline, group +order: 4 +cover: /vchart/preview/timeline-group_2.0.jpeg +option: extensionChart +--- + +# 时间轴图-分组展示 + +通过配置 seriesField,可以在同一个图表中展示多条时间轴,适合对比展示不同主题或类别的时间线。 + +## 关键配置 + +- `seriesField` 指定分组字段 +- 多条时间轴会并行显示,每条时间轴独立展示 + +## 代码演示 + +```javascript livedemo +/** --在业务中使用时请添加以下代码-- */ +// 在业务中使用时, 请额外依赖 @visactor/vchart-extension,包版本保持和vchart一致 +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --在业务中使用时请添加以上代码-- */ + +/** --在业务中使用时请删除以下代码-- */ +const { registerTimelineChart } = VChartExtension; +/** --在业务中使用时请删除以上代码-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + padding: { + left: 60, + right: 60, + top: 100, + bottom: 100 + }, + data: [ + { + id: 'timeline-data', + values: [ + { + category: '产品线A', + title: 'V1.0', + detail: '首次发布', + time: 1, + color: '#4A90E2' + }, + { + category: '产品线A', + title: 'V2.0', + detail: '功能增强', + time: 3, + color: '#4A90E2' + }, + { + category: '产品线A', + title: 'V3.0', + detail: '性能优化', + time: 5, + color: '#4A90E2' + }, + { + category: '产品线B', + title: 'Beta', + detail: '测试版本', + time: 2, + color: '#50C8C8' + }, + { + category: '产品线B', + title: 'V1.0', + detail: '正式发布', + time: 4, + color: '#50C8C8' + }, + { + category: '产品线B', + title: 'V2.0', + detail: '重大更新', + time: 6, + color: '#50C8C8' + } + ] + } + ], + title: { + visible: true, + text: '多产品线发展对比', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + seriesField: 'category', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 13, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 11 + } + }, + line: { + style: { + stroke: datum => datum.color, + lineWidth: 2 + } + } +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## 相关教程 + +[扩展图表:时间轴图](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/zh/extension-chart/timeline-vertical.md b/docs/assets/examples/zh/extension-chart/timeline-vertical.md new file mode 100644 index 0000000000..6fa15521a2 --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/timeline-vertical.md @@ -0,0 +1,134 @@ +--- +category: examples +group: extension chart +title: 时间轴图-垂直布局 +keywords: extension, timeline, vertical +order: 3 +cover: /vchart/preview/timeline-vertical_2.0.jpeg +option: extensionChart +--- + +# 时间轴图-垂直布局 + +时间轴图支持垂直布局,时间从上到下展开,适合在页面左右空间充足时使用。 + +## 关键配置 + +- `direction: 'vertical'` 指定为垂直布局 +- `labelPosition: 'left-right' | 'right-left'` 控制标签在左右侧的交替显示 + +## 代码演示 + +```javascript livedemo +/** --在业务中使用时请添加以下代码-- */ +// 在业务中使用时, 请额外依赖 @visactor/vchart-extension,包版本保持和vchart一致 +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --在业务中使用时请添加以上代码-- */ + +/** --在业务中使用时请删除以下代码-- */ +const { registerTimelineChart } = VChartExtension; +/** --在业务中使用时请删除以上代码-- */ + +const spec = { + type: 'timeline', + direction: 'vertical', + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + year: '2021 Q1', + title: 'V1.0 发布', + detail: '首个正式版本上线', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2021 Q3', + title: 'V2.0 升级', + detail: '性能优化50%', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2022 Q1', + title: 'V3.0 重构', + detail: '架构全面升级', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2022 Q3', + title: 'V4.0 国际化', + detail: '支持多语言', + time: 4, + color: '#9B59B6' + }, + { + id: '5', + year: '2023 Q1', + title: 'V5.0 智能化', + detail: '引入AI能力', + time: 5, + color: '#E74C3C' + } + ] + } + ], + title: { + visible: true, + text: '产品版本迭代历程', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'left-right', + dot: { + style: { + size: 12, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## 相关教程 + +[扩展图表:时间轴图](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/examples/zh/extension-chart/timeline-with-icon.md b/docs/assets/examples/zh/extension-chart/timeline-with-icon.md new file mode 100644 index 0000000000..fa83d31b2a --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/timeline-with-icon.md @@ -0,0 +1,146 @@ +--- +category: examples +group: extension chart +title: 时间轴图-带图标 +keywords: extension, timeline, icon +order: 2 +cover: /vchart/preview/timeline-icon_2.0.jpeg +option: extensionChart +--- + +# 时间轴图-带图标 + +时间轴图支持在事件节点上添加图标,使信息展示更加直观和丰富。图标可以与标题关于时间轴对称显示。 + +## 关键配置 + +- `iconField` 指定图标字段 +- `icon.style` 配置图标样式 +- 图标与标题关于时间轴对称:当标题在上方时,图标在下方;当标题在左侧时,图标在右侧 + +## 代码演示 + +```javascript livedemo +/** --在业务中使用时请添加以下代码-- */ +// 在业务中使用时, 请额外依赖 @visactor/vchart-extension,包版本保持和vchart一致 +// import { registerTimelineChart } from '@visactor/vchart-extension'; +/** --在业务中使用时请添加以上代码-- */ + +/** --在业务中使用时请删除以下代码-- */ +const { registerTimelineChart } = VChartExtension; +/** --在业务中使用时请删除以上代码-- */ + +const spec = { + type: 'timeline', + direction: 'horizontal', + data: [ + { + id: 'timeline-data', + values: [ + { + id: '1', + year: '2021', + title: '产品发布', + detail: '发布第一代产品,获得市场认可', + icon: 'star', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2022', + title: '技术突破', + detail: '核心技术获得重大突破', + icon: 'triangleUp', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2023', + title: '市场扩展', + detail: '业务覆盖全国主要城市', + icon: 'diamond', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2024', + title: '国际化', + detail: '进军国际市场,开启新篇章', + icon: 'cross', + time: 4, + color: '#9B59B6' + } + ] + } + ], + title: { + visible: true, + text: '企业发展历程 - 带图标', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + iconField: 'icon', + labelPosition: 'top-bottom', + dot: { + style: { + size: 10, + fill: datum => datum.color, + stroke: '#fff', + lineWidth: 2 + } + }, + icon: { + visible: true, + style: { + size: 24, + fill: datum => datum.color + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12, + lineHeight: 18 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +registerTimelineChart(); +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); + +// Just for the convenience of console debugging, DO NOT COPY! +window['vchart'] = vchart; +``` + +## 相关教程 + +[扩展图表:时间轴图](/vchart/guide/tutorial_docs/Chart_Extensions/timeline) diff --git a/docs/assets/guide/en/tutorial_docs/Chart_Extensions/timeline.md b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/timeline.md new file mode 100644 index 0000000000..4c64beec6d --- /dev/null +++ b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/timeline.md @@ -0,0 +1,279 @@ +# Extension Chart: Timeline Chart + +Timeline Chart is a visualization chart used to display events in chronological order, particularly suitable for showing project progress, corporate development history, product iteration processes, and similar scenarios. + +VChart provides a timeline chart extension component that supports both horizontal and vertical layout modes, with flexible configuration of event node styles, label positions, icons, and other elements. + +![img](https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/vchart/preview/timeline/timeline-basic.png) + +## How to Use Extension Charts + +Timeline charts need to be manually registered before use. The registration and usage methods are as follows: + +```js +import VChart from '@visactor/vchart'; +import { registerTimelineChart } from '@visactor/vchart-extension'; + +const spec = { + type: 'timeline' + // your spec +}; +registerTimelineChart(); + +const vchart = new VChart(spec, { dom: 'chart' }); +vchart.renderSync(); +``` + +If using CDN import, the registration method is as follows: + +```html + + + +``` + +## Related Configuration Items + +### Timeline Chart Configuration + +```ts +export interface ITimelineChartSpec extends ICartesianChartSpec { + type: 'timeline'; + /** + * Timeline direction + * - 'horizontal': Horizontal direction, time flows from left to right + * - 'vertical': Vertical direction, time flows from top to bottom + */ + direction?: 'horizontal' | 'vertical'; + /** + * Series configuration + */ + series?: IEventSeriesSpec[]; +} +``` + +### Event Series Configuration + +```ts +export interface IEventSeriesSpec extends ICartesianSeriesSpec { + type: 'event'; + /** + * Time field, used to specify the position of events on the timeline + */ + timeField?: string; + /** + * Event name field + */ + eventField?: string; + /** + * Event detail field (subtitle) + */ + subTitleField?: string; + /** + * Icon field, used to display icons or images + */ + iconField?: string; + /** + * Series field, used for grouped display + */ + seriesField?: string; + /** + * Position of title and subtitle + * - Horizontal layout: 'top' | 'bottom' | 'top-bottom' | 'bottom-top' + * - Vertical layout: 'left' | 'right' | 'left-right' | 'right-left' + */ + labelPosition?: LabelPosition; + /** + * Icon mark configuration + * offset: Icon offset distance relative to the dot, in pixels. Positive values offset outward, negative values offset inward + */ + icon?: IMarkSpec & { offset?: number }; + /** + * Event title mark configuration + * subTitleGap: Gap between title and subtitle, in pixels + * offset: Title offset distance relative to the dot, in pixels. Positive values offset outward, negative values offset inward + */ + title?: IMarkSpec & { subTitleGap?: number; offset?: number }; + /** + * Event subtitle mark configuration + */ + subTitle?: IMarkSpec; + /** + * Event line mark configuration + */ + line?: IMarkSpec; + /** + * Arrow mark configuration + * thickness: Arrow thickness, in pixels + */ + arrow?: IMarkSpec & { thickness?: number }; +} +``` + +## Timeline Chart Examples + +- [Basic Timeline Chart](/vchart/demo/extension-chart/timeline-basic) +- [Timeline Chart with Icons](/vchart/demo/extension-chart/timeline-with-icon) + +## Configuration Details + +### Layout Direction + +Timeline charts support two layout directions: + +- **Horizontal Layout** (`direction: 'horizontal'`): Time flows from left to right, suitable for displaying horizontal timeline processes +- **Vertical Layout** (`direction: 'vertical'`): Time flows from top to bottom, suitable for displaying vertical development histories + +### Data Field Configuration + +Timeline charts require the following data field configurations: + +- `timeField`: Time field, used to determine the position of events on the timeline +- `eventField`: Event name field, displayed as the title +- `subTitleField`: Event detail field, displayed as subtitle (optional) +- `iconField`: Icon field, used to display icons or images (optional) +- `seriesField`: Series field, used for grouped display of multiple timelines (optional) + +### Label Position Configuration + +The `labelPosition` controls the display position of titles and subtitles: + +**For Horizontal Layout:** + +- `top`: Title always above the timeline +- `bottom`: Title always below the timeline +- `top-bottom`: Titles alternate between top and bottom (first on top, second on bottom) +- `bottom-top`: Titles alternate between bottom and top (first on bottom, second on top) + +**For Vertical Layout:** + +- `left`: Title always on the left of the timeline +- `right`: Title always on the right of the timeline +- `left-right`: Titles alternate between left and right (first on left, second on right) +- `right-left`: Titles alternate between right and left (first on right, second on left) + +### Icon Feature + +Timeline charts support adding icons to event nodes. Icons are displayed symmetrically with titles relative to the timeline: + +- Horizontal layout: When title is above, icon is below; when title is below, icon is above +- Vertical layout: When title is on the left, icon is on the right; when title is on the right, icon is on the left + +Icons can use VChart's built-in symbol shapes (such as 'star', 'diamond', 'triangleUp', etc.) or image URLs. + +### Style Configuration + +#### Dot Mark Style + +Configure dot mark style for event nodes via `dot`: + +```js +dot: { + style: { + size: 12, + fill: '#4A90E2', + stroke: '#fff', + lineWidth: 2 + } +} +``` + +#### Icon Style + +Configure icon style via `icon`: + +```js +icon: { + style: { + size: 24, + fill: '#4A90E2', + shape: 'star' // or image URL + } +} +``` + +#### Title Style + +Configure title and subtitle styles separately via `title` and `subTitle`: + +```js +title: { + style: { + fontSize: 14, + fontWeight: 'bold', + fill: '#333' + } +}, +subTitle: { + style: { + fontSize: 12, + fill: '#666', + lineHeight: 18 + } +} +``` + +#### Timeline Line Style + +Configure timeline line style via `line`: + +```js +line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } +} +``` + +#### Arrow Style + +Configure connecting arrow style via `arrow`: + +```js +arrow: { + visible: true, + thickness: 16, + style: { + fill: '#4A90E2' + } +} +``` + +### Grouped Display + +By configuring `seriesField`, you can implement grouped display of multiple timelines, suitable for comparing timelines of multiple themes or categories. + +### Interactive Features + +Timeline charts support the following interactive features: + +- **Tooltip**: When hovering over dot marks, icons, or arrows, detailed event information is displayed +- **Click Events**: You can listen to click events on dot marks to implement custom interactions + +## Application Scenarios + +Timeline charts are suitable for the following scenarios: + +1. **Project Management**: Display project milestones and key nodes +2. **Corporate Development**: Show company development history and important events +3. **Product Iteration**: Display product version update history +4. **Historical Events**: Show chronological order of historical events +5. **Resume**: Display work experience and educational background + +## Notes + +1. Time fields should be numeric or date types to ensure correct sorting +2. When using `seriesField` for grouping, reasonably control the number of groups to avoid overcrowded charts +3. Label position selection should consider content length and chart space to avoid text overlap +4. Icon size should coordinate with the overall layout, not too large or too small diff --git a/docs/assets/guide/menu.json b/docs/assets/guide/menu.json index 1a0727f952..3fe4488545 100644 --- a/docs/assets/guide/menu.json +++ b/docs/assets/guide/menu.json @@ -954,6 +954,13 @@ "zh": "蜡烛图", "en": "Candlestick" } + }, + { + "path": "timeline", + "title": { + "zh": "时间线图组件", + "en": "Timeline" + } } ] } diff --git a/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/timeline.md b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/timeline.md new file mode 100644 index 0000000000..6cee61874b --- /dev/null +++ b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/timeline.md @@ -0,0 +1,279 @@ +# 扩展图表:时间轴图 + +时间轴图(Timeline Chart)是一种用于按时间顺序展示事件的可视化图表,特别适合展示项目进度、企业发展历程、产品迭代过程等场景。 + +VChart 提供了时间轴图扩展组件,支持水平和垂直两种布局方式,可以灵活配置事件节点的样式、标签位置和图标等元素。 + +![img](https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/vchart/preview/timeline/timeline-basic.png) + +## 如何使用扩展图表 + +时间轴图需要手动注册后才能使用,注册和使用方式如下: + +```js +import VChart from '@visactor/vchart'; +import { registerTimelineChart } from '@visactor/vchart-extension'; + +const spec = { + type: 'timeline' + // your spec +}; +registerTimelineChart(); + +const vchart = new VChart(spec, { dom: 'chart' }); +vchart.renderSync(); +``` + +如果是通过 cdn 引入的方式,注册方式如下: + +```html + + + +``` + +## 相关配置项 + +### 时间轴图配置 + +```ts +export interface ITimelineChartSpec extends ICartesianChartSpec { + type: 'timeline'; + /** + * 时间轴方向 + * - 'horizontal': 水平方向,时间从左到右 + * - 'vertical': 垂直方向,时间从上到下 + */ + direction?: 'horizontal' | 'vertical'; + /** + * 系列配置 + */ + series?: IEventSeriesSpec[]; +} +``` + +### 事件系列配置 + +```ts +export interface IEventSeriesSpec extends ICartesianSeriesSpec { + type: 'event'; + /** + * 时间字段,用于指定事件在时间轴上的位置 + */ + timeField?: string; + /** + * 事件名称字段 + */ + eventField?: string; + /** + * 事件详情字段(副标题) + */ + subTitleField?: string; + /** + * 图标字段,用于显示图标或图片 + */ + iconField?: string; + /** + * 系列字段,用于分组显示 + */ + seriesField?: string; + /** + * 标题和副标题的位置 + * - 水平布局: 'top' | 'bottom' | 'top-bottom' | 'bottom-top' + * - 垂直布局: 'left' | 'right' | 'left-right' | 'right-left' + */ + labelPosition?: LabelPosition; + /** + * 图标图元配置 + * offset: 图标相对于点的偏移距离,单位像素,正值向外偏移,负值向内偏移 + */ + icon?: IMarkSpec & { offset?: number }; + /** + * 事件标题图元配置 + * subTitleGap: 标题与副标题的间距,单位像素 + * offset: 标题相对于点的偏移距离,单位像素,正值向外偏移,负值向内偏移 + */ + title?: IMarkSpec & { subTitleGap?: number; offset?: number }; + /** + * 事件副标题图元配置 + */ + subTitle?: IMarkSpec; + /** + * 事件线图元配置 + */ + line?: IMarkSpec; + /** + * 箭头图元配置 + * thickness: 箭头的厚度,单位像素 + */ + arrow?: IMarkSpec & { thickness?: number }; +} +``` + +## 时间轴图示例 + +- [基础时间轴图](/vchart/demo/extension-chart/timeline-basic) +- [带图标的时间轴图](/vchart/demo/extension-chart/timeline-with-icon) + +## 配置详解 + +### 布局方向 + +时间轴图支持两种布局方向: + +- **水平布局** (`direction: 'horizontal'`): 时间从左到右展开,适合展示横向的时间进程 +- **垂直布局** (`direction: 'vertical'`): 时间从上到下展开,适合展示纵向的发展历程 + +### 数据字段配置 + +时间轴图需要配置以下数据字段: + +- `timeField`: 时间字段,用于确定事件在时间轴上的位置 +- `eventField`: 事件名称字段,显示为标题 +- `subTitleField`: 事件详情字段,显示为副标题(可选) +- `iconField`: 图标字段,用于显示图标或图片(可选) +- `seriesField`: 系列字段,用于分组展示多条时间轴(可选) + +### 标签位置配置 + +通过 `labelPosition` 可以控制标题和副标题的显示位置: + +**水平布局时:** + +- `top`: 标题始终在时间轴上方 +- `bottom`: 标题始终在时间轴下方 +- `top-bottom`: 标题交替显示在上方和下方(第一个在上,第二个在下) +- `bottom-top`: 标题交替显示在下方和上方(第一个在下,第二个在上) + +**垂直布局时:** + +- `left`: 标题始终在时间轴左侧 +- `right`: 标题始终在时间轴右侧 +- `left-right`: 标题交替显示在左侧和右侧(第一个在左,第二个在右) +- `right-left`: 标题交替显示在右侧和左侧(第一个在右,第二个在左) + +### 图标功能 + +时间轴图支持在事件节点上添加图标,图标会与标题关于时间轴对称显示: + +- 水平布局:当标题在上方时,图标在下方;当标题在下方时,图标在上方 +- 垂直布局:当标题在左侧时,图标在右侧;当标题在右侧时,图标在左侧 + +图标可以使用 VChart 内置的 symbol 形状(如 'star'、'diamond'、'triangleUp' 等)或图片 URL。 + +### 样式配置 + +#### 点标记样式 + +通过 `dot` 配置事件节点的点标记样式: + +```js +dot: { + style: { + size: 12, + fill: '#4A90E2', + stroke: '#fff', + lineWidth: 2 + } +} +``` + +#### 图标样式 + +通过 `icon` 配置图标的样式: + +```js +icon: { + style: { + size: 24, + fill: '#4A90E2', + shape: 'star' // 或图片 URL + } +} +``` + +#### 标题样式 + +通过 `title` 和 `subTitle` 分别配置标题和副标题的样式: + +```js +title: { + style: { + fontSize: 14, + fontWeight: 'bold', + fill: '#333' + } +}, +subTitle: { + style: { + fontSize: 12, + fill: '#666', + lineHeight: 18 + } +} +``` + +#### 时间轴线样式 + +通过 `line` 配置时间轴线的样式: + +```js +line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } +} +``` + +#### 箭头样式 + +通过 `arrow` 配置连接箭头的样式: + +```js +arrow: { + visible: true, + thickness: 16, + style: { + fill: '#4A90E2' + } +} +``` + +### 分组展示 + +通过配置 `seriesField`,可以实现多条时间轴的分组展示,适合对比展示多个主题或类别的时间线。 + +### 交互功能 + +时间轴图支持以下交互功能: + +- **Tooltip**: 鼠标悬停在点标记、图标或箭头上时,会显示事件的详细信息 +- **点击事件**: 可以监听点标记的点击事件,实现自定义交互 + +## 应用场景 + +时间轴图适用于以下场景: + +1. **项目管理**: 展示项目的里程碑和关键节点 +2. **企业发展**: 展示公司的发展历程和重要事件 +3. **产品迭代**: 展示产品版本的更新历史 +4. **历史事件**: 展示历史事件的时间顺序 +5. **个人简历**: 展示工作经历和教育背景 + +## 注意事项 + +1. 时间字段应该是数值或日期类型,以确保正确的排序 +2. 当使用 `seriesField` 分组时,建议合理控制分组数量,避免图表过于拥挤 +3. 标签位置的选择应该考虑内容长度和图表空间,避免文本重叠 +4. 图标大小应该与整体布局协调,不宜过大或过小 diff --git a/docs/public/vchart/preview/timeline-arrow_2.0.jpeg b/docs/public/vchart/preview/timeline-arrow_2.0.jpeg new file mode 100644 index 0000000000..ff56e5c225 Binary files /dev/null and b/docs/public/vchart/preview/timeline-arrow_2.0.jpeg differ diff --git a/docs/public/vchart/preview/timeline-basic_2.0.jpeg b/docs/public/vchart/preview/timeline-basic_2.0.jpeg new file mode 100644 index 0000000000..20e44d2fe8 Binary files /dev/null and b/docs/public/vchart/preview/timeline-basic_2.0.jpeg differ diff --git a/docs/public/vchart/preview/timeline-group_2.0.jpeg b/docs/public/vchart/preview/timeline-group_2.0.jpeg new file mode 100644 index 0000000000..c6767d9754 Binary files /dev/null and b/docs/public/vchart/preview/timeline-group_2.0.jpeg differ diff --git a/docs/public/vchart/preview/timeline-icon_2.0.jpeg b/docs/public/vchart/preview/timeline-icon_2.0.jpeg new file mode 100644 index 0000000000..6754cea82d Binary files /dev/null and b/docs/public/vchart/preview/timeline-icon_2.0.jpeg differ diff --git a/docs/public/vchart/preview/timeline-vertical_2.0.jpeg b/docs/public/vchart/preview/timeline-vertical_2.0.jpeg new file mode 100644 index 0000000000..92b237a416 Binary files /dev/null and b/docs/public/vchart/preview/timeline-vertical_2.0.jpeg differ diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/demo-horizontal.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/demo-horizontal.ts new file mode 100644 index 0000000000..0d8b8ac1bc --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/demo-horizontal.ts @@ -0,0 +1,150 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +/** + * 企业优势列表 - 水平布局示例 + * 参考设计图复现 + */ + +const timelineData = [ + { + id: '1', + year: '2021', + title: '品牌影响力', + detail: '在目标用户群中具备较强认知与信任度', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2022', + title: '技术研发力', + detail: '拥有自研核心系统与持续创新能力', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2023', + title: '市场增长快', + detail: '近一年用户规模实现快速增长', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2020', + title: '服务满意度', + detail: '用户对服务体系整体评分较高', + time: 4, + color: '#9B59B6' + }, + { + id: '5', + year: '2022', + title: '数据资产全', + detail: '构建了完整用户标签与画像体系', + time: 5, + color: '#8E44AD' + }, + { + id: '6', + year: '2023', + title: '创新能力强', + detail: '新产品上线频率高于行业平均', + time: 6, + color: '#2ECC71' + } +]; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'enterprise-advantages', + direction: 'horizontal', + padding: { + left: 60, + right: 60, + top: 150, + bottom: 150 + }, + background: '#f5f5f5', + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + title: { + visible: true, + text: '企业优势列表', + subtext: '展示企业在不同维度上的核心优势与表现值', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + }, + subtextStyle: { + fontSize: 14, + fill: '#666' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: (datum: Datum) => String((datum as Record).color ?? '#4A90E2'), + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12, + lineHeight: 18 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +declare global { + interface Window { + vchart?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchart = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/demo-vertical.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/demo-vertical.ts new file mode 100644 index 0000000000..f7670447ae --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/demo-vertical.ts @@ -0,0 +1,137 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +/** + * 企业发展时间线 - 垂直布局示例 + * 参考设计图复现 + */ + +const timelineData = [ + { + id: '01', + year: '2018年', + title: '企业成立,完成初期团队搭建和产品定位', + time: 1, + color: '#4A90E2' + }, + { + id: '02', + year: '2020年', + title: '发布首款核心产品,打开区域市场', + time: 2, + color: '#50C8C8' + }, + { + id: '03', + year: '2021年', + title: '启动数字化平台,提升内部运营效率', + time: 3, + color: '#F5A623' + }, + { + id: '04', + year: '2022年', + title: '完成A轮融资,加速市场拓展布局', + time: 4, + color: '#9B59B6' + }, + { + id: '05', + year: '2024年', + title: '推进生态合作,拓展全国影响力', + time: 5, + color: '#5B6AE0' + } +]; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'enterprise-development', + direction: 'vertical', + padding: { + left: 200, + right: 200, + top: 80, + bottom: 80 + }, + background: '#f5f5f5', + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + title: { + visible: true, + text: '企业发展时间线', + subtext: '展示企业在关键年份的战略动作与发展节点', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + }, + subtextStyle: { + fontSize: 14, + fill: '#666' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'year', + subTitleField: 'title', + labelPosition: 'left-right', + dot: { + style: { + size: 14, + fill: (datum: Datum) => String((datum as Record).color ?? '#4A90E2'), + stroke: '#fff', + lineWidth: 2 + } + }, + title: { + style: { + fill: '#333', + fontSize: 16, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 13, + lineHeight: 20 + } + }, + line: { + style: { + stroke: 'red', + lineWidth: 3 + } + } + } + ] +}; + +declare global { + interface Window { + vchart?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchart = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/enterprise-development-horizontal.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/enterprise-development-horizontal.ts new file mode 100644 index 0000000000..b8a59d4f37 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/enterprise-development-horizontal.ts @@ -0,0 +1,170 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +const timelineData = [ + { + id: '1', + year: '2018年', + step: '01', + title: '企业成立', + detail: '完成初期团队搭建和产品定位', + time: 1, + color: '#5B8FF9' + }, + { + id: '2', + year: '2020年', + step: '02', + title: '发布首款核心产品', + detail: '打开区域市场', + time: 2, + color: '#5AD8A6' + }, + { + id: '3', + year: '2021年', + step: '03', + title: '启动数字化平台', + detail: '提升内部运营效率', + time: 3, + color: '#E8684A' + }, + { + id: '4', + year: '2022年', + step: '04', + title: '完成A轮融资', + detail: '加速市场拓展布局', + time: 4, + color: '#9270CA' + }, + { + id: '5', + year: '2024年', + step: '05', + title: '推进生态合作', + detail: '拓展全国影响力', + time: 5, + color: '#6C5DD3' + } +]; + +const getDatumString = (datum: Datum | undefined, key: string) => { + if (!datum || typeof datum !== 'object') { + return ''; + } + const value = (datum as Record)[key]; + return typeof value === 'string' ? value : ''; +}; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'enterprise-development', + direction: 'horizontal', + padding: { + left: 200, + right: 200, + top: 120, + bottom: 80 + }, + background: '#f5f5f5', + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + title: { + visible: true, + text: '企业发展时间线', + subtext: '展示企业在关键年份的战略动作与发展节点', + textStyle: { + fontSize: 32, + fontWeight: 'bold', + fill: '#1a1a1a' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'year', + subTitleField: 'title', + labelPosition: 'right', + arrow: { + visible: true + }, + dot: { + style: { + size: 20, + fill: (datum: Datum) => String((datum as Record).color ?? '#5B8FF9'), + lineWidth: 4, + stroke: '#ffffff' + } + }, + line: { + style: { + stroke: '#d9d9d9', + lineWidth: 4 + } + }, + title: { + style: { + fill: '#1a1a1a', + fontSize: 18, + fontWeight: 600 + } + }, + subTitle: { + style: { + fill: '#1a1a1a', + fontSize: 15, + fontWeight: 'normal' + } + } + } + ], + tooltip: { + mark: { + title: { + value: (datum?: Datum) => getDatumString(datum, 'year') + }, + content: [ + { + key: '阶段', + value: (datum?: Datum) => getDatumString(datum, 'step') + }, + { + key: '标题', + value: (datum?: Datum) => getDatumString(datum, 'title') + }, + { + key: '详情', + value: (datum?: Datum) => getDatumString(datum, 'detail') + } + ] + } + } +}; + +declare global { + interface Window { + vchartH?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchartH = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/enterprise-development.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/enterprise-development.ts new file mode 100644 index 0000000000..e5b895797f --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/enterprise-development.ts @@ -0,0 +1,189 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +const timelineData = [ + { + id: '1', + year: '2018年', + step: '01', + title: '企业成立', + detail: '完成初期团队搭建和产品定位', + time: 1, + color: '#5B8FF9' + }, + { + id: '2', + year: '2020年', + step: '02', + title: '发布首款核心产品', + detail: '打开区域市场', + time: 2, + color: '#5AD8A6' + }, + { + id: '3', + year: '2021年', + step: '03', + title: '启动数字化平台', + detail: '提升内部运营效率', + time: 3, + color: '#E8684A' + }, + { + id: '4', + year: '2022年', + step: '04', + title: '完成A轮融资', + detail: '加速市场拓展布局', + time: 4, + color: '#9270CA' + }, + { + id: '5', + year: '2024年', + step: '05', + title: '推进生态合作', + detail: '拓展全国影响力', + time: 5, + color: '#6C5DD3' + } +]; + +const getDatumString = (datum: Datum | undefined, key: string) => { + if (!datum || typeof datum !== 'object') { + return ''; + } + const value = (datum as Record)[key]; + return typeof value === 'string' ? value : ''; +}; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'enterprise-development', + direction: 'vertical', + padding: { + left: 200, + right: 200, + top: 120, + bottom: 80 + }, + background: '#f5f5f5', + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + title: { + visible: true, + text: '企业发展时间线', + subtext: '展示企业在关键年份的战略动作与发展节点', + textStyle: { + fontSize: 32, + fontWeight: 'bold', + fill: '#1a1a1a' + } + }, + axes: [ + { + orient: 'left', + type: 'band', + // inverse: true, + label: { + visible: false + }, + tick: { + visible: false + }, + grid: { + visible: false + }, + domainLine: { + visible: false + } + } + ], + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'year', + subTitleField: 'title', + labelPosition: 'right', + arrow: { + visible: false + }, + dot: { + style: { + size: 20, + fill: (datum: Datum) => String((datum as Record).color ?? '#5B8FF9'), + lineWidth: 4, + stroke: '#ffffff' + } + }, + line: { + style: { + stroke: '#d9d9d9', + lineWidth: 4 + } + }, + title: { + style: { + fill: '#1a1a1a', + fontSize: 18, + fontWeight: 600 + } + }, + subTitle: { + style: { + fill: '#1a1a1a', + fontSize: 15, + fontWeight: 'normal' + } + } + } + ], + tooltip: { + mark: { + title: { + value: (datum?: Datum) => getDatumString(datum, 'year') + }, + content: [ + { + key: '阶段', + value: (datum?: Datum) => getDatumString(datum, 'step') + }, + { + key: '标题', + value: (datum?: Datum) => getDatumString(datum, 'title') + }, + { + key: '详情', + value: (datum?: Datum) => getDatumString(datum, 'detail') + } + ] + } + } +}; + +declare global { + interface Window { + vchartVertical?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchartVertical = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/group.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/group.ts new file mode 100644 index 0000000000..22ad1c0c2c --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/group.ts @@ -0,0 +1,154 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +const timelineData = [ + { + id: 'alpha-plan', + group: '研发', + stage: '计划', + time: 1, + event: '需求评审', + detail: 'PRD 评审' + }, + { + id: 'alpha-design', + group: '设计', + stage: '设计', + time: 2, + event: '视觉稿', + detail: '核心页面' + }, + { + id: 'alpha-dev', + group: '研发', + stage: '开发', + time: 3, + event: '功能联调', + detail: '基础功能' + }, + { + id: 'alpha-test', + group: '研发', + stage: '测试', + time: 4, + event: '灰度验证', + detail: '核心流程' + }, + { + id: 'alpha-campaign', + group: '运营', + stage: '运营', + time: 5, + event: '上线预热', + detail: '活动物料' + }, + { + id: 'alpha-launch', + group: '运营', + stage: '发布', + time: 6, + event: '正式上线', + detail: '全量发布' + } +]; + +const getDatumString = (datum: Datum | undefined, key: string) => { + if (!datum || typeof datum !== 'object') { + return ''; + } + const value = (datum as Record)[key]; + return typeof value === 'string' ? value : ''; +}; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'timeline-group', + direction: 'horizontal', + padding: { + left: 80, + right: 40, + top: 20, + bottom: 40 + }, + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + axes: [ + { + orient: 'bottom', + type: 'band', + label: { + formatMethod: (value: string | string[]) => { + const raw = Array.isArray(value) ? value[0] : value; + return raw ? `第${raw}阶段` : ''; + } + } + } + ], + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + seriesField: 'group', + eventField: 'event', + dotTypeField: 'stage', + title: { + style: { + fill: '#2e2f32' + } + }, + subTitle: { + style: { + fill: '#6b6f76', + dy: 6 + } + } + } + ], + tooltip: { + mark: { + title: { + value: (datum?: Datum) => getDatumString(datum, 'event') + }, + content: [ + { + key: '分组', + value: (datum?: Datum) => getDatumString(datum, 'group') + }, + { + key: '阶段', + value: (datum?: Datum) => getDatumString(datum, 'stage') + }, + { + key: '说明', + value: (datum?: Datum) => getDatumString(datum, 'detail') + } + ] + } + } +}; + +declare global { + interface Window { + vchart?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchart = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/horizontal.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/horizontal.ts new file mode 100644 index 0000000000..c077013e76 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/horizontal.ts @@ -0,0 +1,154 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +const timelineData = [ + { + id: 'q1', + quarter: '2023 年 Q1', + title: '项目启动', + detail: '· 团队组建\n· 确定技术选型', + position: 'top', + color: '#8f69ff', + time: 1 + }, + { + id: 'q2', + quarter: '2023 年 Q2', + title: '产品 MVP', + detail: '· 完成核心功能开发\n· 发布内测版本', + position: 'bottom', + color: '#4c7dff', + time: 2 + }, + { + id: 'q3', + quarter: '2023 年 Q3', + title: '市场推广', + detail: '· 线上营销活动\n· 拓展商企合作伙伴', + position: 'top', + color: '#f4c21f', + time: 3 + }, + { + id: 'q4', + quarter: '2023 年 Q4', + title: 'A 轮融资', + detail: '· 完成融资千万\n· 扩大研发团队', + position: 'bottom', + color: '#f39b3d', + time: 4 + } +]; + +const getDatumString = (datum: Datum | undefined, key: string) => { + if (!datum || typeof datum !== 'object') { + return ''; + } + const value = (datum as Record)[key]; + return typeof value === 'string' ? value : ''; +}; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'timeline-horizontal', + direction: 'horizontal', + padding: { + left: 80, + right: 80, + top: 120, + bottom: 120 + }, + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + axes: [ + { + orient: 'bottom', + type: 'band', + label: { + visible: false + }, + tick: { + visible: false + }, + grid: { + visible: false + }, + domainLine: { + visible: false + } + } + ], + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + dotTypeField: 'quarter', + labelPosition: 'top-bottom', + dot: { + style: { + size: 12, + fill: (datum: Datum) => String((datum as Record).color ?? '') + } + }, + title: { + style: { + fill: '#2e2f32', + fontSize: 14, + fontWeight: 600 + } + }, + subTitle: { + style: { + fill: '#6b6f76', + fontSize: 12, + lineHeight: 18 + } + } + } + ], + tooltip: { + mark: { + title: { + value: (datum?: Datum) => getDatumString(datum, 'title') + }, + content: [ + { + key: '季度', + value: (datum?: Datum) => getDatumString(datum, 'quarter') + }, + { + key: '内容', + value: (datum?: Datum) => getDatumString(datum, 'detail') + } + ] + } + } +}; + +declare global { + interface Window { + vchart?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchart = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/icon-demo-vertical.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/icon-demo-vertical.ts new file mode 100644 index 0000000000..9fd76d90a4 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/icon-demo-vertical.ts @@ -0,0 +1,146 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +/** + * Icon 功能演示 - 垂直布局示例 + * 展示垂直布局时 iconMark 与 title 关于 lineMark 对称的效果 + */ + +const timelineData = [ + { + id: '1', + year: '2021', + title: '产品发布', + detail: '发布第一代产品,获得市场认可', + icon: 'star', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2022', + title: '技术突破', + detail: '核心技术获得重大突破', + icon: 'triangleUp', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2023', + title: '市场扩展', + detail: '业务覆盖全国主要城市', + icon: 'diamond', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2024', + title: '国际化', + detail: '进军国际市场,开启新篇章', + icon: 'cross', + time: 4, + color: '#9B59B6' + } +]; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'icon-demo-vertical', + direction: 'vertical', + padding: { + left: 200, + right: 200, + top: 60, + bottom: 60 + }, + background: '#f5f5f5', + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + title: { + visible: true, + text: 'Icon 功能演示 - 垂直布局', + subtext: 'iconMark 与 title 关于 lineMark 对称显示', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + }, + subtextStyle: { + fontSize: 14, + fill: '#666' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + iconField: 'icon', + labelPosition: 'left-right', + dotLabelGap: 8, + dot: { + style: { + size: 10, + fill: (datum: Datum) => String((datum as Record).color ?? '#4A90E2'), + stroke: '#fff', + lineWidth: 2 + } + }, + icon: { + style: { + size: 24, + fill: (datum: Datum) => String((datum as Record).color ?? '#4A90E2') + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12, + lineHeight: 18 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +declare global { + interface Window { + vchart?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchart = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/icon-demo.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/icon-demo.ts new file mode 100644 index 0000000000..327a9cb992 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/icon-demo.ts @@ -0,0 +1,145 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +/** + * Icon 功能演示 - 水平布局示例 + * 展示 iconMark 与 title 关于 lineMark 对称的效果 + */ + +const timelineData = [ + { + id: '1', + year: '2021', + title: '产品发布', + detail: '发布第一代产品,获得市场认可', + icon: 'star', + time: 1, + color: '#4A90E2' + }, + { + id: '2', + year: '2022', + title: '技术突破', + detail: '核心技术获得重大突破', + icon: 'triangleUp', + time: 2, + color: '#50C8C8' + }, + { + id: '3', + year: '2023', + title: '市场扩展', + detail: '业务覆盖全国主要城市', + icon: 'diamond', + time: 3, + color: '#F5A623' + }, + { + id: '4', + year: '2024', + title: '国际化', + detail: '进军国际市场,开启新篇章', + icon: 'cross', + time: 4, + color: '#9B59B6' + } +]; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'icon-demo', + direction: 'horizontal', + padding: { + left: 60, + right: 60, + top: 120, + bottom: 120 + }, + background: '#f5f5f5', + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + title: { + visible: true, + text: 'Icon 功能演示', + subtext: 'iconMark 与 title 关于 lineMark 对称显示', + style: { + fontSize: 24, + fontWeight: 'bold', + fill: '#333' + }, + subtextStyle: { + fontSize: 14, + fill: '#666' + } + }, + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + eventField: 'title', + subTitleField: 'detail', + labelPosition: 'top-bottom', + dotLabelGap: 8, + dot: { + style: { + size: 10, + fill: (datum: Datum) => String((datum as Record).color ?? '#4A90E2'), + stroke: '#fff', + lineWidth: 2 + } + }, + icon: { + style: { + size: 24, + fill: (datum: Datum) => String((datum as Record).color ?? '#4A90E2') + } + }, + title: { + style: { + fill: '#333', + fontSize: 14, + fontWeight: 'bold' + } + }, + subTitle: { + style: { + fill: '#666', + fontSize: 12, + lineHeight: 18 + } + }, + line: { + style: { + stroke: '#c0c3c7', + lineWidth: 2 + } + } + } + ] +}; + +declare global { + interface Window { + vchart?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchart = cs; +}; + +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/vertical.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/vertical.ts new file mode 100644 index 0000000000..0972a4d9a0 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/timeline/vertical.ts @@ -0,0 +1,158 @@ +import { VChart, type Datum } from '@visactor/vchart'; +import { registerTimelineChart } from '../../../../../src'; +import type { ITimelineChartSpec } from '../../../../../src/charts/timeline'; + +const timelineData = [ + { + id: 'alpha-kickoff', + project: 'Project Alpha', + group: '研发', + stage: '立项', + time: new Date('2024-01-02').getTime(), + event: '立项', + detail: '需求确认' + }, + { + id: 'alpha-design', + project: 'Project Alpha', + group: '研发', + stage: '设计', + time: new Date('2024-02-01').getTime(), + event: '设计', + detail: '视觉/交互' + }, + { + id: 'alpha-release', + project: 'Project Alpha', + group: '研发', + stage: '发布', + time: new Date('2024-03-01').getTime(), + event: '发布', + detail: '对外上线' + }, + { + id: 'beta-warmup', + project: 'Project Beta', + group: '运营', + stage: '预热', + time: new Date('2024-01-06').getTime(), + event: '预热', + detail: '内容投放' + }, + { + id: 'beta-campaign', + project: 'Project Beta', + group: '运营', + stage: '活动', + time: new Date('2024-02-05').getTime(), + event: '活动', + detail: '主站曝光' + }, + { + id: 'beta-review', + project: 'Project Beta', + group: '运营', + stage: '复盘', + time: new Date('2024-03-05').getTime(), + event: '复盘', + detail: '指标总结' + } +]; + +const getDatumString = (datum: Datum | undefined, key: string) => { + if (!datum || typeof datum !== 'object') { + return ''; + } + const value = (datum as Record)[key]; + return typeof value === 'string' ? value : ''; +}; + +const spec: ITimelineChartSpec = { + type: 'timeline', + name: 'timeline-vertical', + direction: 'vertical', + data: [ + { + id: 'timeline-data', + values: timelineData + } + ], + axes: [ + { + orient: 'left', + type: 'time', + label: { + // formatMethod: (value: string | string[]) => { + // const raw = Array.isArray(value) ? value[0] : value; + // const timeValue = Number(raw); + // if (Number.isFinite(timeValue)) { + // return new Date(timeValue).toLocaleDateString(); + // } + // return raw ? String(raw) : ''; + // } + } + } + ], + series: [ + { + type: 'event', + dataId: 'timeline-data', + timeField: 'time', + seriesField: 'group', + eventField: 'event', + dotTypeField: 'stage', + title: { + style: { + fill: '#2e2f32' + } + }, + subTitle: { + style: { + fill: '#6b6f76', + dy: 6 + } + } + } + ], + tooltip: { + mark: { + title: { + value: (datum?: Datum) => getDatumString(datum, 'event') + }, + content: [ + { + key: '项目', + value: (datum?: Datum) => getDatumString(datum, 'project') + }, + { + key: '阶段', + value: (datum?: Datum) => getDatumString(datum, 'stage') + }, + { + key: '说明', + value: (datum?: Datum) => getDatumString(datum, 'detail') + } + ] + } + } +}; + +declare global { + interface Window { + vchart?: VChart; + } +} + +const run = () => { + registerTimelineChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + cs.renderSync(); + window.vchart = cs; +}; + +run(); diff --git a/packages/vchart-extension/src/charts/ranking-bar/ranking-bar.ts b/packages/vchart-extension/src/charts/ranking-bar/ranking-bar.ts index fe7e3ed08d..e7447658b8 100644 --- a/packages/vchart-extension/src/charts/ranking-bar/ranking-bar.ts +++ b/packages/vchart-extension/src/charts/ranking-bar/ranking-bar.ts @@ -1,4 +1,4 @@ -import { IRankingBarSpec } from './interface'; +import type { IRankingBarSpec } from './interface'; import { VChart, BaseChart } from '@visactor/vchart'; import { RankingBarChartSpecTransformer } from './ranking-bar-transformer'; diff --git a/packages/vchart-extension/src/charts/ranking-list/constant.ts b/packages/vchart-extension/src/charts/ranking-list/constant.ts index eaf063834e..797df516bd 100644 --- a/packages/vchart-extension/src/charts/ranking-list/constant.ts +++ b/packages/vchart-extension/src/charts/ranking-list/constant.ts @@ -1,4 +1,4 @@ -import { IRankingListSpec } from './interface'; +import type { IRankingListSpec } from './interface'; const cornerRadius = 5; const animationDuration = 1000; diff --git a/packages/vchart-extension/src/charts/ranking-list/ranking-list.ts b/packages/vchart-extension/src/charts/ranking-list/ranking-list.ts index 5b9c9020b2..272046cc0c 100644 --- a/packages/vchart-extension/src/charts/ranking-list/ranking-list.ts +++ b/packages/vchart-extension/src/charts/ranking-list/ranking-list.ts @@ -1,4 +1,4 @@ -import { IRankingListSpec } from './interface'; +import type { IRankingListSpec } from './interface'; import { VChart, BaseChart, BarChart } from '@visactor/vchart'; import { RankingListChartSpecTransformer } from './ranking-list-transformer'; diff --git a/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde-transformer.ts b/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde-transformer.ts index 5c4c1e77cf..644e3012d5 100644 --- a/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde-transformer.ts +++ b/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde-transformer.ts @@ -1,7 +1,7 @@ -import { Datum } from '@visactor/vchart/src/typings'; +import type { Datum } from '@visactor/vchart/src/typings'; import type { ISequenceScatterKDESpec } from './interface'; import { CommonChartSpecTransformer } from '@visactor/vchart'; -import { Point } from './interface'; +import type { Point } from './interface'; import { calculateKDE } from './utils'; const DATA_KEY = 'dataKey'; @@ -83,7 +83,7 @@ export class SequenceScatterKDEChartSpecTransformer extends CommonChartSpecTrans type: 'text', dataIndex: 1, style: { - text: (datum: Datum) => datum['iter'], + text: (datum: Datum) => datum.iter, x: 10, y: () => 10, textBaseline: 'top', diff --git a/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde.ts b/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde.ts index c72061b5b2..0b02909198 100644 --- a/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde.ts +++ b/packages/vchart-extension/src/charts/sequence-scatter-kde/sequence-scatter-kde.ts @@ -1,4 +1,4 @@ -import { ISequenceScatterKDESpec } from './interface'; +import type { ISequenceScatterKDESpec } from './interface'; import { VChart, BaseChart, ScatterChart } from '@visactor/vchart'; import { SequenceScatterKDEChartSpecTransformer } from './sequence-scatter-kde-transformer'; diff --git a/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link-transformer.ts b/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link-transformer.ts index a40644337b..95f2be3f76 100644 --- a/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link-transformer.ts +++ b/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link-transformer.ts @@ -1,4 +1,4 @@ -import { Datum } from '@visactor/vchart/src/typings'; +import type { Datum } from '@visactor/vchart/src/typings'; import type { ISequenceScatterLinkSpec } from './interface'; import { CommonChartSpecTransformer } from '@visactor/vchart'; @@ -11,7 +11,7 @@ export class SequenceScatterLinkChartSpecTransformer extends CommonChartSpecTran transformSpec(spec: any): void { super.transformSpec(spec); const dataSpecs = processSequenceData(spec as unknown as ISequenceScatterLinkSpec); - const showTooltip = spec.taskType === 'neighborhood' ? false : true; + const showTooltip = spec.taskType !== 'neighborhood'; spec.type = 'common'; @@ -76,9 +76,8 @@ export class SequenceScatterLinkChartSpecTransformer extends CommonChartSpecTran lineDash: (datum: { type: string }) => { if (datum.type === 'same_type') { return [0, 0]; - } else { - return [3, 2]; } + return [3, 2]; }, lineWidth: 0.8, strokeOpacity: 0.6 @@ -122,7 +121,7 @@ export class SequenceScatterLinkChartSpecTransformer extends CommonChartSpecTran visible: true, style: { visible: () => { - return spec.taskType == 'neighborhood'; + return spec.taskType === 'neighborhood'; }, type: 'text', fontFamily: 'Console', diff --git a/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link.ts b/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link.ts index f65c7b5505..3fd1626e90 100644 --- a/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link.ts +++ b/packages/vchart-extension/src/charts/sequence-scatter-link/sequence-scatter-link.ts @@ -1,4 +1,4 @@ -import { ISequenceScatterLinkSpec } from './interface'; +import type { ISequenceScatterLinkSpec } from './interface'; import { VChart, BaseChart, ScatterChart } from '@visactor/vchart'; import { SequenceScatterLinkChartSpecTransformer } from './sequence-scatter-link-transformer'; diff --git a/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel-transformer.ts b/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel-transformer.ts index cb97d6c0f3..c0c1246d36 100644 --- a/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel-transformer.ts +++ b/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel-transformer.ts @@ -1,4 +1,4 @@ -import { Datum } from '@visactor/vchart/src/typings'; +import type { Datum } from '@visactor/vchart/src/typings'; import type { ISequenceScatterPixelSpec } from './interface'; import { CommonChartSpecTransformer } from '@visactor/vchart'; import { processSequenceData } from './utils'; @@ -71,7 +71,7 @@ export class SequenceScatterPixelChartSpecTransformer extends CommonChartSpecTra type: 'text', dataIndex: 1, style: { - text: (datum: Datum) => datum['inter'], + text: (datum: Datum) => datum.inter, x: 50, y: () => 10, textBaseline: 'top', diff --git a/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel.ts b/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel.ts index 00697a4203..234d512ee0 100644 --- a/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel.ts +++ b/packages/vchart-extension/src/charts/sequence-scatter-pixel/sequence-scatter-pixel.ts @@ -1,4 +1,4 @@ -import { ISequenceScatterPixelSpec } from './interface'; +import type { ISequenceScatterPixelSpec } from './interface'; import { VChart, BaseChart, ScatterChart } from '@visactor/vchart'; import { SequenceScatterPixelChartSpecTransformer } from './sequence-scatter-pixel-transformer'; diff --git a/packages/vchart-extension/src/charts/sequence-scatter-pixel/utils.ts b/packages/vchart-extension/src/charts/sequence-scatter-pixel/utils.ts index 1b316088c1..313a45a7a2 100644 --- a/packages/vchart-extension/src/charts/sequence-scatter-pixel/utils.ts +++ b/packages/vchart-extension/src/charts/sequence-scatter-pixel/utils.ts @@ -1,4 +1,4 @@ -import { ISequenceScatterPixelSpec } from './interface'; +import type { ISequenceScatterPixelSpec } from './interface'; import { BACKGROUND_KEY, DATA_KEY } from './constant'; // 将RGB三元组数组转换为Canvas可用的imageData diff --git a/packages/vchart-extension/src/charts/timeline/index.ts b/packages/vchart-extension/src/charts/timeline/index.ts new file mode 100644 index 0000000000..444d0d4b42 --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/index.ts @@ -0,0 +1,4 @@ +export * from './interface'; +export * from './timeline'; +export type { IEventSeriesSpec, IEventSeriesTheme } from './series/interface'; +export { registerEventSeries } from './series/event-series'; diff --git a/packages/vchart-extension/src/charts/timeline/interface.ts b/packages/vchart-extension/src/charts/timeline/interface.ts new file mode 100644 index 0000000000..2a2ac33fd3 --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/interface.ts @@ -0,0 +1,10 @@ +import type { IChartExtendsSeriesSpec, IChartSpec, ICartesianAxisSpec } from '@visactor/vchart'; +import type { IEventSeriesSpec } from './series/interface'; + +export interface ITimelineChartSpec + extends IChartSpec, + Omit, 'type' | 'title'> { + type: 'timeline'; + series?: IEventSeriesSpec[]; + axes?: ICartesianAxisSpec[]; +} diff --git a/packages/vchart-extension/src/charts/timeline/series/constant.ts b/packages/vchart-extension/src/charts/timeline/series/constant.ts new file mode 100644 index 0000000000..fa45bedd0b --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/series/constant.ts @@ -0,0 +1 @@ +export const EVENT_SERIES_TYPE = 'event'; diff --git a/packages/vchart-extension/src/charts/timeline/series/event-series.ts b/packages/vchart-extension/src/charts/timeline/series/event-series.ts new file mode 100644 index 0000000000..a2034a30f0 --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/series/event-series.ts @@ -0,0 +1,634 @@ +import type { StringOrNumber } from '@visactor/vchart'; +import { + AttributeLevel, + CartesianSeries, + Factory, + MarkTypeEnum, + SeriesMarkNameEnum, + baseSeriesMark, + registerSymbolMark, + registerTextMark, + registerLineMark, + registerPathMark, + STATE_VALUE_ENUM, + type Datum, + type IMark, + type IPoint, + type SeriesMarkMap, + BaseSeriesSpecTransformer +} from '@visactor/vchart'; +import { EVENT_SERIES_TYPE } from './constant'; +import type { IEventSeriesSpec, LabelPosition } from './interface'; +import { event } from './theme'; +import { isValid } from '@visactor/vutils'; + +type AxisHelper = { + isContinuous?: boolean; + getSpec?: () => { type?: string }; + dataToPosition?: (values: unknown[], cfg?: { bandPosition?: number }) => number; + valueToPosition?: (value: unknown) => number; +}; + +const eventSeriesMark = { + ...baseSeriesMark, + [SeriesMarkNameEnum.line]: { name: SeriesMarkNameEnum.line, type: MarkTypeEnum.line }, + [SeriesMarkNameEnum.dot]: { name: SeriesMarkNameEnum.dot, type: MarkTypeEnum.symbol }, + icon: { name: 'icon', type: MarkTypeEnum.symbol }, + [SeriesMarkNameEnum.title]: { name: SeriesMarkNameEnum.title, type: MarkTypeEnum.text }, + [SeriesMarkNameEnum.subTitle]: { name: SeriesMarkNameEnum.subTitle, type: MarkTypeEnum.text }, + arrow: { name: 'arrow', type: MarkTypeEnum.path } +} as any; + +export class EventSeries extends CartesianSeries { + static readonly type: string = EVENT_SERIES_TYPE; + type = EVENT_SERIES_TYPE as any; + + static readonly mark: SeriesMarkMap = eventSeriesMark; + static readonly builtInTheme = { event }; + static readonly transformerConstructor = BaseSeriesSpecTransformer as any; + readonly transformerConstructor = BaseSeriesSpecTransformer as any; + + readonly coordinate: 'cartesian' = 'cartesian'; + + protected declare _spec: T; + + private _dotMark?: IMark; + private _iconMark?: IMark; + private _titleMark?: IMark; + private _subTitleMark?: IMark; + private _axisLineMark?: IMark; + private _arrowMark?: IMark; + + private _timeField?: string; + private _eventField?: string; + private _subTitleField?: string; + private _iconField?: string; + private _labelPosition?: LabelPosition; + + setAttrFromSpec(): void { + super.setAttrFromSpec(); + this.setSeriesField(this._spec.seriesField); + this._timeField = this._spec.timeField as string | undefined; + this._eventField = this._spec.eventField; + this._subTitleField = this._spec.subTitleField; + this._iconField = this._spec.iconField; + this._labelPosition = this._spec.labelPosition; + } + + getDimensionField(): string[] { + return this._timeField ? [this._timeField] : []; + } + + getMeasureField(): string[] { + const fields: string[] = []; + if (this._eventField) { + fields.push(this._eventField); + } + if (this._subTitleField) { + fields.push(this._subTitleField); + } + return fields; + } + + initMark(): void { + this._axisLineMark = this._createMark(EventSeries.mark.line, { + isSeriesMark: true, + groupKey: this._seriesField + }); + this._arrowMark = this._createMark((EventSeries.mark as any).arrow, { + isSeriesMark: true + }); + + this._dotMark = this._createMark(EventSeries.mark.dot, { + isSeriesMark: true + }); + + this._iconMark = this._createMark((EventSeries.mark as any).icon, { + isSeriesMark: true + }); + this._titleMark = this._createMark(EventSeries.mark.title); + + this._subTitleMark = this._createMark(EventSeries.mark.subTitle); + } + + protected initTooltip() { + super.initTooltip(); + + // 将 dot、icon 和 arrow mark 添加到 tooltip 触发器集合中 + this._dotMark && this._tooltipHelper.activeTriggerSet.mark.add(this._dotMark); + this._titleMark && this._tooltipHelper.activeTriggerSet.mark.add(this._titleMark); + this._subTitleMark && this._tooltipHelper.activeTriggerSet.mark.add(this._subTitleMark); + this._iconMark && this._tooltipHelper.activeTriggerSet.mark.add(this._iconMark); + this._arrowMark && this._tooltipHelper.activeTriggerSet.mark.add(this._arrowMark); + } + + initMarkStyle(): void { + if (this._axisLineMark) { + this.setMarkStyle( + this._axisLineMark, + { + points: this._getAxisPoints.bind(this), + visible: this._isFirstDataInGroup.bind(this), + stroke: '#c0c3c7', + lineWidth: 1 + }, + STATE_VALUE_ENUM.STATE_NORMAL, + AttributeLevel.Series + ); + } + + const dotSpec = this._spec.dot as { style?: { size?: number; fill?: unknown } } | undefined; + const dotSize = typeof dotSpec?.style?.size === 'number' ? dotSpec.style.size : 8; + + const titleSpec = this._spec.title; + const titleStyle = titleSpec?.style ?? {}; + const subTitleSpec = this._spec.subTitle; + const subTitleStyle = subTitleSpec?.style ?? {}; + + // 获取 title 字体大小,用于计算 subTitle 的位置 + const titleFontSize = typeof titleStyle.fontSize === 'number' ? titleStyle.fontSize : 14; + + // dot 和 label 之间的间距 + const labelOffset = dotSize / 2 + (titleSpec?.offset ?? 6); + + if (this._dotMark) { + this.setMarkStyle( + this._dotMark, + { + x: (datum: Datum) => this._getPoint(datum).x, + y: (datum: Datum) => this._getPoint(datum).y, + size: dotSize, + fill: dotSpec?.style?.fill ?? this.getColorAttribute() + }, + STATE_VALUE_ENUM.STATE_NORMAL, + AttributeLevel.Series + ); + } + + if (this._arrowMark) { + this.setMarkStyle( + this._arrowMark, + { + path: (datum: Datum) => this._getArrowPath(datum), + fill: dotSpec?.style?.fill ?? this.getColorAttribute() + }, + STATE_VALUE_ENUM.STATE_NORMAL, + AttributeLevel.Series + ); + } + + if (this._titleMark) { + this.setMarkStyle( + this._titleMark, + { + fontSize: 14, + ...titleStyle, + x: (datum: Datum) => this._getTitlePosition(datum, labelOffset).x, + y: (datum: Datum) => this._getTitlePosition(datum, labelOffset).y, + textAlign: (datum: Datum) => this._getLabelTextAlign(datum), + textBaseline: (datum: Datum) => this._getLabelTextBaseline(datum, true), + text: (datum: Datum) => this._getDatumString(datum, this._eventField) + }, + STATE_VALUE_ENUM.STATE_NORMAL, + AttributeLevel.Series + ); + } + + if (this._subTitleMark) { + this.setMarkStyle( + this._subTitleMark, + { + ...subTitleStyle, + x: (datum: Datum) => this._getSubTitlePosition(datum, labelOffset, titleFontSize).x, + y: (datum: Datum) => this._getSubTitlePosition(datum, labelOffset, titleFontSize).y, + textAlign: (datum: Datum) => this._getLabelTextAlign(datum), + textBaseline: (datum: Datum) => this._getLabelTextBaseline(datum, false), + text: (datum: Datum) => this._getDatumString(datum, this._subTitleField) + }, + STATE_VALUE_ENUM.STATE_NORMAL, + AttributeLevel.Series + ); + } + + // icon 和 line 之间的间距 + + const iconSpec = this._spec.icon as { offset?: number; style?: { size?: number; fill?: unknown } } | undefined; + const iconSize = typeof iconSpec?.style?.size === 'number' ? iconSpec.style.size : 20; + const iconOffset = (isValid(iconSpec?.offset) ? iconSpec.offset + dotSize / 2 : labelOffset) + iconSize / 2; + + if (this._iconMark) { + this.setMarkStyle( + this._iconMark, + { + x: (datum: Datum) => this._getIconPosition(datum, iconOffset).x, + y: (datum: Datum) => this._getIconPosition(datum, iconOffset).y, + size: iconSize, + fill: iconSpec?.style?.fill ?? this.getColorAttribute(), + shape: (datum: Datum) => this._getDatumString(datum, this._iconField) || 'circle' + }, + STATE_VALUE_ENUM.STATE_NORMAL, + AttributeLevel.Series + ); + } + } + + /** + * 根据数据索引判断标签应该放在哪一侧 + * @returns 'primary' 表示 top/left,'secondary' 表示 bottom/right + */ + private _getLabelSide(datum: Datum): 'primary' | 'secondary' { + const data = this._getViewDataList(); + const index = data.indexOf(datum); + const position = this._labelPosition; + + if (this.direction === 'vertical') { + // vertical 布局: left/right + switch (position) { + case 'left': + return 'primary'; + case 'right': + return 'secondary'; + case 'left-right': + return index % 2 === 0 ? 'primary' : 'secondary'; + case 'right-left': + return index % 2 === 0 ? 'secondary' : 'primary'; + default: + return 'primary'; // 默认 left + } + } else { + // horizontal 布局: top/bottom + switch (position) { + case 'top': + return 'primary'; + case 'bottom': + return 'secondary'; + case 'top-bottom': + return index % 2 === 0 ? 'primary' : 'secondary'; + case 'bottom-top': + return index % 2 === 0 ? 'secondary' : 'primary'; + default: + return 'primary'; // 默认 top + } + } + } + + /** + * 获取标签的文本对齐方式 + */ + private _getLabelTextAlign(datum: Datum): 'left' | 'right' | 'center' { + if (this.direction === 'vertical') { + const side = this._getLabelSide(datum); + return side === 'primary' ? 'right' : 'left'; + } + return 'center'; + } + + /** + * 获取标签的文本基线 + */ + private _getLabelTextBaseline(datum: Datum, isTitle: boolean): 'top' | 'bottom' | 'middle' { + if (this.direction === 'vertical') { + // vertical 布局时:title 用 top,subTitle 用 top + return 'middle'; + } + const side = this._getLabelSide(datum); + return side === 'primary' ? 'bottom' : 'top'; + } + + /** + * 获取 title 的位置 + */ + private _getTitlePosition(datum: Datum, offset: number): IPoint { + const point = this._getPoint(datum); + const side = this._getLabelSide(datum); + + if (this.direction === 'vertical') { + // vertical: left/right,标签垂直排列 + return { + x: side === 'primary' ? point.x - offset : point.x + offset, + y: point.y + }; + } + // horizontal: top/bottom + return { + x: point.x, + y: side === 'primary' ? point.y - offset : point.y + offset + }; + } + + /** + * 获取 subTitle 的位置 + */ + private _getSubTitlePosition(datum: Datum, offset: number, titleFontSize: number): IPoint { + const point = this._getPoint(datum); + const side = this._getLabelSide(datum); + const gap = this._spec.title?.subTitleGap ?? 4; + + if (this.direction === 'vertical') { + // vertical: left/right,subTitle 在 title 下方 + // offset 是 title 的偏移,subTitle 需要在 title 基础上再向下偏移 + const titleStyle = this._spec.title?.style ?? {}; + const titleLineHeight = typeof titleStyle.lineHeight === 'number' ? titleStyle.lineHeight : titleFontSize * 1.2; + + return { + x: side === 'primary' ? point.x - offset : point.x + offset, + y: point.y + titleLineHeight + gap + }; + } + // horizontal: top/bottom + return { + x: point.x, + y: side === 'primary' ? point.y - (offset + titleFontSize + gap) : point.y + (offset + titleFontSize + gap) + }; + } + + /** + * 获取 icon 的位置 + * icon 与 title 关于 lineMark 对称 + */ + private _getIconPosition(datum: Datum, offset: number): IPoint { + const point = this._getPoint(datum); + const side = this._getLabelSide(datum); + + if (this.direction === 'vertical') { + // vertical: 当 title 在 right 时,icon 在 left;当 title 在 left 时,icon 在 right + return { + x: side === 'primary' ? point.x + offset : point.x - offset, + y: point.y + }; + } + // horizontal: 当 title 在 top 时,icon 在 bottom;当 title 在 bottom 时,icon 在 top + return { + x: point.x, + y: side === 'primary' ? point.y + offset : point.y - offset + }; + } + + private _getViewDataList(): Datum[] { + return this.getViewData()?.latestData ?? []; + } + + private _getPoint(datum: Datum): IPoint { + if (this.direction === 'vertical') { + // vertical 布局:x 轴是分类方向(seriesField),y 轴是时间方向(timeField) + const x = this._getPositionFromAxis(datum, this.getXAxisHelper(), this._seriesField); + const y = this._getPositionFromAxis(datum, this.getYAxisHelper(), this._timeField); + return { x, y }; + } + + // horizontal 布局:x 轴是时间方向(timeField),y 轴是分类方向(seriesField) + const x = this._getPositionFromAxis(datum, this.getXAxisHelper(), this._timeField); + const y = this._getPositionFromAxis(datum, this.getYAxisHelper(), this._seriesField); + return { x, y }; + } + + /** + * 根据轴的类型计算位置 + * @param datum 数据项 + * @param axisHelper 轴助手 + * @param field 字段名 + * @returns 位置值 + */ + private _getPositionFromAxis(datum: Datum, axisHelper: AxisHelper | undefined, field?: string): number { + if (!axisHelper) { + // 如果没有轴助手,使用默认的中间位置 + return this._getDefaultPosition(field); + } + + if (!field || !(field in datum)) { + // 如果没有字段或数据中没有该字段,使用默认位置 + return this._getDefaultPosition(field); + } + + const value = (datum as Record)[field]; + const axisType = axisHelper.getSpec?.()?.type; + + // 根据轴类型选择不同的计算方式 + if (axisType === 'band' || !axisHelper.isContinuous) { + // band 轴:使用 dataToPosition,传入值数组 + return axisHelper.dataToPosition?.([value], { bandPosition: 0.5 }) ?? this._getDefaultPosition(field); + } + + // linear/time 轴:使用 valueToPosition,直接传入值 + return axisHelper.valueToPosition?.(value) ?? this._getDefaultPosition(field); + } + + onXAxisHelperUpdate(): void { + super.onXAxisHelperUpdate?.(); + this.onMarkPositionUpdate(); + } + + onYAxisHelperUpdate(): void { + super.onYAxisHelperUpdate?.(); + this.onMarkPositionUpdate(); + } + + /** + * 获取默认位置(当没有轴或字段时使用) + */ + private _getDefaultPosition(field?: string): number { + const rect = this._region.getLayoutRect(); + + // 如果没有 seriesField,说明没有分类轴,返回中心位置 + if (!this._seriesField || field === this._seriesField) { + const isHorizontal = this.direction !== 'vertical'; + // horizontal 布局:返回垂直中心(y方向) + // vertical 布局:返回水平中心(x方向) + return isHorizontal ? rect.height * 0.5 : rect.width * 0.5; + } + + // 对于时间轴,返回区域起点 + return 0; + } + + private _getAxisPoints(datum: Datum): IPoint[] { + const rect = this._region.getLayoutRect(); + + if (this.direction === 'vertical') { + // vertical 布局:轴是垂直的,x 位置根据 seriesField 计算 + const x = this._getPositionFromAxis(datum, this.getXAxisHelper(), this._seriesField); + return [ + { x, y: 0 }, + { x, y: rect.height } + ]; + } + + // horizontal 布局:轴是水平的,y 位置根据 seriesField 计算 + const y = this._getPositionFromAxis(datum, this.getYAxisHelper(), this._seriesField); + return [ + { x: 0, y }, + { x: rect.width, y } + ]; + } + + private _isFirstDataInGroup(datum: Datum): boolean { + if (!this._seriesField) { + const data = this._getViewDataList(); + return data.indexOf(datum) === 0; + } + + const data = this._getViewDataList(); + const categoryValue = (datum as Record)[this._seriesField]; + + // 找到该分类中的第一条数据 + const firstInGroup = data.find( + d => this._seriesField && (d as Record)[this._seriesField] === categoryValue + ); + return datum === firstInGroup; + } + + private _getNextDatum(datum: Datum): Datum | null { + const data = this._getViewDataList(); + const currentIndex = data.indexOf(datum); + + if (currentIndex === -1 || currentIndex === data.length - 1) { + return null; + } + + if (!this._seriesField) { + return data[currentIndex + 1]; + } + + const categoryValue = (datum as Record)[this._seriesField]; + + // 在同一分组中找到下一个数据 + for (let i = currentIndex + 1; i < data.length; i++) { + const nextDatum = data[i]; + if ((nextDatum as Record)[this._seriesField] === categoryValue) { + return nextDatum; + } + } + + return null; + } + + private _getPreviousDatum(datum: Datum): Datum | null { + const data = this._getViewDataList(); + const currentIndex = data.indexOf(datum); + + if (currentIndex === -1 || currentIndex === 0) { + return null; + } + + if (!this._seriesField) { + return data[currentIndex - 1]; + } + + const categoryValue = (datum as Record)[this._seriesField]; + + // 在同一分组中找到上一个数据 + for (let i = currentIndex - 1; i >= 0; i--) { + const prevDatum = data[i]; + if ((prevDatum as Record)[this._seriesField] === categoryValue) { + return prevDatum; + } + } + + return null; + } + + private _getArrowPath(datum: Datum): string { + const point = this._getPoint(datum); + const nextDatum = this._getNextDatum(datum); + const prevDatum = this._getPreviousDatum(datum); + + const arrowThickness = this._spec.arrow?.thickness ?? 16; + const rect = this._region.getLayoutRect(); + + if (this.direction === 'vertical') { + const axisHelper = this.getYAxisHelper(); + const inverse = axisHelper.isInverse(); + const startPoint = prevDatum + ? { + x: point.x, + y: (this._getPoint(prevDatum).y + point.y) / 2 + } + : { x: point.x, y: inverse ? 0 : rect.height }; + const endPoint = nextDatum + ? { + x: point.x, + y: (this._getPoint(nextDatum).y + point.y) / 2 + } + : { x: point.x, y: inverse ? rect.height : 0 }; + const tag = inverse ? 1 : -1; + + const arrowHeight = arrowThickness / 3; + const arrowWidth = arrowThickness / 2; + + return `M ${startPoint.x - arrowWidth} ${startPoint.y} L ${startPoint.x} ${startPoint.y + tag * arrowHeight} L ${ + startPoint.x + arrowWidth + } ${startPoint.y} + L ${endPoint.x + arrowWidth} ${endPoint.y - tag * arrowHeight} + L ${endPoint.x} ${endPoint.y} + L ${endPoint.x - arrowWidth} ${endPoint.y - tag * arrowHeight} Z`; + } + + const axisHelper = this.getXAxisHelper(); + const inverse = axisHelper.isInverse(); + const startPoint = prevDatum + ? { + x: (this._getPoint(prevDatum).x + point.x) / 2, + y: point.y + } + : { x: inverse ? rect.width : 0, y: point.y }; + const endPoint = nextDatum + ? { + x: (this._getPoint(nextDatum).x + point.x) / 2, + y: point.y + } + : { x: inverse ? 0 : rect.width, y: point.y }; + const tag = inverse ? -1 : 1; + + const arrowHeight = arrowThickness / 2; + const arrowWidth = arrowThickness / 3; + + return `M ${startPoint.x} ${startPoint.y - arrowHeight} L ${startPoint.x + tag * arrowWidth} ${startPoint.y} L ${ + startPoint.x + } ${startPoint.y + arrowHeight} + L ${endPoint.x - tag * arrowWidth} ${endPoint.y + arrowHeight} + L ${endPoint.x} ${endPoint.y} + L ${endPoint.x - tag * arrowWidth} ${endPoint.y - arrowHeight} Z`; + } + + private _getDatumString(datum: Datum | undefined, field?: string): string { + if (!datum || !field) { + return ''; + } + const value = (datum as Record)[field]; + return typeof value === 'string' ? value : value == null ? '' : String(value); + } + + valueToPosition(timeValue: StringOrNumber, eventValue?: StringOrNumber): IPoint { + if (timeValue && typeof timeValue === 'object') { + return this.dataToPosition(timeValue as Datum); + } + + const mockDatum: Record = {}; + if (this._timeField) { + mockDatum[this._timeField] = timeValue; + } + if (this._eventField && eventValue !== undefined) { + mockDatum[this._eventField] = eventValue; + } + return this._getPoint(mockDatum as Datum); + } + + getActiveMarks(): IMark[] { + return [ + this._axisLineMark, + this._dotMark, + this._iconMark, + this._arrowMark, + this._titleMark, + this._subTitleMark + ].filter(Boolean) as IMark[]; + } +} + +export const registerEventSeries = () => { + registerSymbolMark(); + registerTextMark(); + registerLineMark(); + registerPathMark(); + Factory.registerSeries(EventSeries.type, EventSeries); +}; diff --git a/packages/vchart-extension/src/charts/timeline/series/interface.ts b/packages/vchart-extension/src/charts/timeline/series/interface.ts new file mode 100644 index 0000000000..e7e94671af --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/series/interface.ts @@ -0,0 +1,74 @@ +import type { + IMarkSpec, + ISymbolMarkSpec, + ITextMarkSpec, + ILineMarkSpec, + IPathMarkSpec, + ICartesianSeriesSpec +} from '@visactor/vchart'; + +/** horizontal 布局时的标题位置 */ +export type HorizontalLabelPosition = 'top' | 'bottom' | 'top-bottom' | 'bottom-top'; + +/** vertical 布局时的标题位置 */ +export type VerticalLabelPosition = 'left' | 'right' | 'left-right' | 'right-left'; + +/** 标题位置配置 */ +export type LabelPosition = HorizontalLabelPosition | VerticalLabelPosition; + +export interface IEventSeriesSpec extends ICartesianSeriesSpec, IEventSeriesTheme { + type: 'event'; + /** + * 时间字段,用于指定事件在时间轴上的位置 + */ + timeField?: string; + /** + * 事件名称字段 + */ + eventField?: string; + /** + * 事件详情字段(副标题) + */ + subTitleField?: string; + /** + * 系列字段,用于分组显示 + */ + seriesField?: string; + /** + * 图标字段,用于显示图标或图片 + */ + iconField?: string; + /** 标题和副标题的位置 */ + labelPosition?: LabelPosition; +} + +export interface IEventSeriesTheme { + /** + * 点图元配置 + */ + dot?: IMarkSpec; + /** + * 图标图元配置 + * offset: 图标相对于点的偏移距离,单位像素,正值向外偏移,负值向内偏移 + */ + icon?: IMarkSpec & { offset?: number }; + /** + * 事件标题图元配置 + * subTitleGap: 标题与副标题的间距,单位像素 + * offset: 标题相对于点的偏移距离,单位像素,正值向外偏移,负值向内偏移 + */ + title?: IMarkSpec & { subTitleGap?: number; offset?: number }; + /** + * 事件副标题图元配置 + */ + subTitle?: IMarkSpec; + /** + * 事件线图元配置 + */ + line?: IMarkSpec; + /** + * 箭头图元配置 + * thickness: 箭头的厚度,单位像素 + */ + arrow?: IMarkSpec & { thickness?: number }; +} diff --git a/packages/vchart-extension/src/charts/timeline/series/theme.ts b/packages/vchart-extension/src/charts/timeline/series/theme.ts new file mode 100644 index 0000000000..6da313417f --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/series/theme.ts @@ -0,0 +1,40 @@ +import type { IEventSeriesTheme } from './interface'; + +export const event: IEventSeriesTheme = { + dot: { + style: { + size: 8 + } + }, + icon: { + visible: false, + style: { + size: 20 + } + }, + line: { + visible: true, + style: { + stroke: '#c0c3c7', + lineWidth: 1 + } + }, + title: { + visible: true, + offset: 6, + subTitleGap: 4, + style: { + fontSize: 14 + } + }, + subTitle: { + visible: true, + style: { + fontSize: 12 + } + }, + arrow: { + visible: false, + thickness: 16 + } +}; diff --git a/packages/vchart-extension/src/charts/timeline/timeline-transformer.ts b/packages/vchart-extension/src/charts/timeline/timeline-transformer.ts new file mode 100644 index 0000000000..bd24693b2c --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/timeline-transformer.ts @@ -0,0 +1,141 @@ +import { BaseChartSpecTransformer } from '@visactor/vchart'; +import type { ICartesianAxisSpec, ICartesianLinearAxisSpec } from '@visactor/vchart'; +import type { ITimelineChartSpec } from './interface'; +import { isNil, get, merge, isObject } from '@visactor/vutils'; + +export class TimelineChartSpecTransformer< + T extends ITimelineChartSpec = ITimelineChartSpec +> extends BaseChartSpecTransformer { + protected _getDefaultSeriesSpec(spec: T): any { + return super._getDefaultSeriesSpec(spec, [ + 'timeField', + 'eventField', + 'seriesField', + 'titleField', + 'subTitleField', + 'iconField', + 'dot', + 'title', + 'subTitle', + 'icon', + 'line', + 'arrow', + 'labelPosition', + 'sortDataByAxis' + ]); + } + + protected _setDefaultXAxisSpec(spec: T): ICartesianAxisSpec { + return { + type: 'band', + orient: 'bottom', + label: { + visible: false + }, + tick: { + visible: false + }, + grid: { + visible: false + }, + domainLine: { + visible: false + }, + paddingInner: 0, + paddingOuter: 0 + } as ICartesianAxisSpec; + } + + protected _setDefaultYAxisSpec(spec: T): ICartesianAxisSpec { + return { + type: 'band', + inverse: true, + orient: 'left', + label: { + visible: false + }, + tick: { + visible: false + }, + grid: { + visible: false + }, + domainLine: { + visible: false + }, + paddingInner: 0, + paddingOuter: 0 + } as ICartesianAxisSpec; + } + + protected _transformAxisSpec(spec: T) { + if (!spec.axes) { + spec.axes = []; + } + + const haxAxes = { x: false, y: false }; + spec.axes.forEach((axis: ICartesianAxisSpec) => { + const { orient } = axis; + let defaultSpec: Partial = null; + if (orient === 'top' || orient === 'bottom') { + haxAxes.x = true; + + defaultSpec = this._setDefaultXAxisSpec(spec); + } + if (orient === 'left' || orient === 'right') { + haxAxes.y = true; + + defaultSpec = this._setDefaultYAxisSpec(spec); + } + + if (defaultSpec) { + Object.keys(defaultSpec).forEach(key => { + (axis as any)[key] = isObject((axis as any)[key]) + ? merge((defaultSpec as any)[key], (axis as any)[key]) + : isNil((axis as any)[key]) + ? (defaultSpec as any)[key] + : (axis as any)[key]; + }); + } + }); + if (!haxAxes.x) { + spec.axes.push(this._setDefaultXAxisSpec(spec)); + } + if (!haxAxes.y) { + spec.axes.push(this._setDefaultYAxisSpec(spec)); + } + } + + transformSpec(spec: T): void { + super.transformSpec(spec); + this.transformSeriesSpec(spec); + this._transformAxisSpec(spec); + + const direction = spec.direction ?? 'horizontal'; + + // 将 direction 传递给 series,并设置 xField/yField 以便轴系统收集数据 + spec.series?.forEach(seriesSpec => { + // 根据 direction 将 timeField 映射到 xField 或 yField + // 这样轴系统才能正确收集数据 + if (direction === 'vertical') { + // vertical 布局:y 轴是时间方向 + if (seriesSpec.timeField && !seriesSpec.yField) { + seriesSpec.yField = seriesSpec.timeField; + } + // x 轴是分类方向(如果没有 seriesField,创建一个虚拟字段) + if (!seriesSpec.xField) { + seriesSpec.xField = seriesSpec.seriesField || '__vchart_timeline_dummy__'; + } + } else { + // horizontal 布局:x 轴是时间方向 + if (seriesSpec.timeField && !seriesSpec.xField) { + seriesSpec.xField = seriesSpec.timeField; + } + // y 轴是分类方向(如果没有 seriesField,创建一个虚拟字段) + if (!seriesSpec.yField) { + seriesSpec.yField = seriesSpec.seriesField || '__vchart_timeline_dummy__'; + } + } + }); + } +} diff --git a/packages/vchart-extension/src/charts/timeline/timeline.ts b/packages/vchart-extension/src/charts/timeline/timeline.ts new file mode 100644 index 0000000000..445230c1ea --- /dev/null +++ b/packages/vchart-extension/src/charts/timeline/timeline.ts @@ -0,0 +1,19 @@ +import { TimelineChartSpecTransformer } from './timeline-transformer'; +import type { ITimelineChartSpec } from './interface'; +import { registerEventSeries } from './series/event-series'; +import { BaseChart, Factory, registerMarkTooltipProcessor } from '@visactor/vchart'; + +export class TimelineChart extends BaseChart { + static readonly type: string = 'timeline'; + static readonly seriesType: string = 'event'; + static readonly transformerConstructor = TimelineChartSpecTransformer; + readonly transformerConstructor = TimelineChartSpecTransformer; + readonly type: string = 'timeline'; + readonly seriesType: string = 'event'; +} + +export const registerTimelineChart = () => { + registerMarkTooltipProcessor(); + registerEventSeries(); + Factory.registerChart(TimelineChart.type, TimelineChart); +}; diff --git a/packages/vchart-extension/src/components/map-label/map-label-transformer.ts b/packages/vchart-extension/src/components/map-label/map-label-transformer.ts index 1b7f78450d..32c5782adc 100644 --- a/packages/vchart-extension/src/components/map-label/map-label-transformer.ts +++ b/packages/vchart-extension/src/components/map-label/map-label-transformer.ts @@ -1,5 +1,5 @@ import { BaseComponentSpecTransformer } from '@visactor/vchart'; -import { IMapLabelSpec, IMapLabelStyleSpec } from './type'; +import type { IMapLabelSpec, IMapLabelStyleSpec } from './type'; import { mapLabel } from './theme'; export class MapLabelSpecTransformer< diff --git a/packages/vchart-extension/src/index.ts b/packages/vchart-extension/src/index.ts index 6db81fb8fe..ae1e1b5c8e 100644 --- a/packages/vchart-extension/src/index.ts +++ b/packages/vchart-extension/src/index.ts @@ -17,6 +17,7 @@ export { register3DPlugin } from './charts/3d/plugin'; export * from './charts/pictogram'; export * from './charts/image-cloud'; export * from './charts/candlestick'; +export * from './charts/timeline'; export * from './components/series-break'; export * from './components/bar-link'; diff --git a/packages/vchart/src/vchart-all.ts b/packages/vchart/src/vchart-all.ts index a621f7955a..2f516e085c 100644 --- a/packages/vchart/src/vchart-all.ts +++ b/packages/vchart/src/vchart-all.ts @@ -94,6 +94,7 @@ VChart.useRegisters([ registerSequenceChart, registerCorrelationChart, // 优化vchart-all体积, 默认不注册 + // registerTimelineChart, // registerLiquidChart, // registerVennChart, registerCommonChart, diff --git a/specs/001-add-timeline-chart/checklists/requirements.md b/specs/001-add-timeline-chart/checklists/requirements.md new file mode 100644 index 0000000000..69feec104a --- /dev/null +++ b/specs/001-add-timeline-chart/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Timeline 图表类型 + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-01-28 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` diff --git a/specs/001-add-timeline-chart/contracts/timeline-spec.yaml b/specs/001-add-timeline-chart/contracts/timeline-spec.yaml new file mode 100644 index 0000000000..f48cc35039 --- /dev/null +++ b/specs/001-add-timeline-chart/contracts/timeline-spec.yaml @@ -0,0 +1,128 @@ +openapi: 3.0.3 +info: + title: Timeline Spec Contract + version: 1.0.0 + description: Public configuration contract for Timeline chart specifications. +paths: + /timeline/validate: + post: + summary: Validate a timeline chart spec + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TimelineSpec' + responses: + '200': + description: Validation result + content: + application/json: + schema: + type: object + properties: + valid: + type: boolean + errors: + type: array + items: + type: string +components: + schemas: + TimelineSpec: + type: object + required: + - type + - data + - layout + properties: + type: + type: string + enum: [timeline] + layout: + $ref: '#/components/schemas/TimelineLayout' + data: + $ref: '#/components/schemas/TimelineData' + TimelineLayout: + type: object + required: + - type + properties: + type: + type: string + enum: [horizontal, vertical, radial, s-curve] + options: + type: object + additionalProperties: true + TimelineData: + type: object + required: + - eventPoints + properties: + timePoints: + type: array + items: + $ref: '#/components/schemas/TimePoint' + eventPoints: + type: array + items: + $ref: '#/components/schemas/EventPoint' + series: + type: array + items: + $ref: '#/components/schemas/Series' + TimePoint: + type: object + required: + - id + - time + properties: + id: + type: string + time: + oneOf: + - type: string + - type: number + label: + type: string + meta: + type: object + additionalProperties: true + EventPoint: + type: object + required: + - id + - time + - title + properties: + id: + type: string + time: + oneOf: + - type: string + - type: number + title: + type: string + description: + type: string + seriesId: + type: string + seriesName: + type: string + icon: + type: string + meta: + type: object + additionalProperties: true + Series: + type: object + required: + - id + properties: + id: + type: string + name: + type: string + style: + type: object + additionalProperties: true diff --git a/specs/001-add-timeline-chart/data-model.md b/specs/001-add-timeline-chart/data-model.md new file mode 100644 index 0000000000..5348a78b48 --- /dev/null +++ b/specs/001-add-timeline-chart/data-model.md @@ -0,0 +1,44 @@ +# Data Model: Timeline 图表类型 + +## Entities + +### 时间点 (TimePoint) +- Fields: + - id: String + - time: Date/String/Number + - label: String (optional) + - meta: Object (optional) + +### 事件点 (EventPoint) +- Fields: + - id: String + - time: Date/String/Number + - title: String + - description: String (optional) + - seriesId: String (optional) + - seriesName: String (optional) + - icon: String (optional) + - meta: Object (optional) + +### 系列 (Series) +- Fields: + - id: String + - name: String + - style: Object (optional) + +## Spec 结构(概要) +- type: 'timeline' +- data: + - timePoints: TimePoint[] + - eventPoints: EventPoint[] + - series: Series[] (optional) +- layout: + - type: 'horizontal' | 'vertical' | 'radial' | 's-curve' + - options: Object +- tooltip/crosshair/legend: 复用现有组件结构 + +## Validation Rules +- 所有 EventPoint 必须包含可解析的 time +- layout.type 必须为支持的枚举之一 +- 多系列时,seriesId/seriesName 至少一个有效 +- timePoints 可为空,但必须支持仅 timePoints 的展示 diff --git a/specs/001-add-timeline-chart/plan.md b/specs/001-add-timeline-chart/plan.md new file mode 100644 index 0000000000..9a161f2927 --- /dev/null +++ b/specs/001-add-timeline-chart/plan.md @@ -0,0 +1,70 @@ +# Implementation Plan: Timeline 图表类型 + +**Branch**: `001-add-timeline-chart` | **Date**: 2026-01-28 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/001-add-timeline-chart/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +新增 Timeline 图表类型,支持展示事件点与时间点,并具备横向、纵向、径向、S 线等布局类型,支持多系列事件点。技术上在现有 VChart 架构内实现新的 chart 类型与布局模块,遵循跨端一致性与类型约束,提供清晰的 Spec 与示例。 + +## Technical Context + +**Language/Version**: TypeScript (strict) +**Primary Dependencies**: @visactor/vchart, @visactor/vchart-types, VRender/VGrammar(内部体系) +**Storage**: N/A +**Testing**: Jest/Vitest 单元测试 + 视觉回归 +**Target Platform**: Web(桌面/移动)、多端封装一致行为 +**Project Type**: Monorepo(Rush) +**Performance Goals**: 60fps 交互,Tooltip/Crosshair 响应 <16ms +**Constraints**: 体积控制,跨端一致性,类型严格,无控制台调试输出 +**Scale/Scope**: 新增一个图表类型及其布局/渲染/交互,不引入外部服务 + +## Constitution Check + +_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._ + +- 规范驱动:已有 spec.md,后续设计与实现遵循任务清单 +- 类型对齐:新增类型需落位 `@visactor/vchart-types` +- 代码质量:遵循 ESLint/Prettier,严格 TypeScript +- 测试:需新增单元测试与视觉回归用例 +- 文档与示例:需在 docs 站点提供示例与 API 文档 +- 跨端一致性:Tooltip/Crosshair/Legend 等交互行为需对齐 +- 许可证与依赖:不新增不兼容许可证依赖 + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-add-timeline-chart/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) + +```text +packages/vchart/ +├── src/chart/timeline/ # Timeline 图表主实现 +├── src/component/... # 复用或新增组件 +├── src/theme/... # 默认主题扩展(如需) +└── __tests__/... # 单元与视觉测试用例 + +packages/vchart-types/ +└── types/chart/timeline.d.ts # Timeline 的对外类型定义 + +packages/docs/ (或内部 docs 包) +└── docs/timeline/* # 文档与示例 +``` + +**Structure Decision**: 在 vchart 核心包内新增 timeline chart 目录;类型定义落位 vchart-types;示例与文档在 docs 包维护;测试用例与视觉回归在 vchart 内。 + +## Complexity Tracking + +No constitution violations identified for this feature. diff --git a/specs/001-add-timeline-chart/quickstart.md b/specs/001-add-timeline-chart/quickstart.md new file mode 100644 index 0000000000..13225c9ae3 --- /dev/null +++ b/specs/001-add-timeline-chart/quickstart.md @@ -0,0 +1,40 @@ +# Quickstart: Timeline 图表类型 + +## 目标 +用最小配置创建一个 Timeline 图表,展示时间点与事件点,并切换布局。 + +## 步骤 + +1. 准备数据 + - 至少包含一组事件点 + - 可选提供时间点与系列信息 + +2. 选择布局类型 + - horizontal / vertical / radial / s-curve + +3. 渲染图表 + - 使用 Timeline 图表类型与配置项 + +## 最小示例(结构示意) + +```json +{ + "type": "timeline", + "layout": { "type": "horizontal" }, + "data": { + "timePoints": [ + { "id": "t1", "time": "2025-01-01", "label": "开始" } + ], + "eventPoints": [ + { "id": "e1", "time": "2025-01-01", "title": "事件 A" } + ], + "series": [ + { "id": "s1", "name": "系列 1" } + ] + } +} +``` + +## 成功标准 +- 图表能展示时间点与事件点 +- 切换布局后视觉结构发生变化 diff --git a/specs/001-add-timeline-chart/research.md b/specs/001-add-timeline-chart/research.md new file mode 100644 index 0000000000..cc2520b931 --- /dev/null +++ b/specs/001-add-timeline-chart/research.md @@ -0,0 +1,27 @@ +# Research: Timeline 图表类型 + +## 决策与依据 + +### Decision 1: 支持四种布局类型作为首期范围 +- **Decision**: 首期支持横向、纵向、径向、S 线布局 +- **Rationale**: 覆盖信息图中最常见的时间轴呈现方式,满足多场景展示 +- **Alternatives considered**: 仅支持横纵布局(功能不足),增加更多复杂布局(成本过高) + +### Decision 2: 事件点与时间点作为独立实体 +- **Decision**: 事件点与时间点独立建模,事件点关联时间点 +- **Rationale**: 支持仅时间点或密集事件点的场景,同时增强多系列扩展性 +- **Alternatives considered**: 仅事件点(无法表达空时间点),仅时间点(无法表达事件内容) + +### Decision 3: 多系列以系列标识区分 +- **Decision**: 使用 seriesId/seriesName 区分多系列事件点 +- **Rationale**: 与现有图表体系一致,便于样式与交互对齐 +- **Alternatives considered**: 仅颜色区分(缺乏语义)、嵌套结构(复杂度更高) + +### Decision 4: 交互一致性与性能目标沿用项目规范 +- **Decision**: Tooltip/Crosshair/Legend 交互对齐全局规范,交互响应 <16ms +- **Rationale**: 符合宪法要求,减少跨端差异 +- **Alternatives considered**: 允许不同端行为(增加维护成本) + +## 未解决问题 + +无 NEEDS CLARIFICATION 项,研究阶段完成。 diff --git a/specs/001-add-timeline-chart/spec.md b/specs/001-add-timeline-chart/spec.md new file mode 100644 index 0000000000..6394fdbf00 --- /dev/null +++ b/specs/001-add-timeline-chart/spec.md @@ -0,0 +1,97 @@ +# Feature Specification: Timeline 图表类型 + +**Feature Branch**: `001-add-timeline-chart` +**Created**: 2026-01-28 +**Status**: Draft +**Input**: User description: "我想要实现一个新的图表类型,Timeline ,用于封装信息图领域的图表时间轴,需要支持的功能有:1. 时间轴图表需要展示:事件点、时间点 2. 时间点支持多种布局类型:横向布局、纵向布局、径向布局、S线布局等 3. 可能有多个系列来展示事件点" + +## User Scenarios & Testing _(mandatory)_ + +### User Story 1 - 创建并展示时间轴图表 (Priority: P1) + +可视化开发者需要用 Timeline 图表展示一系列事件点与时间点,以便在信息图中清晰表达时间顺序与事件分布。 + +**Why this priority**: 这是该图表类型的核心用途,没有它无法构成可用的最小功能集。 + +**Independent Test**: 使用一组包含时间点与事件点的数据创建 Timeline 图表,确认可视化结果包含事件点与时间点。 + +**Acceptance Scenarios**: + +1. **Given** 一组包含时间点与事件点的数据,**When** 渲染 Timeline 图表,**Then** 图表同时展示事件点与时间点。 +2. **Given** 事件点包含名称与时间信息,**When** 渲染 Timeline 图表,**Then** 用户可以从图表中识别事件发生顺序。 + +--- + +### User Story 2 - 切换时间轴布局 (Priority: P2) + +可视化开发者需要在不同场景下选择不同布局,以适配横向、纵向、径向或 S 线等信息图设计风格。 + +**Why this priority**: 布局适配是时间轴图表在信息图场景中的关键差异化能力。 + +**Independent Test**: 对同一数据分别选择横向、纵向、径向、S 线布局并渲染,确认布局方向与结构发生变化。 + +**Acceptance Scenarios**: + +1. **Given** 一组时间轴数据,**When** 选择横向布局,**Then** 时间轴按水平方向展开。 +2. **Given** 一组时间轴数据,**When** 选择纵向布局,**Then** 时间轴按垂直方向展开。 +3. **Given** 一组时间轴数据,**When** 选择径向布局,**Then** 时间轴围绕中心呈放射结构。 +4. **Given** 一组时间轴数据,**When** 选择 S 线布局,**Then** 时间轴呈连续曲线结构排列。 + +--- + +### User Story 3 - 展示多系列事件点 (Priority: P3) + +可视化开发者需要同时展示多个系列的事件点,以表达不同来源或类型的事件分布。 + +**Why this priority**: 多系列能力提升表达力,但在没有该能力时仍能满足基本使用。 + +**Independent Test**: 提供包含多个系列标识的事件点数据并渲染,确认不同系列可同时显示。 + +**Acceptance Scenarios**: + +1. **Given** 含有多个系列标识的事件点数据,**When** 渲染 Timeline 图表,**Then** 不同系列的事件点同时出现。 + +--- + +### Edge Cases + +- 当数据中仅包含时间点且没有事件点时,图表仍能展示时间点。 +- 当事件点时间重复或密集时,图表仍能展示所有事件点而不丢失。 +- 当选择的布局类型不支持当前数据时,图表能够给出可理解的提示或降级表现。 + +## Requirements _(mandatory)_ + +### Functional Requirements + +- **FR-001**: 系统必须支持 Timeline 图表类型的创建与渲染。 +- **FR-002**: 系统必须支持时间轴图表同时展示时间点与事件点。 +- **FR-003**: 系统必须支持横向布局的时间轴展示。 +- **FR-004**: 系统必须支持纵向布局的时间轴展示。 +- **FR-005**: 系统必须支持径向布局的时间轴展示。 +- **FR-006**: 系统必须支持 S 线布局的时间轴展示。 +- **FR-007**: 系统必须支持同一时间轴内多个系列的事件点展示。 +- **FR-008**: 系统必须允许用户在多个布局类型之间切换。 + +### Key Entities _(include if feature involves data)_ + +- **时间点**: 表示时间轴上的关键时间节点,包含时间值与可选的描述信息。 +- **事件点**: 表示发生在时间轴上的事件,包含时间信息、事件内容与所属系列标识。 +- **系列**: 用于区分不同来源或类型的事件点集合。 + +## Assumptions + +- 默认提供可读的时间顺序展示逻辑,无需用户手动排序数据。 +- 默认使用一致的视觉区分来标识不同系列的事件点。 +- 当布局类型切换时,图表内容保持不变,仅布局结构发生变化。 + +## Dependencies + +- 无外部系统或第三方服务依赖。 + +## Success Criteria _(mandatory)_ + +### Measurable Outcomes + +- **SC-001**: 90% 的试用用户能够在 5 分钟内创建并展示包含时间点与事件点的时间轴图表。 +- **SC-002**: 用户能够在 2 分钟内完成至少两种布局类型的切换并验证呈现差异。 +- **SC-003**: 95% 的多系列数据在渲染后仍能完整展示所有事件点。 diff --git a/specs/001-add-timeline-chart/tasks.md b/specs/001-add-timeline-chart/tasks.md new file mode 100644 index 0000000000..18058513ca --- /dev/null +++ b/specs/001-add-timeline-chart/tasks.md @@ -0,0 +1,67 @@ +# Tasks: Timeline 图表类型 + +**Input**: Design documents from `/specs/001-add-timeline-chart/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/ + +## Phase 1: Setup (Shared Infrastructure) + +- [ ] T001 Verify spec artifacts exist under specs/001-add-timeline-chart/ +- [ ] T002 [P] Run targeted build for @visactor/vchart +- [ ] T003 [P] Add docs navigation placeholder for Timeline in packages/docs + +## Phase 2: Foundational (Blocking Prerequisites) + +- [ ] T004 Define Timeline public types in packages/vchart-types/types/chart/timeline.d.ts +- [ ] T005 [P] Register timeline chart in packages/vchart/src/vchart-all.ts +- [ ] T006 [P] Create chart entry dir packages/vchart/src/chart/timeline/ +- [ ] T007 Establish unit test scaffold in packages/vchart/**tests**/timeline/ +- [ ] T008 [P] Add schema generation hookup in packages/vchart-schema for Timeline spec + +## Phase 3: User Story 1 - 创建并展示时间轴图表 (Priority: P1) 🎯 MVP + +**Goal**: 渲染包含时间点与事件点的 Timeline 基本图表 +**Independent Test**: 提供最小数据,成功渲染并可视识别时间顺序与事件 + +- [ ] T009 [P] [US1] Implement timeline spec transformer in packages/vchart/src/chart/timeline/transformer.ts +- [ ] T010 [P] [US1] Implement layout engine (horizontal/vertical) in packages/vchart/src/chart/timeline/layout/base.ts +- [ ] T011 [US1] Render time axis and event markers in packages/vchart/src/chart/timeline/render.ts +- [ ] T012 [US1] Wire data ingestion for timePoints/eventPoints in packages/vchart/src/chart/timeline/data.ts +- [ ] T013 [US1] Add minimal theme defaults for timeline in packages/vchart/src/theme/chart/timeline.ts +- [ ] T014 [US1] Add unit tests for transformer and data handling in packages/vchart/**tests**/timeline/ +- [ ] T015 [US1] Add visual regression case for basic timeline in packages/vchart/**tests**/runtime/browser/ +- [ ] T016 [US1] Create runtime demo in **tests**/runtime/browser/test-page/timeline-basic.ts + +## Phase 4: User Story 2 - 切换时间轴布局 (Priority: P2) + +**Goal**: 支持横向、纵向、径向、S 线布局切换 +**Independent Test**: 对同一数据切换布局后结构明显变化 + +- [ ] T017 [P] [US2] Implement vertical layout strategy in packages/vchart/src/chart/timeline/layout/vertical.ts +- [ ] T018 [P] [US2] Implement radial layout strategy in packages/vchart/src/chart/timeline/layout/radial.ts +- [ ] T019 [P] [US2] Implement s-curve layout strategy in packages/vchart/src/chart/timeline/layout/s-curve.ts +- [ ] T020 [US2] Add layout type switching in transformer and spec in packages/vchart/src/chart/timeline/transformer.ts +- [ ] T021 [US2] Add layout switch tests in packages/vchart/**tests**/timeline/ +- [ ] T022 [US2] Add layout demo in **tests**/runtime/browser/test-page/timeline-layouts.ts + +## Phase 5: User Story 3 - 展示多系列事件点 (Priority: P3) + +**Goal**: 多系列事件点同时展示并可区分 +**Independent Test**: 同一时间轴显示多个系列,颜色/图例区分明显 + +- [ ] T023 [P] [US3] Series mapping for eventPoints in packages/vchart/src/chart/timeline/data.ts +- [ ] T024 [US3] Legend integration and series styling in packages/vchart/src/chart/timeline/render.ts +- [ ] T025 [US3] Add multi-series unit tests in packages/vchart/**tests**/timeline/ +- [ ] T026 [US3] Multi-series demo in **tests**/runtime/browser/test-page/timeline-series.ts + +## Phase N: Polish & Cross-Cutting Concerns + +- [ ] T027 [P] Add docs examples and API reference in packages/docs/docs/timeline/ +- [ ] T028 Add dense events performance test and tuning in packages/vchart/src/chart/timeline/layout/\* +- [ ] T029 Add edge-case handling for timePoints-only datasets in packages/vchart/src/chart/timeline/data.ts +- [ ] T030 Add fallback behavior for unsupported layout types in packages/vchart/src/chart/timeline/transformer.ts +- [ ] T031 Add quickstart example validation using specs/001-add-timeline-chart/quickstart.md + +## Dependencies & Execution Order + +- Setup → Foundational → US1 → US2 → US3 → Polish +- [P] 任务表示可并行(不同文件、无未完成依赖)