G2源码解析之函数式API与规范式API
背景
G2
设计了一套规范(Spec) 去描述可以绘制的可视化,使得用户可以通过调用 chart.options(options)
根据指定的满足规范的选项(options) 去渲染图表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| (() => { const chart = new G2.Chart();
chart.options({ type: 'interval', data: [ { genre: 'Sports', sold: 275 }, { genre: 'Strategy', sold: 115 }, { genre: 'Action', sold: 120 }, { genre: 'Shooter', sold: 350 }, { genre: 'Other', sold: 150 }, ], encode: { x: 'genre', y: 'sold', }, });
chart.render();
return chart.getContainer(); })();
|
基于底层的 Spec
,为了提供更多样化和灵活地声明图表的能力,G2
也提供了一系列函数式 API 来声明图表,比如声明上面简单的条形图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| (() => { const chart = new G2.Chart();
chart .interval() .data([ { genre: 'Sports', sold: 275 }, { genre: 'Strategy', sold: 115 }, { genre: 'Action', sold: 120 }, { genre: 'Shooter', sold: 350 }, { genre: 'Other', sold: 150 }, ]) .encode('x', 'genre') .encode('y', 'sold');
chart.render();
return chart.getContainer(); })();
|
本文将探究G2
是如何实现两套语法用于绘制图表的。
源码追踪
初始化图表
- 在构造函数中解构了
RuntimeOptions
,并对图表基础结构进行了初始化,以Interval
举例,在_create
中将上文的Interval
继承MarkNode
创建了Class Mark
,并创建了以class mark
的 key
作为名称的方法。
- 将
options
方法从参数中解构了type
并更新了根节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| export class Runtime<Spec extends G2Spec = G2Spec> extends CompositionNode { constructor(options: RuntimeOptions) { const { container, canvas, renderer, plugins, lib, createCanvas, ...rest } = options; ... this._create(); } ... private _create() { ... this._marks = {}; for (const key of marks) { const name = key.split('.').pop(); class Mark extends MarkNode { constructor() { super({}, name); } } this._marks[name] = Mark; this[name] = function (composite) { const node = this.append(Mark); if (name === 'mark') node.type = composite; return node; }; } ... } ... options(options?: Spec): Runtime<Spec> | Spec { if (arguments.length === 0) return optionsOf(this) as Spec; const { type } = options; if (type) this._previousDefinedType = type; updateRoot( this, options, this._previousDefinedType, this._marks, this._compositions, ); return this; } }
|
updateRoot
中对具体的图表节点树做了增删改。appendNode
中又调用了createNode
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function updateRoot( node: Node, options: G2ViewTree, definedType: string, mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ) { ... const discovered: [Node, Node, G2ViewTree][] = [[null, node, rootOptions]]; while (discovered.length) { if (!oldNode) { appendNode(parent, newNode, mark, composition); } else if (!newNode) { oldNode.remove(); } else { updateNode(oldNode, newNode); ... } } }
|
createNode
中根据options
中的type
属性创建了具体的composition
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function typeCtor( type: string | ((...args: any[]) => any), mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ): new () => Node { if (typeof type === 'function') return mark.mark; const node = { ...mark, ...composition }; const ctor = node[type]; if (!ctor) throw new Error(`Unknown mark: ${type}.`); return ctor; }
function createNode( options: G2ViewTree, mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ): Node { ... const { type, children, ...value } = options; const Ctor = typeCtor(type, mark, composition); const node = new Ctor(); node.value = value; node.type = type; return node; }
|
函数调用式的参数写入
- 通过装饰器的方式将各个
props
作为方法写入了对应的Mark class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import { markProps } from './props'; @defineProps(markProps) export class MarkNode extends Node<Spec & { [key: string]: any }> { ... } export function defineProps( descriptors: Record<string, NodePropertyDescriptor>, ) { return (Node: NodeClass) => { for (const [name, descriptor] of Object.entries(descriptors)) { const { type } = descriptor; if (type === 'value') defineValueProp(Node, name, descriptor); else if (type === 'array') defineArrayProp(Node, name, descriptor); else if (type === 'object') defineObjectProp(Node, name, descriptor); else if (type === 'node') defineNodeProp(Node, name, descriptor); else if (type === 'container') defineContainerProp(Node, name, descriptor); else if (type === 'mix') defineMixProp(Node, name, descriptor); } return Node as any; }; }
function defineValueProp( Node: NodeClass, name: string, { key = name }: NodePropertyDescriptor, ) { Node.prototype[name] = function (value) { if (arguments.length === 0) return this.attr(key); return this.attr(key, value); }; }
|
配置式的参数写入
在创建图表类型的节点时将type, children
之外的属性作为value写入了图表实例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| function createNode( options: G2ViewTree, mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ): Node { ... const { type, children, ...value } = options; const Ctor = typeCtor(type, mark, composition); const node = new Ctor(); node.value = value; ... return node; }
|
配置式的实现方式
在初始化图表时创建了options
方法。并根据options
中的type
字段创建了对应的mark class
。
在创建对应的mark class
之后将options
中除了type
和children
之外的属性作为value
写入了mark class
的实例。
函数式的实现方式
- 在初始化图表时创建了与
mark class
名称对应的方法。
- 在函数调用式的参数写入中为
mark class
中所需要的属性,创建了对应的方法。
总结
- 两种调用方式都是创建了一个某种类型的图表实例。
- 两种调用方式的不同本质上对应的是,创建图表类型实例方式的不同,以及参数写入方式的不同。
- 创建图表实例方式的不同本质上是通过 将所有的图表类型均作为
function
注册到runtime
上、以及在options
解析type
并手动创建对应的实例来实现的。
- 不同的参数写入方式是通过 将所有的参数类型通过装饰器创建对应的
function
进各个不同的图表类型以支持函数式的参数写入、以及在options
中解构type
和children
之外的参数来实现的。
参考 & 引用
开始使用 | G2 (antgroup.com)