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.
+
+
+
+## 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 提供了时间轴图扩展组件,支持水平和垂直两种布局方式,可以灵活配置事件节点的样式、标签位置和图标等元素。
+
+
+
+## 如何使用扩展图表
+
+时间轴图需要手动注册后才能使用,注册和使用方式如下:
+
+```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] 任务表示可并行(不同文件、无未完成依赖)