Skip to content

Commit f97d320

Browse files
authored
Merge pull request #4197 from VisActor/feat/support-sankey-custom-layout
Feat/support sankey custom layout
2 parents bde172f + 7f7f977 commit f97d320

File tree

7 files changed

+181
-28
lines changed

7 files changed

+181
-28
lines changed

packages/vchart/src/chart/sankey/sankey-transformer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export class SankeyChartSpecTransformer<
3636
'link',
3737
'emphasis',
3838
'inverse',
39-
'overflow'
39+
'overflow',
40+
'customLayout'
4041
]);
4142

4243
return series;

packages/vchart/src/core/vchart.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,9 @@ export class VChart implements IVChart {
367367
private _isReleased: boolean;
368368

369369
private _chartPlugin?: IChartPluginService;
370+
get chartPlugin() {
371+
return this._chartPlugin;
372+
}
370373
private _onResize?: () => void;
371374

372375
constructor(spec: ISpec, options: IInitOption) {

packages/vchart/src/data/transforms/sankey.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ export interface ISankeyOpt extends SankeyOptions {
77
sourceField: string;
88
valueField: string;
99
view: () => { x0: number; x1: number; y0: number; y1: number };
10+
customLayout?: (
11+
layout: SankeyLayout,
12+
originalData: SankeyData,
13+
view: ReturnType<ISankeyOpt['view']>,
14+
option: ISankeyOpt
15+
) => ReturnType<SankeyLayout['layout']>;
1016
}
1117

1218
export const collectHierarchyField = (set: Set<any>, data: any[], field: string) => {
@@ -99,7 +105,11 @@ export const sankeyLayout = (data: SankeyData[], op: ISankeyOpt) => {
99105

100106
const result = [];
101107

102-
result.push(layout.layout(originalData, view));
108+
if (op.customLayout) {
109+
result.push(op.customLayout(layout, originalData, view, op));
110+
} else {
111+
result.push(layout.layout(originalData, view));
112+
}
103113

104114
return result;
105115
};

packages/vchart/src/plugin/chart/plugin-service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export class ChartPluginService<T extends IChartPlugin = IChartPlugin>
1515
this.globalInstance = globalInstance;
1616
}
1717

18+
getPlugin(name: string): T | undefined {
19+
return this._plugins.find(plugin => plugin.name === name);
20+
}
21+
1822
onInit(chartSpec: any) {
1923
this._plugins.forEach(plugin => {
2024
plugin.onInit && plugin.onInit(this, chartSpec);

packages/vchart/src/plugin/chart/scroll/scroll.ts

Lines changed: 147 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
1-
import type { IGroup } from '@visactor/vrender-core';
1+
import { isValidNumber, merge } from '@visactor/vutils';
2+
import type { IGroup, IRectGraphicAttribute } from '@visactor/vrender-core';
23
import { BasePlugin } from '../../base/base-plugin';
34
import type { IChartPluginService } from '../interface';
45
import { registerChartPlugin } from '../register';
56
import type { IScrollPlugin, IScrollPluginSpec } from './interface';
67
import { ScrollBar as ScrollBarComponent } from '@visactor/vrender-components';
8+
import { Event } from '../../../event/event';
9+
import type { ExtendEventParam } from '../../../event/interface';
710

8-
const scrollBarSize = 10;
11+
// 由vrender透出, 接入新版本后需修改
12+
const SCROLLBAR_EVENT = 'scrollDrag';
13+
const SCROLLBAR_END_EVENT = 'scrollUp';
914

15+
const DefaultTheme: {
16+
size?: number;
17+
railStyle?: Omit<IRectGraphicAttribute, 'width' | 'height'>;
18+
sliderStyle?: Omit<IRectGraphicAttribute, 'width' | 'height'>;
19+
} = {
20+
size: 10,
21+
railStyle: undefined,
22+
sliderStyle: undefined
23+
};
1024
/**
1125
* ScrollPlugin 类
1226
* @since 1.0.0
@@ -15,26 +29,32 @@ export class ScrollPlugin extends BasePlugin implements IScrollPlugin {
1529
static readonly pluginType: 'chart' = 'chart';
1630
static readonly type: string = 'chartScroll';
1731
readonly type: string = 'chartScroll';
32+
readonly name: string = ScrollPlugin.type;
1833

1934
private _service: IChartPluginService;
2035

2136
private _spec: IScrollPluginSpec;
37+
private _lastScrollX = 0;
38+
private _lastScrollY = 0;
2239

2340
private _scrollLimit = {
2441
x: {
2542
min: 0,
2643
max: 0,
44+
size: 0,
2745
percent: 0
2846
},
2947
y: {
3048
min: 0,
3149
max: 0,
50+
size: 0,
3251
percent: 0
3352
}
3453
};
3554

3655
private _xScrollComponent: ScrollBarComponent;
3756
private _yScrollComponent: ScrollBarComponent;
57+
private _event: Event;
3858

3959
constructor() {
4060
super(ScrollPlugin.type);
@@ -56,18 +76,24 @@ export class ScrollPlugin extends BasePlugin implements IScrollPlugin {
5676
const canvasSize = service.globalInstance.getChart().getCanvasRect();
5777
this._scrollLimit.x.min = Math.min(canvasSize.width - viewBoxSize.width, 0);
5878
this._scrollLimit.x.percent = Math.abs(canvasSize.width / viewBoxSize.width);
79+
this._scrollLimit.x.size = viewBoxSize.width;
5980
this._scrollLimit.y.min = Math.min(canvasSize.height - viewBoxSize.height, 0);
6081
this._scrollLimit.y.percent = Math.abs(canvasSize.height / viewBoxSize.height);
82+
this._scrollLimit.y.size = viewBoxSize.height;
83+
84+
if (!this._event) {
85+
this._event = new Event(this._service.globalInstance.getChart().getOption().eventDispatcher, null);
86+
}
6187
}
6288

6389
onAfterRender() {
6490
const rootMark = this.getRootMark();
6591
if (rootMark) {
6692
if (!this._xScrollComponent) {
67-
this._updateScrollX(rootMark, 0);
93+
this._updateScrollX(rootMark, 0, 0);
6894
}
6995
if (!this._yScrollComponent) {
70-
this._updateScrollY(rootMark, 0);
96+
this._updateScrollY(rootMark, 0, 0);
7197
}
7298
}
7399
}
@@ -94,20 +120,57 @@ export class ScrollPlugin extends BasePlugin implements IScrollPlugin {
94120
if (!rootMark) {
95121
return;
96122
}
97-
this._updateScrollX(rootMark, rootMark.attribute.x - scrollX);
98-
this._updateScrollY(rootMark, rootMark.attribute.y - scrollY);
123+
const { percent: yPercent, y } = this._computeFinalScrollY(rootMark.attribute.y - scrollY) ?? {};
124+
const { percent: xPercent, x } = this._computeFinalScrollX(rootMark.attribute.x - scrollX) ?? {};
125+
const eventResult: { x?: number; y?: number } = {};
126+
if (isValidNumber(x)) {
127+
this._updateScrollX(rootMark, x, xPercent);
128+
eventResult.x = x;
129+
}
130+
if (isValidNumber(y)) {
131+
this._updateScrollY(rootMark, y, yPercent);
132+
eventResult.y = y;
133+
}
134+
135+
this._event.emit('chartScroll', eventResult as ExtendEventParam);
99136
};
100137

101-
private _updateScrollY(rootMark: IGroup, y: number) {
138+
private _computeFinalScrollY(y: number) {
139+
if (this._lastScrollY === y) {
140+
return null;
141+
}
142+
this._lastScrollY = y;
102143
if (this._spec.y?.enable === false) {
103-
return;
144+
return null;
104145
}
105146
const finalY = Math.max(this._scrollLimit.y.min, Math.min(y, this._scrollLimit.y.max));
106-
const percent = Math.abs(finalY / this._scrollLimit.y.min);
147+
const percent = Math.abs(finalY / this._scrollLimit.y.size);
148+
return {
149+
y: finalY,
150+
percent
151+
};
152+
}
153+
private _computeFinalScrollX(x: number) {
154+
if (this._lastScrollX === x) {
155+
return null;
156+
}
157+
this._lastScrollX = x;
158+
if (this._spec.x?.enable === false) {
159+
return null;
160+
}
161+
const finalX = Math.max(this._scrollLimit.x.min, Math.min(x, this._scrollLimit.x.max));
162+
const percent = Math.abs(finalX / this._scrollLimit.x.size);
163+
return {
164+
x: finalX,
165+
percent
166+
};
167+
}
168+
169+
private _updateScrollY(rootMark: IGroup, y: number, percent: number) {
107170
const yScrollComponent = this._getYScrollComponent();
108171
yScrollComponent.setAttribute('range', [percent, percent + this._scrollLimit.y.percent]);
109172
rootMark.setAttributes({
110-
y: finalY
173+
y: y
111174
});
112175
}
113176

@@ -119,32 +182,44 @@ export class ScrollPlugin extends BasePlugin implements IScrollPlugin {
119182
this._yScrollComponent = new ScrollBarComponent({
120183
...rest,
121184
zIndex: 9999,
122-
x: canvasSize.width - scrollBarSize,
185+
x: canvasSize.width - DefaultTheme.size,
123186
y: 0,
124-
width: scrollBarSize,
187+
width: DefaultTheme.size,
125188
height: canvasSize.height,
126189
range: [0, canvasSize.height / viewSize.height],
127190
direction: 'vertical',
128191
delayTime: rest?.delayTime ?? 30,
129192
realTime: rest?.realTime ?? true,
130-
sliderStyle: { fill: 'rgba(0,0,0,0.3)' }
193+
railStyle: DefaultTheme.railStyle,
194+
sliderStyle: DefaultTheme.sliderStyle
195+
});
196+
// 绑定事件,防抖,防止频繁触发
197+
this._yScrollComponent.addEventListener(SCROLLBAR_EVENT, (e: any) => {
198+
const value = e.detail.value;
199+
const { percent, y } = this._computeFinalScrollY(-value[0] * this._scrollLimit.y.size) ?? {};
200+
if (percent !== undefined && y !== undefined) {
201+
this._updateScrollY(this.getRootMark(), y, percent);
202+
this._event.emit('chartScroll', { y } as ExtendEventParam);
203+
}
204+
});
205+
this._yScrollComponent.addEventListener(SCROLLBAR_END_EVENT, (e: any) => {
206+
const value = e.detail.value;
207+
const { percent, y } = this._computeFinalScrollY(-value[0] * this._scrollLimit.y.size) ?? {};
208+
if (percent !== undefined && y !== undefined) {
209+
this._updateScrollY(this.getRootMark(), y, percent);
210+
this._event.emit('chartScroll', { y } as ExtendEventParam);
211+
}
131212
});
132213
this.getRootMark().parent?.addChild(this._yScrollComponent);
133214
}
134215
return this._yScrollComponent;
135216
}
136217

137-
private _updateScrollX(rootMark: IGroup, x: number) {
138-
if (this._spec.x?.enable === false) {
139-
return;
140-
}
141-
const finalX = Math.max(this._scrollLimit.x.min, Math.min(x, this._scrollLimit.x.max));
142-
const percent = Math.abs(finalX / this._scrollLimit.x.min);
218+
private _updateScrollX(rootMark: IGroup, x: number, percent: number) {
143219
const xScrollComponent = this._getXScrollComponent();
144220
xScrollComponent.setAttribute('range', [percent, percent + this._scrollLimit.x.percent]);
145-
146221
rootMark.setAttributes({
147-
x: finalX
222+
x: x
148223
});
149224
}
150225

@@ -157,24 +232,71 @@ export class ScrollPlugin extends BasePlugin implements IScrollPlugin {
157232
...rest,
158233
zIndex: 9999,
159234
x: 0,
160-
y: canvasSize.height - scrollBarSize,
235+
y: canvasSize.height - DefaultTheme.size,
161236
width: canvasSize.width,
162-
height: scrollBarSize,
237+
height: DefaultTheme.size,
163238
range: [0, canvasSize.width / viewSize.width],
164239
direction: 'horizontal',
165240
delayTime: rest?.delayTime ?? 30,
166-
realTime: rest?.realTime ?? true
241+
realTime: rest?.realTime ?? true,
242+
sliderStyle: DefaultTheme.sliderStyle,
243+
railStyle: DefaultTheme.railStyle
244+
});
245+
// 绑定事件,防抖,防止频繁触发
246+
this._xScrollComponent.addEventListener(SCROLLBAR_EVENT, (e: any) => {
247+
const value = e.detail.value;
248+
const { percent, x } = this._computeFinalScrollX(-value[0] * this._scrollLimit.x.size) ?? {};
249+
if (percent !== undefined && x !== undefined) {
250+
this._updateScrollX(this.getRootMark(), x, percent);
251+
this._event.emit('chartScroll', { x } as ExtendEventParam);
252+
}
253+
});
254+
this._xScrollComponent.addEventListener(SCROLLBAR_END_EVENT, (e: any) => {
255+
const value = e.detail.value;
256+
const { percent, x } = this._computeFinalScrollX(-value[0] * this._scrollLimit.x.size) ?? {};
257+
if (percent !== undefined && x !== undefined) {
258+
this._updateScrollX(this.getRootMark(), x, percent);
259+
this._event.emit('chartScroll', { x } as ExtendEventParam);
260+
}
167261
});
168262
this.getRootMark().parent?.addChild(this._xScrollComponent);
169263
}
170264
return this._xScrollComponent;
171265
}
266+
267+
/**
268+
* api
269+
*/
270+
scrollTo({ x, y }: { x?: number; y?: number }) {
271+
const rootMark = this.getRootMark();
272+
if (rootMark) {
273+
if (x !== undefined) {
274+
const { x: finalX, percent } = this._computeFinalScrollX(x) ?? {};
275+
if (finalX !== undefined && percent !== undefined) {
276+
this._updateScrollX(rootMark, finalX, percent);
277+
}
278+
}
279+
if (y !== undefined) {
280+
const { y: finalY, percent } = this._computeFinalScrollY(y) ?? {};
281+
if (finalY !== undefined && percent !== undefined) {
282+
this._updateScrollY(rootMark, finalY, percent);
283+
}
284+
}
285+
}
286+
}
172287
}
173288

174289
/**
175290
* 注册 ScrollPlugin
176291
* @since 1.0.0
177292
*/
178-
export const registerScrollPlugin = () => {
293+
export const registerScrollPlugin = (theme?: {
294+
size?: number;
295+
railStyle?: Omit<IRectGraphicAttribute, 'width' | 'height'>;
296+
sliderStyle?: Omit<IRectGraphicAttribute, 'width' | 'height'>;
297+
}) => {
298+
DefaultTheme.size = theme?.size ?? DefaultTheme.size;
299+
DefaultTheme.railStyle = merge({}, DefaultTheme.railStyle ?? {}, theme?.railStyle ?? {});
300+
DefaultTheme.sliderStyle = merge({}, DefaultTheme.sliderStyle ?? {}, theme?.sliderStyle ?? {});
179301
registerChartPlugin(ScrollPlugin);
180302
};

packages/vchart/src/series/sankey/interface.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { IRectMarkSpec, ILinkPathMarkSpec } from '../../typings/visual';
44
import type { IAnimationSpec } from '../../animation/spec';
55
import type { SeriesMarkNameEnum } from '../interface/type';
66
import type { ILabelSpec } from '../../component/label/interface';
7+
import type { SankeyLayout } from '@visactor/vgrammar-sankey';
8+
import type { ISankeyOpt } from '../../data/transforms/sankey';
79

810
export type SankeyMark = 'node' | 'link' | 'label';
911

@@ -200,6 +202,16 @@ export interface ISankeySeriesSpec extends Omit<ISeriesSpec, 'data'>, IAnimation
200202
// /** 是否开启进度条 */
201203
// enable: boolean;
202204
// };
205+
206+
/**
207+
* 自定义布局
208+
* @since 1.13.19
209+
*/
210+
customLayout?: (
211+
layout: SankeyLayout,
212+
originalData: SankeyData,
213+
view: ReturnType<ISankeyOpt['view']>
214+
) => ReturnType<SankeyLayout['layout']>;
203215
}
204216

205217
export interface SankeyLinkDatum {

packages/vchart/src/series/sankey/sankey.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ export class SankeySeries<T extends ISankeySeriesSpec = ISankeySeriesSpec> exten
145145
linkHeight: this._spec.linkHeight,
146146
equalNodeHeight: this._spec.equalNodeHeight,
147147
linkOverlap: this._spec.linkOverlap,
148-
inverse: this._spec.inverse
148+
inverse: this._spec.inverse,
149+
customLayout: this._spec.customLayout
149150
} as ISankeyOpt,
150151
level: TransformLevel.sankeyLayout
151152
});

0 commit comments

Comments
 (0)