前言
AntV是蚂蚁金服全新一代数据可视化解决方案,其中G6主要用于解决图可视领域相关的前端可视化问题,其是一个简单、易用、完备的图可视化引擎。本文旨在通过简要分析G6 5.x版本源码来对图可视领域的一些底层引擎进行一个大致了解,同时也为G6引擎的社区共建共享提供一些力量,可以更好的提供插件化功能的编写。
架构
新版G6整体是基于“插件化”的架构进行设计的,对外整体暴露Graph
类及StdLib
标准库,将主题、数据处理、布局、视图、类型、交互等均作为插件来进行处理,提供更高层次的定制化需求,提升更好的开源能力。
目录
整体采用monorepo进行源码的仓库管理
- packages
- g6
- docs
- src
- constant
- index.ts
- shape.ts
- item
- combo.ts
- edge.ts
- item.ts
- node.ts
- runtime
- controller
- data.ts
- extensions.ts
- index.ts
- interaction.ts
- item.ts
- layout.ts
- plugin.ts
- theme.ts
- viewport.ts
- graph.ts
- hooks.ts
- controller
- stdlib
- behavior
- activate-relations.ts
- brush-select.ts
- click-select.ts
- drag-canvas.ts
- drag-node.ts
- hover-activate.ts
- lasso-select.ts
- orbit-canvas-3d.ts
- rotate-canvas-3d.ts
- track-canvas-3d.ts
- zoom-canvas-3d.ts
- zoom-canvas.ts
- data
- comboFromNode.ts
- item
- edge
- base.ts
- index.ts
- line.ts
- node
- base.ts
- base3d.ts
- circle.ts
- index.ts
- sphere.ts
- edge
- plugin
- grid
- index.ts
- legend
- index.ts
- minimap
- index.ts
- grid
- selector
- lasso.ts
- rect.ts
- theme
- dark.ts
- light.ts
- themeSolver
- base.ts
- spec.ts
- subject.ts
- index.ts
- behavior
- types
- animate.ts
- behavior.ts
- combo.ts
- common.ts
- data.ts
- edge.ts
- event.ts
- graph.ts
- hook.ts
- index.ts
- item.ts
- layout.ts
- node.ts
- plugin.ts
- render.ts
- spec.ts
- stdlib.ts
- theme.ts
- view.ts
- util
- animate.ts
- array.ts
- canvas.ts
- event.ts
- extend.ts
- extension.ts
- index.ts
- item.ts
- mapper.ts
- math.ts
- point.ts
- shape.ts
- shape3d.ts
- text.ts
- type.ts
- zoom.ts
- index.ts
- constant
- tests
源码
从架构层次可以看出,整体对外暴露的就是Graph
的类以及stdLib
的标准库,因而在分析源码调用过程中,我们抓住Graph进行逐步的往外拓展,从而把握整体的一个设计链路,避免陷入局部无法抽离。
Graph
对外暴露的Graph
类是整个G6图的核心类
// https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts
export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
extends EventEmitter
implements IGraph<B, T>
{
public hooks: Hooks;
// for nodes and edges, which will be separate into groups
public canvas: Canvas;
// the container dom for the graph canvas
public container: HTMLElement;
// the tag to indicate whether the graph instance is destroyed
public destroyed: boolean;
// the renderer type of current graph
public rendererType: RendererName;
// for transient shapes for interactions, e.g. transient node and related edges while draging, delegates
public transientCanvas: Canvas;
// for background shapes, e.g. grid, pipe indices
public backgroundCanvas: Canvas;
// the tag indicates all the three canvases are all ready
private canvasReady: boolean;
private specification: Specification<B, T>;
private dataController: DataController;
private interactionController: InteractionController;
private layoutController: LayoutController;
private viewportController: ViewportController;
private itemController: ItemController;
private extensionController: ExtensionController;
private themeController: ThemeController;
private pluginController: PluginController;
private defaultSpecification = {
theme: {
type: 'spec',
base: 'light',
},
};
constructor(spec: Specification<B, T>) {
super();
// TODO: analyse cfg
this.specification = Object.assign({}, this.defaultSpecification, spec);
this.initHooks();
this.initCanvas();
this.initControllers();
this.hooks.init.emit({
canvases: {
background: this.backgroundCanvas,
main: this.canvas,
transient: this.transientCanvas,
},
});
const { data } = spec;
if (data) {
// TODO: handle multiple type data configs
this.read(data as GraphData);
}
}
// 初始化控制器,用于各种类型插件的依赖注入
private initControllers() {
this.dataController = new DataController(this);
this.interactionController = new InteractionController(this);
this.layoutController = new LayoutController(this);
this.themeController = new ThemeController(this);
this.itemController = new ItemController(this);
this.viewportController = new ViewportController(this);
this.extensionController = new ExtensionController(this);
this.pluginController = new PluginController(this);
}
// 初始化画布
private initCanvas() {
const { renderer, container, width, height } = this.specification;
let pixelRatio;
if (renderer && !isString(renderer)) {
// @ts-ignore
this.rendererType = renderer.type || 'canvas';
// @ts-ignore
pixelRatio = renderer.pixelRatio;
} else {
// @ts-ignore
this.rendererType = renderer || 'canvas';
}
const containerDOM = isString(container)
? document.getElementById(container as string)
: container;
if (!containerDOM) {
console.error(
`Create graph failed. The container for graph ${containerDOM} is not exist.`,
);
this.destroy();
return;
}
this.container = containerDOM;
const size = [width, height];
if (size[0] === undefined) {
size[0] = containerDOM.scrollWidth;
}
if (size[1] === undefined) {
size[1] = containerDOM.scrollHeight;
}
this.backgroundCanvas = createCanvas(
this.rendererType,
containerDOM,
size[0],
size[1],
pixelRatio,
);
this.canvas = createCanvas(
this.rendererType,
containerDOM,
size[0],
size[1],
pixelRatio,
);
this.transientCanvas = createCanvas(
this.rendererType,
containerDOM,
size[0],
size[1],
pixelRatio,
true,
{
pointerEvents: 'none',
},
);
Promise.all(
[this.backgroundCanvas, this.canvas, this.transientCanvas].map(
(canvas) => canvas.ready,
),
).then(() => (this.canvasReady = true));
}
// 改变渲染类型,默认为Canvas
public changeRenderer(type) {
}
// 初始化生命周期钩子函数
private initHooks() {
this.hooks = {
init: new Hook<{
canvases: {
background: Canvas;
main: Canvas;
transient: Canvas;
};
}>({ name: 'init' }),
datachange: new Hook<{ data: GraphData; type: DataChangeType }>({
name: 'datachange',
}),
itemchange: new Hook<{
type: ITEM_TYPE;
changes: GraphChange<NodeModelData, EdgeModelData>[];
graphCore: GraphCore;
theme: ThemeSpecification;
}>({ name: 'itemchange' }),
render: new Hook<{
graphCore: GraphCore;
theme: ThemeSpecification;
transientCanvas: Canvas;
}>({
name: 'render',
}),
layout: new Hook<{ graphCore: GraphCore }>({ name: 'layout' }),
viewportchange: new Hook<ViewportChangeHookParams>({ name: 'viewport' }),
modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
behaviorchange: new Hook<{
action: 'update' | 'add' | 'remove';
modes: string[];
behaviors: (string | BehaviorOptionsOf<{}>)[];
}>({ name: 'behaviorchange' }),
itemstatechange: new Hook<{
ids: ID[];
states?: string[];
value?: boolean;
}>({
name: 'itemstatechange',
}),
itemvisibilitychange: new Hook<{ ids: ID[]; value: boolean }>({
name: 'itemvisibilitychange',
}),
transientupdate: new Hook<{
type: ITEM_TYPE | SHAPE_TYPE;
id: ID;
config: {
style: ShapeStyle;
action: 'remove' | 'add' | 'update' | undefined;
};
canvas: Canvas;
}>({ name: 'transientupdate' }),
pluginchange: new Hook<{
action: 'update' | 'add' | 'remove';
plugins: (
| string
| { key: string; type: string; [cfgName: string]: unknown }
)[];
}>({ name: 'pluginchange' }),
themechange: new Hook<{
theme: ThemeSpecification;
canvases: {
background: Canvas;
main: Canvas;
transient: Canvas;
};
}>({ name: 'init' }),
destroy: new Hook<{}>({ name: 'destroy' }),
};
}
// 更改spec配置
public updateSpecification(spec: Specification<B, T>): Specification<B, T> {
}
// 更改theme配置
public updateTheme(theme: ThemeOptionsOf<T>) {
}
// 获取配置信息
public getSpecification(): Specification<B, T> {
}
// 数据渲染,diff比对
public async read(data: GraphData) {
}
// 更改图数据
public async changeData(
data: GraphData,
type: 'replace' | 'mergeReplace' = 'mergeReplace',
) {
}
// 清空画布
public clear() {
}
// 获取视图中心
public getViewportCenter(): PointLike {
}
// 更给视图转换
public async transform(
options: GraphTransformOptions,
effectTiming?: CameraAnimationOptions,
): Promise<void> {
}
// 立刻停止当前过渡变化
public stopTransformTransition() {
}
// 画布位移
public async translate(
distance: Partial<{
dx: number;
dy: number;
dz: number;
}>,
effectTiming?: CameraAnimationOptions,
) {
}
// 画布移动至视图坐标
public async translateTo(
{ x, y }: PointLike,
effectTiming?: CameraAnimationOptions,
) {
}
// 画布放大/缩小
public async zoom(
ratio: number,
origin?: PointLike,
effectTiming?: CameraAnimationOptions,
) {
}
// 画布放大/缩小至
public async zoomTo(
zoom: number,
origin?: PointLike,
effectTiming?: CameraAnimationOptions,
) {
}
// 获取画布放大/缩小比例
public getZoom() {
}
// 旋转画布
public async rotate(
angle: number,
origin?: PointLike,
effectTiming?: CameraAnimationOptions,
) {
}
// 旋转画布至
public async rotateTo(
angle: number,
origin?: PointLike,
effectTiming?: CameraAnimationOptions,
) {
}
// 自适应画布
public async fitView(
options?: {
padding: Padding;
rules: FitViewRules;
},
effectTiming?: CameraAnimationOptions,
) {
}
// 对齐画布中心与视图中心
public async fitCenter(effectTiming?: CameraAnimationOptions) {
}
// 对齐元素
public async focusItem(id: ID | ID[], effectTiming?: CameraAnimationOptions) {
}
// 获取画布大小
public getSize(): number[] {
}
// 设置画布大小
public setSize(size: number[]) {
}
// 获取视图下的渲染坐标
public getCanvasByViewport(viewportPoint: Point): Point {
}
// 获取画布下的渲染视图
public getViewportByCanvas(canvasPoint: Point): Point {
}
// 获取浏览器坐标
public getClientByCanvas(canvasPoint: Point): Point {
}
// 获取画布坐标
public getCanvasByClient(clientPoint: Point): Point {
}
// ===== item operations =====
// 获取节点数据
public getNodeData(condition: ID | Function): NodeModel | undefined {
}
// 获取边数据
public getEdgeData(condition: ID | Function): EdgeModel | undefined {
}
// 获取combo数据
public getComboData(condition: ID | Function): ComboModel | undefined {
}
// 获取所有节点数据
public getAllNodesData(): NodeModel[] {
}
// 获取所有边数据
public getAllEdgesData(): EdgeModel[] {
}
// 获取所有combo类型数据
public getAllCombosData(): ComboModel[] {
}
// 获取相关边数据
public getRelatedEdgesData(
nodeId: ID,
direction: 'in' | 'out' | 'both' = 'both',
): EdgeModel[] {
}
// 获取临近节点数据
public getNeighborNodesData(
nodeId: ID,
direction: 'in' | 'out' | 'both' = 'both',
): NodeModel[] {
}
// 获取状态类型的id
public findIdByState(
itemType: ITEM_TYPE,
state: string,
value: string | boolean = true,
additionalFilter?: (item: NodeModel | EdgeModel | ComboModel) => boolean,
): ID[] {
}
// 添加数据
public addData(
itemType: ITEM_TYPE,
models:
| NodeUserModel
| EdgeUserModel
| ComboUserModel
| NodeUserModel[]
| EdgeUserModel[]
| ComboUserModel[],
stack?: boolean,
):
| NodeModel
| EdgeModel
| ComboModel
| NodeModel[]
| EdgeModel[]
| ComboModel[] {
}
// 移除数据
public removeData(itemType: ITEM_TYPE, ids: ID | ID[], stack?: boolean) {
}
// 更新数据
public updateData(
itemType: ITEM_TYPE,
models:
| Partial<NodeUserModel>
| Partial<EdgeUserModel>
| Partial<
| ComboUserModel
| Partial<NodeUserModel>[]
| Partial<EdgeUserModel>[]
| Partial<ComboUserModel>[]
>,
stack?: boolean,
):
| NodeModel
| EdgeModel
| ComboModel
| NodeModel[]
| EdgeModel[]
| ComboModel[] {
}
// 更新节点位置
public updateNodePosition(
models:
| Partial<NodeUserModel>
| Partial<
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>,
stack?: boolean,
) {
}
// 显示类型元素
public showItem(ids: ID | ID[], disableAniamte?: boolean) {
}
// 隐藏类型元素
public hideItem(ids: ID | ID[], disableAniamte?: boolean) {
}
// 设置类型元素状态
public setItemState(
ids: ID | ID[],
states: string | string[],
value: boolean,
) {
}
// 获取类型元素状态
public getItemState(id: ID, state: string) {
}
// 清空类型元素状态
public clearItemState(ids: ID | ID[], states?: string[]) {
}
// 获取渲染器box
public getRenderBBox(id: ID | undefined): AABB | false {
}
// 获取显示类型id
public getItemVisible(id: ID) {
}
// ===== combo operations =====
// 创建组
public createCombo(
combo: string | ComboUserModel,
childrenIds: string[],
stack?: boolean,
) {
}
// 取消组
public uncombo(comboId: ID, stack?: boolean) {
}
// 释放组
public collapseCombo(comboId: ID, stack?: boolean) {
}
// 扩展组
public expandCombo(comboId: ID, stack?: boolean) {
}
// ===== layout =====
// 设置布局参数
public async layout(options?: LayoutOptions) {
}
// 取消布局算法
public stopLayout() {
}
// 设置交互模式
public setMode(mode: string) {
}
// 添加交互行为
public addBehaviors(
behaviors: BehaviorOptionsOf<B>[],
modes: string | string[],
) {
}
// 移除交互行为
public removeBehaviors(behaviorKeys: string[], modes: string | string[]) {
}
// 更新交互行为
public updateBehavior(behavior: BehaviorOptionsOf<B>, mode?: string) {
}
// 添加插件
public addPlugins(
pluginCfgs: (
| {
key: string;
type: string;
[cfgName: string]: unknown; // TODO: configs from plugins
}
| string
)[],
) {
}
// 移除插件
public removePlugins(pluginKeys: string[]) {
}
// 更新插件
public updatePlugin(plugin: {
key: string;
type: string;
[cfg: string]: unknown;
}) {
}
// 绘制过渡动效
public drawTransient(
type: ITEM_TYPE | SHAPE_TYPE,
id: ID,
config: {
action: 'remove' | 'add' | 'update' | undefined;
style: ShapeStyle;
onlyDrawKeyShape?: boolean;
},
): DisplayObject {
}
// 销毁画布
public destroy(callback?: Function) {
}
}
StdLib
标准库用于提供和社区开发者进行交互的标准构件,方便自定义开发及共建共享。其中,提供了data
、extensions
、interaction
、item
、layout
、plugin
、theme
以及viewport
的插件化能力。
这里,以plugin
的控制器接入为例,代码如下:
// https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/controller/plugin.ts
export class PluginController {
public extensions: any = [];
public graph: IGraph;
/**
* Plugins on graph.
* @example
* { 'minimap': Minimap, 'tooltip': Tooltip }
*/
private pluginMap: Map<string, { type: string; plugin: Plugin }> = new Map();
/**
* Listeners added by all current plugins.
* @example
* {
* 'minimap': { 'afterlayout': function },
* }
*/
private listenersMap: Record<string, Record<string, Listener>> = {};
constructor(graph: IGraph<any, any>) {
this.graph = graph;
this.tap();
}
/**
* Subscribe the lifecycle of graph.
*/
private tap() {
this.graph.hooks.init.tap(this.onPluginInit.bind(this));
this.graph.hooks.pluginchange.tap(this.onPluginChange.bind(this));
this.graph.hooks.destroy.tap(this.onDestroy.bind(this));
}
private onPluginInit() {
// 1. Initialize new behaviors.
this.pluginMap.clear();
const { graph } = this;
const pluginConfigs = graph.getSpecification().plugins || [];
pluginConfigs.forEach((config) => {
this.initPlugin(config);
});
// 2. Add listeners for each behavior.
this.listenersMap = {};
this.pluginMap.forEach((item, key) => {
const { plugin } = item;
this.addListeners(key, plugin);
});
}
private initPlugin(config) {
const { graph } = this;
const Plugin = getExtension(config, registry.useLib, 'plugin');
const options = typeof config === 'string' ? {} : config;
const type = typeof config === 'string' ? config : config.type;
const key = typeof config === 'string' ? config : config.key || type;
const plugin = new Plugin(options);
plugin.init(graph);
this.pluginMap.set(key, { type, plugin });
return { key, type, plugin };
}
private onPluginChange(params: {
action: 'update' | 'add' | 'remove';
plugins: (string | { key: string; type: string; options: any })[];
}) {
const { action, plugins: pluginCfgs } = params;
if (action === 'add') {
pluginCfgs.forEach((config) => {
const { key, plugin } = this.initPlugin(config);
this.addListeners(key, plugin);
});
return;
}
if (action === 'remove') {
pluginCfgs.forEach((config) => {
const key =
typeof config === 'string' ? config : config.key || config.type;
const item = this.pluginMap.get(key);
if (!item) return;
const { plugin } = item;
this.removeListeners(key);
plugin.destroy();
this.pluginMap.delete(key);
});
return;
}
if (action === 'update') {
pluginCfgs.forEach((config) => {
if (typeof config === 'string') return;
const key = config.key || config.type;
const item = this.pluginMap.get(key);
if (!item) return;
const { plugin } = item;
plugin.updateCfgs(config);
this.removeListeners(key);
this.addListeners(key, plugin);
});
return;
}
}
private addListeners = (key: string, plugin: Plugin) => {
const events = plugin.getEvents();
this.listenersMap[key] = {};
Object.keys(events).forEach((eventName) => {
// Wrap the listener with error logging.
const listener = wrapListener(
key,
eventName,
events[eventName].bind(plugin),
);
this.graph.on(eventName, listener);
this.listenersMap[key][eventName] = listener;
});
};
private removeListeners = (key: string) => {
const listeners = this.listenersMap[key];
Object.keys(listeners).forEach((eventName) => {
const listener = listeners[eventName];
if (listener) {
this.graph.off(eventName, listener);
}
});
};
private onDestroy() {
this.pluginMap.forEach((item) => {
const { plugin } = item;
plugin.destroy();
});
}
destroy() {}
}
而其他的标准库控制器,则通过配置的方式,在Graph
初始化时进行接入,代码如下:
// https://github.com/antvis/G6/blob/v5/packages/g6/src/stdlib/index.ts
const stdLib = {
transforms: {
comboFromNode,
},
themes: {
light: LightTheme,
dark: DarkTheme,
},
themeSolvers: {
spec: SpecThemeSolver,
subject: SubjectThemeSolver,
},
layouts: layoutRegistry,
behaviors: {
'activate-relations': ActivateRelations,
'drag-canvas': DragCanvas,
'hover-activate': HoverActivate,
'zoom-canvas': ZoomCanvas,
'drag-node': DragNode,
'click-select': ClickSelect,
'brush-select': BrushSelect,
'lasso-select': LassoSelect,
'zoom-canvas-3d': ZoomCanvas3D,
'rotate-canvas-3d': RotateCanvas3D,
'track-canvas-3d': TrackCanvas3D,
'orbit-canvas-3d': OrbitCanvas3D,
},
plugins: {
minimap: Minimap,
legend: Legend,
},
nodes: {
'circle-node': CircleNode,
'sphere-node': SphereNode,
},
edges: {
'line-edge': LineEdge,
},
combos: {},
};
export { stdLib }
总结
综上所述,AntV G6 5.0提供了更好的插件化机制,将功能进行解耦后提供暴露的通用标准制式方便开发者能够更好的开源与共建共享,从而利用开源社区的力量将G6建设成为更加通用且适配更多场景的图可视化库。开源的商业逻辑从来不在开源本身,用闭源的思路做开源就失去了开源的价值,共勉!!!
参考
- G6官网
- G6源码
- AntV 图发布会圆满收官,G6 5.0 招募社区大牛,共同拥抱开源!