Skip to content

Commit bfb8336

Browse files
authored
Merge pull request #4551 from kitee0325/feat/extension-mark-sync-state-plugin
feat: add ExtensionMarkSyncStatePlugin for vchart-extension
2 parents 5de2eeb + 9434098 commit bfb8336

8 files changed

Lines changed: 736 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@visactor/vchart-extension",
5+
"comment": "feat: add ExtensionMarkSyncStatePlugin to synchronize interactive states of extensionMark with primary marks",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@visactor/vchart-extension",
10+
"email": "kitee0325@gmail.com"
11+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# ExtensionMark SyncState Plugin
2+
3+
When using `extensionMark` to draw custom graphics on a series, these graphics do not follow the interactive states (hover, select, etc.) of the primary marks (bars, dots, etc.) by default.
4+
5+
The `ExtensionMarkSyncStatePlugin` enables extensionMarks configured with `syncState: true` to automatically synchronize the interactive states of the corresponding primary marks, providing consistent visual feedback during interaction.
6+
7+
## How It Works
8+
9+
After each render, the plugin pairs extensionMark graphic elements with primary mark graphic elements via `context.key` (the data dimension identifier). When a primary mark's graphic changes state (e.g., highlight, blur, select), the corresponding extensionMark graphic automatically adopts the same state.
10+
11+
> Note: State synchronization only works when the extension mark and the primary mark are bound to the same datum (i.e., they share the same `context.key`).
12+
13+
## Registration
14+
15+
```js
16+
import { registerExtensionMarkSyncStatePlugin } from '@visactor/vchart-extension';
17+
18+
// Register before creating VChart instances
19+
registerExtensionMarkSyncStatePlugin();
20+
```
21+
22+
When using the CDN global variable `VChartExtension`, call `VChartExtension.registerExtensionMarkSyncStatePlugin()`.
23+
24+
## Usage
25+
26+
Add `syncState: true` to the extensionMark configuration, along with the corresponding `state` styles:
27+
28+
```javascript livedemo
29+
/** --Add the following code when used in business-- */
30+
// When using in a business context, install @visactor/vchart-extension separately (keep version consistent with vchart)
31+
// import { registerExtensionMarkSyncStatePlugin } from '@visactor/vchart-extension';
32+
/** --Add the above code when used in business-- */
33+
34+
/** --Delete the following code when used in business-- */
35+
const { registerExtensionMarkSyncStatePlugin } = VChartExtension;
36+
/** --Delete the above code when used in business-- */
37+
38+
registerExtensionMarkSyncStatePlugin();
39+
40+
const data = [
41+
{ category: 'A', value: 80, group: 'February' },
42+
{ category: 'B', value: 120, group: 'February' },
43+
{ category: 'C', value: 60, group: 'February' },
44+
{ category: 'D', value: 150, group: 'February' },
45+
{ category: 'A', value: 90, group: 'March' },
46+
{ category: 'B', value: 100, group: 'March' },
47+
{ category: 'C', value: 110, group: 'March' },
48+
{ category: 'D', value: 70, group: 'March' }
49+
];
50+
51+
const spec = {
52+
type: 'bar',
53+
data: [{ id: 'barData', values: data }],
54+
xField: 'category',
55+
yField: 'value',
56+
seriesField: 'group',
57+
bar: {
58+
state: {
59+
highlight: { stroke: '#000', lineWidth: 2 },
60+
blur: { fillOpacity: 0.2 },
61+
selected: { stroke: 'red', lineWidth: 3 }
62+
}
63+
},
64+
extensionMark: [
65+
{
66+
type: 'symbol',
67+
dataId: 'barData',
68+
name: 'topDot',
69+
syncState: true,
70+
style: {
71+
fill: datum => (datum.group === 'February' ? '#1664FF' : '#1AC6FF'),
72+
symbolType: 'circle',
73+
size: 12,
74+
x: (datum, ctx) =>
75+
ctx.valueToX([datum.category]) + ctx.xBandwidth() / 4 + (datum.group === 'March' ? ctx.xBandwidth() / 2 : 0),
76+
y: (datum, ctx) => ctx.valueToY([datum.value]) - 15
77+
},
78+
state: {
79+
highlight: { fill: 'orange', size: 20, stroke: '#000', lineWidth: 2 },
80+
blur: { fillOpacity: 0.15, size: 8 },
81+
selected: {
82+
fill: 'red',
83+
size: 22,
84+
outerBorder: { distance: 3, lineWidth: 2, stroke: 'red' }
85+
}
86+
}
87+
},
88+
{
89+
type: 'text',
90+
dataId: 'barData',
91+
name: 'topLabel',
92+
syncState: true,
93+
style: {
94+
text: datum => `${datum.value}`,
95+
fontSize: 11,
96+
fill: '#333',
97+
textAlign: 'center',
98+
textBaseline: 'bottom',
99+
x: (datum, ctx) =>
100+
ctx.valueToX([datum.category]) + ctx.xBandwidth() / 4 + (datum.group === 'March' ? ctx.xBandwidth() / 2 : 0),
101+
y: (datum, ctx) => ctx.valueToY([datum.value]) - 26
102+
},
103+
state: {
104+
highlight: { fill: 'orange', fontSize: 16, fontWeight: 'bold' },
105+
blur: { fillOpacity: 0.1 },
106+
selected: { fill: 'red', fontSize: 14, fontWeight: 'bold' }
107+
}
108+
}
109+
],
110+
interaction: {
111+
hover: { enable: true },
112+
select: { enable: true }
113+
},
114+
legends: { visible: true, orient: 'top' },
115+
title: {
116+
visible: true,
117+
text: 'extensionMark syncState Plugin Demo',
118+
subtext: 'Hover / Click bars to see state synchronization'
119+
}
120+
};
121+
122+
const vchart = new VChart(spec, { dom: CONTAINER_ID });
123+
vchart.renderSync();
124+
125+
// Just for the convenience of console debugging, DO NOT COPY!
126+
window['vchart'] = vchart;
127+
```
128+
129+
## API
130+
131+
### registerExtensionMarkSyncStatePlugin()
132+
133+
Registers the ExtensionMark SyncState plugin. Must be called before creating VChart instances.
134+
135+
### Configuration
136+
137+
Add the following field to each extensionMark configuration:
138+
139+
| Property | Type | Default | Description |
140+
| ----------- | --------- | ------- | --------------------------------------------------------------- |
141+
| `syncState` | `boolean` | `false` | Whether to synchronize interactive states from the primary mark |
142+
143+
When using `syncState`, configure the corresponding state styles in `state` (e.g., `highlight`, `blur`, `selected`) to produce visual effects on state changes.
144+
145+
## Important Notes
146+
147+
1. **Data binding**: The extensionMark must have `dataId` (or `dataIndex`) configured to share the same data source with the primary mark, ensuring correct `context.key` pairing
148+
2. **State styles**: `syncState` only synchronizes the state name — it does not provide default styles. Configure `highlight`, `blur`, `selected`, etc. in the `state` object
149+
3. **Group type not supported**: `type: 'group'` extensionMarks do not support `syncState`
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# ExtensionMark 状态同步插件
2+
3+
当使用 `extensionMark` 在系列上补充绘制自定义图形时,这些图形默认不会跟随主图元(如柱子、点等)的交互状态(hover、select 等)变化。
4+
5+
`ExtensionMarkSyncStatePlugin` 插件可以让配置了 `syncState: true` 的 extensionMark 自动同步主图元的交互状态,使自定义图形在交互时与主图元保持一致的视觉反馈。
6+
7+
## 原理说明
8+
9+
插件会在每次渲染完成后,将 extensionMark 的图形元素与主 mark 的图形元素通过 `context.key`(即数据维度标识)进行配对。当主 mark 的图形元素发生状态变化(如 hover 高亮、blur 虚化、select 选中)时,对应的 extensionMark 图形元素会自动同步相同的状态。
10+
11+
> 注意:仅当扩展图元与主图元绑定同一条 datum(即 `context.key` 相同)时,状态同步才会生效。
12+
13+
## 注册插件
14+
15+
```js
16+
import { registerExtensionMarkSyncStatePlugin } from '@visactor/vchart-extension';
17+
18+
// 在创建 VChart 实例之前注册
19+
registerExtensionMarkSyncStatePlugin();
20+
```
21+
22+
如果使用 CDN 打包的全局变量 `VChartExtension`,请调用 `VChartExtension.registerExtensionMarkSyncStatePlugin()`
23+
24+
## 使用方式
25+
26+
`extensionMark` 的配置项中添加 `syncState: true`,并配置对应的 `state` 样式即可:
27+
28+
```javascript livedemo
29+
/** --在业务中使用时请添加以下代码-- */
30+
// 在业务中使用时, 请额外依赖 @visactor/vchart-extension,包版本保持和vchart一致
31+
// import { registerExtensionMarkSyncStatePlugin } from '@visactor/vchart-extension';
32+
/** --在业务中使用时请添加以上代码-- */
33+
34+
/** --在业务中使用时请删除以下代码-- */
35+
const { registerExtensionMarkSyncStatePlugin } = VChartExtension;
36+
/** --在业务中使用时请删除以上代码-- */
37+
38+
registerExtensionMarkSyncStatePlugin();
39+
40+
const data = [
41+
{ category: 'A', value: 80, group: 'February' },
42+
{ category: 'B', value: 120, group: 'February' },
43+
{ category: 'C', value: 60, group: 'February' },
44+
{ category: 'D', value: 150, group: 'February' },
45+
{ category: 'A', value: 90, group: 'March' },
46+
{ category: 'B', value: 100, group: 'March' },
47+
{ category: 'C', value: 110, group: 'March' },
48+
{ category: 'D', value: 70, group: 'March' }
49+
];
50+
51+
const spec = {
52+
type: 'bar',
53+
data: [{ id: 'barData', values: data }],
54+
xField: 'category',
55+
yField: 'value',
56+
seriesField: 'group',
57+
bar: {
58+
state: {
59+
highlight: { stroke: '#000', lineWidth: 2 },
60+
blur: { fillOpacity: 0.2 },
61+
selected: { stroke: 'red', lineWidth: 3 }
62+
}
63+
},
64+
extensionMark: [
65+
{
66+
type: 'symbol',
67+
dataId: 'barData',
68+
name: 'topDot',
69+
syncState: true,
70+
style: {
71+
fill: datum => (datum.group === 'February' ? '#1664FF' : '#1AC6FF'),
72+
symbolType: 'circle',
73+
size: 12,
74+
x: (datum, ctx) =>
75+
ctx.valueToX([datum.category]) + ctx.xBandwidth() / 4 + (datum.group === 'March' ? ctx.xBandwidth() / 2 : 0),
76+
y: (datum, ctx) => ctx.valueToY([datum.value]) - 15
77+
},
78+
state: {
79+
highlight: { fill: 'orange', size: 20, stroke: '#000', lineWidth: 2 },
80+
blur: { fillOpacity: 0.15, size: 8 },
81+
selected: {
82+
fill: 'red',
83+
size: 22,
84+
outerBorder: { distance: 3, lineWidth: 2, stroke: 'red' }
85+
}
86+
}
87+
},
88+
{
89+
type: 'text',
90+
dataId: 'barData',
91+
name: 'topLabel',
92+
syncState: true,
93+
style: {
94+
text: datum => `${datum.value}`,
95+
fontSize: 11,
96+
fill: '#333',
97+
textAlign: 'center',
98+
textBaseline: 'bottom',
99+
x: (datum, ctx) =>
100+
ctx.valueToX([datum.category]) + ctx.xBandwidth() / 4 + (datum.group === 'March' ? ctx.xBandwidth() / 2 : 0),
101+
y: (datum, ctx) => ctx.valueToY([datum.value]) - 26
102+
},
103+
state: {
104+
highlight: { fill: 'orange', fontSize: 16, fontWeight: 'bold' },
105+
blur: { fillOpacity: 0.1 },
106+
selected: { fill: 'red', fontSize: 14, fontWeight: 'bold' }
107+
}
108+
}
109+
],
110+
interaction: {
111+
hover: { enable: true },
112+
select: { enable: true }
113+
},
114+
legends: { visible: true, orient: 'top' },
115+
title: {
116+
visible: true,
117+
text: 'extensionMark syncState 插件示例',
118+
subtext: 'Hover / Click bar 查看状态同步效果'
119+
}
120+
};
121+
122+
const vchart = new VChart(spec, { dom: CONTAINER_ID });
123+
vchart.renderSync();
124+
125+
// Just for the convenience of console debugging, DO NOT COPY!
126+
window['vchart'] = vchart;
127+
```
128+
129+
## API
130+
131+
### registerExtensionMarkSyncStatePlugin()
132+
133+
注册 ExtensionMark 状态同步插件。需要在创建 VChart 实例之前调用。
134+
135+
### 配置项
136+
137+
`extensionMark` 的每个 mark 配置中,增加以下字段:
138+
139+
| 属性 | 类型 | 默认值 | 说明 |
140+
| ----------- | --------- | ------- | ------------------------ |
141+
| `syncState` | `boolean` | `false` | 是否同步主图元的交互状态 |
142+
143+
配合 `syncState` 使用时,需在 `state` 中配置对应状态的样式(如 `highlight``blur``selected`),使状态同步后产生视觉效果。
144+
145+
## 注意事项
146+
147+
1. **数据绑定**:extensionMark 必须配置 `dataId`(或 `dataIndex`),以确保与主 mark 使用相同的数据,`context.key` 才能正确配对
148+
2. **state 样式**`syncState` 仅同步状态名,不提供默认样式。用户需自行在 `state` 中配置 `highlight``blur``selected` 等状态样式
149+
3. **group 类型不支持**`type: 'group'` 的 extensionMark 不支持 `syncState`

0 commit comments

Comments
 (0)