前言
拓扑图是数据可视化畛域一种比拟常见的展现类型,目前业界常见的可视化展示的计划有 ECharts、HighCharts、D3、AntV 等。以后的我的项目应用的是基于 ECharts 的动态关系图渲染,为了后续可能扩大成动静的拓扑图渲染,本文摸索了 ECharts 的原理以及 G6 的原理,也算是对自研一个可视化库的根本实现办法做了一个梳理。
计划抉择
-
ECharts
- 关系图
-
AntV
-
G6
- Graphin
-
源码解析
ECharts 源码
整个 ECharts 外围对外输入是一个大的 ECharts 类,所有的类型都是基于其进行 new 进去的实例,而其外围是基于对 ZRender 这样一个 Canvas 的封装
ECharts
class ECharts extends Eventful {
// 公共属性
group: string;
// 公有属性
private _zr: zrender.ZRenderType;
private _dom: HTMLElement;
private _model: GlobalModel;
private _throttledZrFlush: zrender.ZRenderType extends {flush: infer R} ? R : never;
private _theme: ThemeOption;
private _locale: LocaleOption;
private _chartsViews: ChartView[] = [];
private _chartsMap: {[viewId: string]: ChartView} = {};
private _componentsViews: ComponentView[] = [];
private _componentsMap: {[viewId: string]: ComponentView} = {};
private _coordSysMgr: CoordinateSystemManager;
private _api: ExtensionAPI;
private _scheduler: Scheduler;
private _messageCenter: MessageCenter;
private _pendingActions: Payload[] = [];
private _disposed: boolean;
private _loadingFX: LoadingEffect;
private _labelManager: LabelManager;
private [OPTION_UPDATED_KEY]: boolean | {silent: boolean};
private [IN_MAIN_PROCESS_KEY]: boolean;
private [CONNECT_STATUS_KEY]: ConnectStatus;
private [STATUS_NEEDS_UPDATE_KEY]: boolean;
// 爱护属性
protected _$eventProcessor: never;
constructor(
dom: HTMLElement,
theme?: string | ThemeOption,
opts?: {
locale?: string | LocaleOption,
renderer?: RendererType,
devicePixelRatio?: number,
useDirtyRect?: boolean,
width?: number,
height?: number
}
) {super(new ECEventProcessor());
opts = opts || {};
if (typeof theme === 'string') {theme = themeStorage[theme] as object;
}
this._dom = dom;
let defaultRenderer = 'canvas';
const zr = this._zr = zrender.init(dom, {
renderer: opts.renderer || defaultRenderer,
devicePixelRatio: opts.devicePixelRatio,
width: opts.width,
height: opts.height,
useDirtyRect: opts.useDirtyRect == null ? defaultUseDirtyRect : opts.useDirtyRect
});
this._locale = createLocaleObject(opts.locale || SYSTEM_LANG);
this._coordSysMgr = new CoordinateSystemManager();
const api = this._api = createExtensionAPI(this);
this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
this._initEvents();
zr.animation.on('frame', this._onframe, this);
bindRenderedEvent(zr, this);
bindMouseEvent(zr, this);
}
private _onframe(): void {}
getDom(): HTMLElement {return this._dom;}
getId(): string {return this.id;}
getZr(): zrender.ZRenderType {return this._zr;}
setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {if (lazyUpdate) {this[OPTION_UPDATED_KEY] = {silent: silent};
this[IN_MAIN_PROCESS_KEY] = false;
this.getZr().wakeUp();
}
else {prepare(this);
updateMethods.update.call(this);
this._zr.flush();
this[OPTION_UPDATED_KEY] = false;
this[IN_MAIN_PROCESS_KEY] = false;
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
}
private getModel(): GlobalModel {return this._model;}
getRenderedCanvas(opts?: {
backgroundColor?: ZRColor
pixelRatio?: number
}): HTMLCanvasElement {if (!env.canvasSupported) {return;}
opts = zrUtil.extend({}, opts || {});
opts.pixelRatio = opts.pixelRatio || this.getDevicePixelRatio();
opts.backgroundColor = opts.backgroundColor
|| this._model.get('backgroundColor');
const zr = this._zr;
return (zr.painter as CanvasPainter).getRenderedCanvas(opts);
}
private _initEvents(): void {each(MOUSE_EVENT_NAMES, (eveName) => {const handler = (e: ElementEvent) => {const ecModel = this.getModel();
const el = e.target;
let params: ECEvent;
const isGlobalOut = eveName === 'globalout';
if (isGlobalOut) {params = {} as ECEvent;
}
else {el && findEventDispatcher(el, (parent) => {const ecData = getECData(parent);
if (ecData && ecData.dataIndex != null) {const dataModel = ecData.dataModel || ecModel.getSeriesByIndex(ecData.seriesIndex);
params = (dataModel && dataModel.getDataParams(ecData.dataIndex, ecData.dataType) || {}) as ECEvent;
return true;
}
// If element has custom eventData of components
else if (ecData.eventData) {params = zrUtil.extend({}, ecData.eventData) as ECEvent;
return true;
}
}, true);
}
if (params) {
let componentType = params.componentType;
let componentIndex = params.componentIndex;
if (componentType === 'markLine'
|| componentType === 'markPoint'
|| componentType === 'markArea'
) {
componentType = 'series';
componentIndex = params.seriesIndex;
}
const model = componentType && componentIndex != null
&& ecModel.getComponent(componentType, componentIndex);
const view = model && this[model.mainType === 'series' ? '_chartsMap' : '_componentsMap'][model.__viewId];
params.event = e;
params.type = eveName;
(this._$eventProcessor as ECEventProcessor).eventInfo = {
targetEl: el,
packedEvent: params,
model: model,
view: view
};
this.trigger(eveName, params);
}
};
(handler as any).zrEventfulCallAtLast = true;
this._zr.on(eveName, handler, this);
});
each(eventActionMap, (actionType, eventType) => {this._messageCenter.on(eventType, function (event) {this.trigger(eventType, event);
}, this);
});
// Extra events
// TODO register?
each(['selectchanged'],
(eventType) => {this._messageCenter.on(eventType, function (event) {this.trigger(eventType, event);
}, this);
}
);
handleLegacySelectEvents(this._messageCenter, this, this._api);
}
dispatchAction(
payload: Payload,
opt?: boolean | {
silent?: boolean,
flush?: boolean | undefined
}
): void {
const silent = opt.silent;
doDispatchAction.call(this, payload, silent);
const flush = opt.flush;
if (flush) {this._zr.flush();
}
else if (flush !== false && env.browser.weChat) {this._throttledZrFlush();
}
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
}
ZRender
ZRender 是典型的 MVC 架构,其中 M 为 Storage,次要对数据进行 CRUD 治理;V 为 Painter,对 Canvas 或 SVG 的生命周期及视图进行治理;C 为 Handler,负责事件的交互解决,实现 dom 事件的模仿封装
class ZRender {
// 公共属性
dom: HTMLElement
id: number
storage: Storage
painter: PainterBase
handler: Handler
animation: Animation
// 公有属性
private _sleepAfterStill = 10;
private _stillFrameAccum = 0;
private _needsRefresh = true
private _needsRefreshHover = true
private _darkMode = false;
private _backgroundColor: string | GradientObject | PatternObject;
constructor(id: number, dom: HTMLElement, opts?: ZRenderInitOpt) {opts = opts || {};
/**
* @type {HTMLDomElement}
*/
this.dom = dom;
this.id = id;
const storage = new Storage();
let rendererType = opts.renderer || 'canvas';
// TODO WebGL
if (useVML) {throw new Error('IE8 support has been dropped since 5.0');
}
if (!painterCtors[rendererType]) {
// Use the first registered renderer.
rendererType = zrUtil.keys(painterCtors)[0];
}
if (!painterCtors[rendererType]) {throw new Error(`Renderer '${rendererType}' is not imported. Please import it first.`);
}
opts.useDirtyRect = opts.useDirtyRect == null
? false
: opts.useDirtyRect;
const painter = new painterCtors[rendererType](dom, storage, opts, id);
this.storage = storage;
this.painter = painter;
const handerProxy = (!env.node && !env.worker)
? new HandlerProxy(painter.getViewportRoot(), painter.root)
: null;
this.handler = new Handler(storage, painter, handerProxy, painter.root);
this.animation = new Animation({
stage: {update: () => this._flush(true)
}
});
this.animation.start();}
/**
* 增加元素
*/
add(el: Element) { }
/**
* 删除元素
*/
remove(el: Element) { }
refresh() {
this._needsRefresh = true;
// Active the animation again.
this.animation.start();}
private _flush(fromInside?: boolean) {
let triggerRendered;
const start = new Date().getTime();
if (this._needsRefresh) {
triggerRendered = true;
this.refreshImmediately(fromInside);
}
if (this._needsRefreshHover) {
triggerRendered = true;
this.refreshHoverImmediately();}
const end = new Date().getTime();
if (triggerRendered) {
this._stillFrameAccum = 0;
this.trigger('rendered', {elapsedTime: end - start});
}
else if (this._sleepAfterStill > 0) {
this._stillFrameAccum++;
// Stop the animiation after still for 10 frames.
if (this._stillFrameAccum > this._sleepAfterStill) {this.animation.stop();
}
}
}
on<Ctx>(eventName: string, eventHandler: EventCallback<Ctx, unknown> | EventCallback<Ctx, unknown, ElementEvent>, context?: Ctx): this {this.handler.on(eventName, eventHandler, context);
return this;
}
off(eventName?: string, eventHandler?: EventCallback<unknown, unknown> | EventCallback<unknown, unknown, ElementEvent>) {this.handler.off(eventName, eventHandler);
}
trigger(eventName: string, event?: unknown) {this.handler.trigger(eventName, event);
}
clear() {}
dispose() {}
}
G6 源码
G6 是 AntV 专门针对图开源的一个库,其底层通过对边和点的定义,以及对地位的确定,来进行图的绘制,其次要包含五大内容:1、图的元素:点、边、分组等;2、图的算法:DFS、BFS、图检测、最短门路、核心度等;3、图布局:force、circle、grid 等;4、图渲染:Canvas 及 SVG 等;5、图交互:框选、点选、拖拽等;而 Graphin 是基于 G6 的应用 React 封装的落地计划
G6
和 ECharts 的外围思路是统一的,都是基于 MVC 的模型,然而 G6 针对图的特点对元素进行了细化,用御术的话说就是“G6 是面粉,ECharts 是面条”,果然同一个作者开发的思路都是极其的类似
export default abstract class AbstractGraph extends EventEmitter implements IAbstractGraph {
protected animating: boolean;
protected cfg: GraphOptions & {[key: string]: any };
protected undoStack: Stack;
protected redoStack: Stack;
public destroyed: boolean;
constructor(cfg: GraphOptions) {super();
this.cfg = deepMix(this.getDefaultCfg(), cfg);
this.init();
this.animating = false;
this.destroyed = false;
if (this.cfg.enabledStack) {this.undoStack = new Stack(this.cfg.maxStep);
this.redoStack = new Stack(this.cfg.maxStep);
}
}
protected init() {this.initCanvas();
const viewController = new ViewController(this);
const modeController = new ModeController(this);
const itemController = new ItemController(this);
const stateController = new StateController(this);
this.set({
viewController,
modeController,
itemController,
stateController,
});
this.initLayoutController();
this.initEventController();
this.initGroups();
this.initPlugins();}
protected abstract initLayoutController(): void;
protected abstract initEventController(): void;
protected abstract initCanvas(): void;
protected abstract initPlugins(): void;
protected initGroups(): void {const canvas: ICanvas = this.get('canvas');
const el: HTMLElement = this.get('canvas').get('el');
const {id} = el;
const group: IGroup = canvas.addGroup({id: `${id}-root`,
className: Global.rootContainerClassName,
});
if (this.get('groupByTypes')) {
const edgeGroup: IGroup = group.addGroup({id: `${id}-edge`,
className: Global.edgeContainerClassName,
});
const nodeGroup: IGroup = group.addGroup({id: `${id}-node`,
className: Global.nodeContainerClassName,
});
const comboGroup: IGroup = group.addGroup({id: `${id}-combo`,
className: Global.comboContainerClassName,
});
// 用于存储自定义的群组
comboGroup.toBack();
this.set({nodeGroup, edgeGroup, comboGroup});
}
const delegateGroup: IGroup = group.addGroup({id: `${id}-delegate`,
className: Global.delegateContainerClassName,
});
this.set({delegateGroup});
this.set('group', group);
}
public node(nodeFn: (config: NodeConfig) => Partial<NodeConfig>): void {if (typeof nodeFn === 'function') {this.set('nodeMapper', nodeFn);
}
}
public edge(edgeFn: (config: EdgeConfig) => Partial<EdgeConfig>): void {if (typeof edgeFn === 'function') {this.set('edgeMapper', edgeFn);
}
}
public combo(comboFn: (config: ComboConfig) => Partial<ComboConfig>): void {if (typeof comboFn === 'function') {this.set('comboMapper', comboFn);
}
}
public addBehaviors(behaviors: string | ModeOption | ModeType[],
modes: string | string[],): AbstractGraph {const modeController: ModeController = this.get('modeController');
modeController.manipulateBehaviors(behaviors, modes, true);
return this;
}
public removeBehaviors(behaviors: string | ModeOption | ModeType[],
modes: string | string[],): AbstractGraph {const modeController: ModeController = this.get('modeController');
modeController.manipulateBehaviors(behaviors, modes, false);
return this;
}
public paint(): void {this.emit('beforepaint');
this.get('canvas').draw();
this.emit('afterpaint');
}
public render(): void {
const self = this;
this.set('comboSorted', false);
const data: GraphData = this.get('data');
if (this.get('enabledStack')) {
// render 之前清空 redo 和 undo 栈
this.clearStack();}
if (!data) {throw new Error('data must be defined first');
}
const {nodes = [], edges = [], combos = [] } = data;
this.clear();
this.emit('beforerender');
each(nodes, (node: NodeConfig) => {self.add('node', node, false, false);
});
// process the data to tree structure
if (combos && combos.length !== 0) {const comboTrees = plainCombosToTrees(combos, nodes);
this.set('comboTrees', comboTrees);
// add combos
self.addCombos(combos);
}
each(edges, (edge: EdgeConfig) => {self.add('edge', edge, false, false);
});
const animate = self.get('animate');
if (self.get('fitView') || self.get('fitCenter')) {self.set('animate', false);
}
// layout
const layoutController = self.get('layoutController');
if (layoutController) {layoutController.layout(success);
if (this.destroyed) return;
} else {if (self.get('fitView')) {self.fitView();
}
if (self.get('fitCenter')) {self.fitCenter();
}
self.emit('afterrender');
self.set('animate', animate);
}
// 将在 onLayoutEnd 中被调用
function success() {
// fitView 与 fitCenter 共存时,fitView 优先,fitCenter 不再执行
if (self.get('fitView')) {self.fitView();
} else if (self.get('fitCenter')) {self.fitCenter();
}
self.autoPaint();
self.emit('afterrender');
if (self.get('fitView') || self.get('fitCenter')) {self.set('animate', animate);
}
}
if (!this.get('groupByTypes')) {if (combos && combos.length !== 0) {this.sortCombos();
} else {
// 为晋升性能,抉择数量少的进行操作
if (data.nodes && data.edges && data.nodes.length < data.edges.length) {const nodesArr = this.getNodes();
// 遍历节点实例,将所有节点提前。nodesArr.forEach((node) => {node.toFront();
});
} else {const edgesArr = this.getEdges();
// 遍历节点实例,将所有节点提前。edgesArr.forEach((edge) => {edge.toBack();
});
}
}
}
if (this.get('enabledStack')) {this.pushStack('render');
}
}
}
Graphin
Graphin 是基于 G6 封装的 React 组件,能够间接进行应用
import React, {ErrorInfo} from 'react';
import G6, {Graph as IGraph, GraphOptions, GraphData, TreeGraphData} from '@antv/g6';
class Graphin extends React.PureComponent<GraphinProps, GraphinState> {static registerNode: RegisterFunction = (nodeName, options, extendedNodeName) => {G6.registerNode(nodeName, options, extendedNodeName);
};
static registerEdge: RegisterFunction = (edgeName, options, extendedEdgeName) => {G6.registerEdge(edgeName, options, extendedEdgeName);
};
static registerCombo: RegisterFunction = (comboName, options, extendedComboName) => {G6.registerCombo(comboName, options, extendedComboName);
};
static registerBehavior(behaviorName: string, behavior: any) {G6.registerBehavior(behaviorName, behavior);
}
static registerFontFamily(iconLoader: IconLoader): {[icon: string]: any } {
/** 注册 font icon */
const iconFont = iconLoader();
const {glyphs, fontFamily} = iconFont;
const icons = glyphs.map((item) => {
return {
name: item.name,
unicode: String.fromCodePoint(item.unicode_decimal),
};
});
return new Proxy(icons, {get: (target, propKey: string) => {const matchIcon = target.find((icon) => {return icon.name === propKey;});
if (!matchIcon) {console.error(`%c fontFamily:${fontFamily},does not found ${propKey} icon`);
return '';
}
return matchIcon?.unicode;
},
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static registerLayout(layoutName: string, layout: any) {G6.registerLayout(layoutName, layout);
}
graphDOM: HTMLDivElement | null = null;
graph: IGraph;
layout: LayoutController;
width: number;
height: number;
isTree: boolean;
data: GraphinTreeData | GraphinData | undefined;
options: GraphOptions;
apis: ApisType;
theme: ThemeData;
constructor(props: GraphinProps) {super(props);
const {
data,
layout,
width,
height,
...otherOptions
} = props;
this.data = data;
this.isTree =
Boolean(props.data && props.data.children) || TREE_LAYOUTS.indexOf(String(layout && layout.type)) !== -1;
this.graph = {} as IGraph;
this.height = Number(height);
this.width = Number(width);
this.theme = {} as ThemeData;
this.apis = {} as ApisType;
this.state = {
isReady: false,
context: {
graph: this.graph,
apis: this.apis,
theme: this.theme,
},
};
this.options = {...otherOptions} as GraphOptions;
this.layout = {} as LayoutController;}
initData = (data: GraphinProps['data']) => {if (data.children) {this.isTree = true;}
console.time('clone data');
this.data = cloneDeep(data);
console.timeEnd('clone data');
};
initGraphInstance = () => {
const {
theme,
data,
layout,
width,
height,
defaultCombo,
defaultEdge,
defaultNode,
nodeStateStyles,
edgeStateStyles,
comboStateStyles,
modes = {default: [] },
animate,
...otherOptions
} = this.props;
const {clientWidth, clientHeight} = this.graphDOM as HTMLDivElement;
this.initData(data);
this.width = Number(width) || clientWidth || 500;
this.height = Number(height) || clientHeight || 500;
const themeResult = getDefaultStyleByTheme(theme);
const {
defaultNodeStyle,
defaultEdgeStyle,
defaultComboStyle,
defaultNodeStatusStyle,
defaultEdgeStatusStyle,
defaultComboStatusStyle,
} = themeResult;
this.theme = themeResult as ThemeData;
this.isTree = Boolean(data.children) || TREE_LAYOUTS.indexOf(String(layout && layout.type)) !== -1;
const isGraphinNodeType = defaultNode?.type === undefined || defaultNode?.type === defaultNodeStyle.type;
const isGraphinEdgeType = defaultEdge?.type === undefined || defaultEdge?.type === defaultEdgeStyle.type;
this.options = {
container: this.graphDOM,
renderer: 'canvas',
width: this.width,
height: this.height,
animate: animate !== false,
/** 默认款式 */
defaultNode: isGraphinNodeType ? deepMix({}, defaultNodeStyle, defaultNode) : defaultNode,
defaultEdge: isGraphinEdgeType ? deepMix({}, defaultEdgeStyle, defaultEdge) : defaultEdge,
defaultCombo: deepMix({}, defaultComboStyle, defaultCombo),
/** status 款式 */
nodeStateStyles: deepMix({}, defaultNodeStatusStyle, nodeStateStyles),
edgeStateStyles: deepMix({}, defaultEdgeStatusStyle, edgeStateStyles),
comboStateStyles: deepMix({}, defaultComboStatusStyle, comboStateStyles),
modes,
...otherOptions,
} as GraphOptions;
if (this.isTree) {this.options.layout = { ...layout};
this.graph = new G6.TreeGraph(this.options);
} else {this.graph = new G6.Graph(this.options);
}
this.graph.data(this.data as GraphData | TreeGraphData);
/** 初始化布局 */
if (!this.isTree) {this.layout = new LayoutController(this);
this.layout.start();}
this.graph.get('canvas').set('localRefresh', false);
this.graph.render();
this.initStatus();
this.apis = ApiController(this.graph);
};
updateLayout = () => {this.layout.changeLayout();
};
componentDidMount() {console.log('did mount...');
this.initGraphInstance();
this.setState({
isReady: true,
context: {
graph: this.graph,
apis: this.apis,
theme: this.theme,
},
});
}
updateOptions = () => {const { layout, data, ...options} = this.props;
return options;
};
initStatus = () => {if (!this.isTree) {const { data} = this.props;
const {nodes = [], edges = []} = data as GraphinData;
nodes.forEach((node) => {const { status} = node;
if (status) {Object.keys(status).forEach((k) => {this.graph.setItemState(node.id, k, Boolean(status[k]));
});
}
});
edges.forEach((edge) => {const { status} = edge;
if (status) {Object.keys(status).forEach((k) => {this.graph.setItemState(edge.id, k, Boolean(status[k]));
});
}
});
}
};
componentDidUpdate(prevProps: GraphinProps) {console.time('did-update');
const isDataChange = this.shouldUpdate(prevProps, 'data');
const isLayoutChange = this.shouldUpdate(prevProps, 'layout');
const isOptionsChange = this.shouldUpdate(prevProps, 'options');
const isThemeChange = this.shouldUpdate(prevProps, 'theme');
console.timeEnd('did-update');
const {data} = this.props;
const isGraphTypeChange = prevProps.data.children !== data.children;
/** 图类型变动 */
if (isGraphTypeChange) {this.initGraphInstance();
console.log('%c isGraphTypeChange', 'color:grey');
}
/** 配置变动 */
if (isOptionsChange) {this.updateOptions();
console.log('isOptionsChange');
}
/** 数据变动 */
if (isDataChange) {this.initData(data);
this.layout.changeLayout();
this.graph.data(this.data as GraphData | TreeGraphData);
this.graph.changeData(this.data as GraphData | TreeGraphData);
this.initStatus();
this.apis = ApiController(this.graph);
console.log('%c isDataChange', 'color:grey');
this.setState((preState) => {
return {
...preState,
context: {
graph: this.graph,
apis: this.apis,
theme: this.theme,
},
};
});
return;
}
/** 布局变动 */
if (isLayoutChange) {
/**
* TODO
* 1. preset 前置布局判断问题
* 2. enablework 问题
* 3. G6 LayoutController 里的逻辑
*/
this.layout.changeLayout();
this.layout.refreshPosition();
/** 走 G6 的 layoutController */
// this.graph.updateLayout();
console.log('%c isLayoutChange', 'color:grey');
}
}
/**
* 组件移除的时候
*/
componentWillUnmount() {this.clear();
}
/**
* 组件解体的时候
* @param error
* @param info
*/
componentDidCatch(error: Error, info: ErrorInfo) {console.error('Catch component error:', error, info);
}
clear = () => {if (this.layout && this.layout.destroyed) {this.layout.destroy(); // tree graph
}
this.layout = {} as LayoutController;
this.graph!.clear();
this.data = {nodes: [], edges: [], combos: [] };
this.graph!.destroy();};
shouldUpdate(prevProps: GraphinProps, key: string) {
/* eslint-disable react/destructuring-assignment */
const prevVal = prevProps[key];
const currentVal = this.props[key] as DiffValue;
const isEqual = deepEqual(prevVal, currentVal);
return !isEqual;
}
render() {const { isReady} = this.state;
const {modes, style} = this.props;
return (<GraphinContext.Provider value={this.state.context}>
<div id="graphin-container">
<div
data-testid="custom-element"
className="graphin-core"
ref={(node) => {this.graphDOM = node;}}
style={{background: this.theme?.background, ...style}}
/>
<div className="graphin-components">
{isReady && (
<>
{
/** modes 不存在的时候,才启动默认的 behaviros,否则会笼罩用户本人传入的 */
!modes && (
<React.Fragment>
{/* 拖拽画布 */}
<DragCanvas />
{/* 缩放画布 */}
<ZoomCanvas />
{/* 拖拽节点 */}
<DragNode />
{/* 点击节点 */}
<DragCombo />
{/* 点击节点 */}
<ClickSelect />
{/* 圈选节点 */}
<BrushSelect />
</React.Fragment>
)
}
{/** resize 画布 */}
<ResizeCanvas graphDOM={this.graphDOM as HTMLDivElement} />
<Hoverable bindType="node" />
{/* <Hoverable bindType="edge" /> */}
{this.props.children}
</>
)}
</div>
</div>
</GraphinContext.Provider>
);
}
}
总结
数据可视化通常是基于 Canvas 进行渲染的,对于简略的图形渲染,咱们经常一个实例一个实例去写,短少系统性的统筹规划的概念,对于须要解决一类问题的可视化计划,能够借鉴 ECharts 及 G6 引擎的做法,基于 MVC 模型,将展现、行为及数据进行拆散,对于特定计划细粒度的把控能够参考 G6 的计划。实质上,大数据可视化展现是一个兼具大数据、视觉传播、前端等多方穿插的畛域,对于怎么进行数据粒度的柔美展现,能够借鉴 data-ink ratio 以及利用力导布局的算法(ps:引入库伦斥力及胡克弹力阻尼衰减进行动效展现,同时配合边线权重进行节点聚合),对于这方面感兴趣的同学,能够参考往年 SEE Conf 的《图解万物——AntV 图可视化剖析解决方案》,数据可视化畛域既业余又穿插,对于深挖此道的同学还是须要下一番功夫的。
参考
- ECharts 关系图官网
- ECharts 官网源码
- ECharts 3.0 源码简要剖析 1 - 总体架构
- ZRender 官网源码
- ZRender 源码剖析 1:总体构造
- ZRender 源码剖析 2:Storage(Model 层)
- ZRender 源码剖析 3:Painter(View 层)- 上
- ZRender 源码剖析 4:Painter(View 层)- 中
- ZRender 源码剖析 5:Shape 绘图详解
- ZRender 源码剖析 6:Shape 对象详解之门路
- G6 官网
- G6 官网源码
- G6 源码浏览 -part1- 运行主流程
- G6 源码浏览 -Part2-Item 与 Shape
- G6 源码浏览 -Part3- 绘制 Paint
- Graphin 官网源码
- Graphin 官网