前言
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
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
plugin
grid
- index.ts
legend
- index.ts
minimap
- index.ts
selector
- lasso.ts
- rect.ts
theme
- dark.ts
- light.ts
themeSolver
- base.ts
- spec.ts
- subject.ts
- index.ts
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
- tests
源码
从架构档次能够看出,整体对外裸露的就是Graph
的类以及stdLib
的规范库,因此在剖析源码调用过程中,咱们抓住Graph进行逐渐的往外拓展,从而把握整体的一个设计链路,防止陷入部分无奈抽离。
Graph
对外裸露的Graph
类是整个G6图的外围类
// https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.tsexport 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.tsexport 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.tsconst 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 招募社区大牛,独特拥抱开源!