preface
AntV It is a new generation of data visualization solution of ant financial services. X6 is mainly used to solve the solutions related to the field of graph editing. It is a graph editing engine with built-in functions and components required by the editor. This paper aims to have a general understanding of some underlying engines in the field of graph editing through a brief analysis of X6 source code, At the same time, it also provides some side understanding for the graph editor that needs to be built based on X6 editing engine in the team, and can quickly find the problem points when encountering problems.
framework
X6 is designed based on the MVVM architecture as a whole. Graph classes are exposed to the outside world. Node, Edge, Port and other exposed methods can be used separately. Some dom operation methods of Jquery class are provided. The overall graph is based on an event base class to process events as a whole, and dispose is used to display and judge instances.
The overall design conforms to SOLID principle , provide event mechanism to decouple publish and subscribe, and provide registration mechanism for extensible structure to organize extensible plug-ins
catalogue
monorepo is used to manage the source code warehouse as a whole
packages
x6
- addon
- common
- geometry
- global
- graph
- layout
- model
- registry
- shape
- style
- types
- util
- view
- x6-angular-shape
x6-geometry
- angle
- curve
- ellipse
- line
- point
- polyline
- rectangle
- x6-react
- x6-react-components
- x6-react-shape
- x6-vector
- x6-vue-shape
Source code
From the architecture level, it can be seen that the overall external exposure is a large class such as Graph. Therefore, in the process of analyzing the source code call, we grasp the Graph to expand step by step, so as to grasp a design link of the whole and avoid falling into a part that cannot be separated
Graph
The Graph class provides a summary of all the structures of the whole, which is exposed to the user
class Graph extends Basecoat<EventArgs> { public readonly options: GraphOptions.Definition public readonly css: CSSManager public readonly model: Model public readonly view: GraphView public readonly hook: HookManager public readonly grid: Grid public readonly defs: Defs public readonly knob: Knob public readonly coord: Coord public readonly renderer: ViewRenderer public readonly snapline: Snapline public readonly highlight: Highlight public readonly transform: Transform public readonly clipboard: Clipboard public readonly selection: Selection public readonly background: Background public readonly history: History public readonly scroller: Scroller public readonly minimap: MiniMap public readonly keyboard: Shortcut public readonly mousewheel: Wheel public readonly panning: Panning public readonly print: Print public readonly format: Format public readonly size: SizeManager // Get the container to be loaded public get container() { return this.view.container } protected get [Symbol.toStringTag]() { return Graph.toStringTag } constructor(options: Partial<GraphOptions.Manual>) { super() this.options = GraphOptions.get(options) this.css = new CSSManager(this) this.hook = new HookManager(this) this.view = this.hook.createView() this.defs = this.hook.createDefsManager() this.coord = this.hook.createCoordManager() this.transform = this.hook.createTransformManager() this.knob = this.hook.createKnobManager() this.highlight = this.hook.createHighlightManager() this.grid = this.hook.createGridManager() this.background = this.hook.createBackgroundManager() this.model = this.hook.createModel() this.renderer = this.hook.createRenderer() this.clipboard = this.hook.createClipboardManager() this.snapline = this.hook.createSnaplineManager() this.selection = this.hook.createSelectionManager() this.history = this.hook.createHistoryManager() this.scroller = this.hook.createScrollerManager() this.minimap = this.hook.createMiniMapManager() this.keyboard = this.hook.createKeyboard() this.mousewheel = this.hook.createMouseWheel() this.print = this.hook.createPrintManager() this.format = this.hook.createFormatManager() this.panning = this.hook.createPanningManager() this.size = this.hook.createSizeManager() } }
Shape
Realize the intermediate decoupling layer of various types of methods for wrapping attributes, etc
// The base class of shape, which marks various attributes of shape, such as labels class Base< Properties extends Node.Properties = Node.Properties, > extends Node<Properties> { get label() { return this.getLabel() } set label(val: string | undefined | null) { this.setLabel(val) } getLabel() { return this.getAttrByPath<string>('text/text') } setLabel(label?: string | null, options?: Node.SetOptions) { if (label == null) { this.removeLabel() } else { this.setAttrByPath('text/text', label, options) } return this } removeLabel() { this.removeAttrByPath('text/text') return this } }
// How to create a shape function createShape( shape: string, config: Node.Config, options: { noText?: boolean ignoreMarkup?: boolean parent?: Node.Definition | typeof Base } = {}, ) { const name = getName(shape) const defaults: Node.Config = { constructorName: name, attrs: { '.': { fill: '#ffffff', stroke: 'none', }, [shape]: { fill: '#ffffff', stroke: '#000000', }, }, } if (!options.ignoreMarkup) { defaults.markup = getMarkup(shape, options.noText === true) } const base = options.parent || Base return base.define( ObjectExt.merge(defaults, config, { shape: name }), ) as typeof Base }
Model
Provides processing methods for Node, Cell, Edge, Prot, etc
class Model extends Basecoat<Model.EventArgs> { public readonly collection: Collection protected readonly batches: KeyValue<number> = {} protected readonly addings: WeakMap<Cell, boolean> = new WeakMap() public graph: Graph | null protected nodes: KeyValue<boolean> = {} protected edges: KeyValue<boolean> = {} protected outgoings: KeyValue<string[]> = {} protected incomings: KeyValue<string[]> = {} protected get [Symbol.toStringTag]() { return Model.toStringTag } constructor(cells: Cell[] = []) { super() this.collection = new Collection(cells) this.setup() } }
Renderer
Render Model related data
class Renderer extends Base { protected views: KeyValue<CellView> protected zPivots: KeyValue<Comment> protected updates: Renderer.Updates protected init() {} protected startListening() {} protected stopListening() {} protected resetUpdates() {} protected onSortModel() {} protected onModelReseted() {} protected onBatchStop() {} protected onCellAdded() {} protected onCellRemove() {} protected onCellZIndexChanged() {} protected onCellVisibleChanged() {} protected processEdgeOnTerminalVisibleChanged() {} protected isEdgeTerminalVisible() {} }
Store
The public storage warehouse of data to interact with renderer
class Store<D> extends Basecoat<Store.EventArgs<D>>{ protected data: D protected previous: D protected changed: Partial<D> protected pending = false protected changing = false protected pendingOptions: Store.MutateOptions | null protected mutate<K extends keyof D>() {} constructor(data: Partial<D> = {}) { super() this.data = {} as D this.mutate(ObjectExt.cloneDeep(data)) this.changed = {} } get() {} set() {} remove() {} clone() {} }
View
Aggregate EdgeView, CellView, etc., and use the relevant DOM operations of jQuery
abstract class View<EventArgs = any> extends Basecoat<EventArgs> { public readonly cid: string public container: Element protected selectors: Markup.Selectors public get priority() { return 2 } constructor() { super() this.cid = Private.uniqueId() View.views[this.cid] = this } }
Geometry
Provide operation and processing of geometry, including Curve, Ellipse, Line, Point, PolyLine, Rectangle, Angle, etc
abstract class Geometry { abstract scale( sx: number, sy: number, origin?: Point.PointLike | Point.PointData, ): this abstract rotate( angle: number, origin?: Point.PointLike | Point.PointData, ): this abstract translate(tx: number, ty: number): this abstract translate(p: Point.PointLike | Point.PointData): this abstract equals(g: any): boolean abstract clone(): Geometry abstract toJSON(): JSONObject | JSONArray abstract serialize(): string valueOf() { return this.toJSON() } toString() { return JSON.stringify(this.toJSON()) } }
Registry
Provide a mechanism for the registry,
class Registry< Entity, Presets = KeyValue<Entity>, OptionalType = never, > { public readonly data: KeyValue<Entity> public readonly options: Registry.Options<Entity | OptionalType> constructor(options: Registry.Options<Entity | OptionalType>) { this.options = { ...options } this.data = (this.options.data as KeyValue<Entity>) || {} this.register = this.register.bind(this) this.unregister = this.unregister.bind(this) } get names() { return Object.keys(this.data) } register() {} unregister() {} get() {} exist() {} }
Events
Provide event listening (publish subscribe) mechanism
class Events<EventArgs extends Events.EventArgs = any> { private listeners: { [name: string]: any[] } = {} on() {} once() {} off() {} trigger() {} emit() {} }
summary
On the whole, we can see that in order to realize an underlying graph editing engine, we need to do a good job in the overall architecture design and deconstruction, which is usually nothing more than the structural variants of MVC. Therefore, in the process of selecting Model layer, View layer and Controller layer, we can comprehensively consider different design schemes in software engineering, such as the design of event system and plug-in mechanism, In addition, in terms of bottom rendering, as the front-end scheme in the field of graph visualization, the selection of different schemes such as SVG, HTML and Canvas also needs targeted consideration, as mentioned above. The depth and breadth of the visualization field are not limited to the front-end side. I hope to systematically study and practice in this area, so as to explore some opportunities in the front-end field and encourage each other!!!