import * as d3 from 'd3'; import { LabelPlacement } from './labellers/LinkLabeller'; import { DragManager } from './managers/DragManager'; import { EdgeManager } from './managers/EdgeManager'; import { NodeManager } from './managers/NodeManager'; import { AxesRenderer } from './renderers/AxesRenderer'; import { LabelRenderer } from './renderers/labels/LabelRenderer'; import { LinkLabelRenderer } from './renderers/labels/LinkLabelRenderer'; import { LinkRenderer } from './renderers/links/LinkRenderer'; import { ShapeRenderer } from './renderers/shapes/ShapeRenderer'; import { BundleRouter } from './routers/BundleRouter'; import { EdgeStyle } from './styles/EdgeStyle'; import { NodeStyle } from './styles/NodeStyle'; import { Edge } from './types/Edge'; import { Node } from './types/Node'; class Graph { // Canvas rendering private _canvas: HTMLCanvasElement | null; private _context: CanvasRenderingContext2D | null; // Renderers private _axes: AxesRenderer; private _linkLabelRenderers: LinkLabelRenderer[]; private _linkRenderers: LinkRenderer[]; private _nodeLabelRenderers: LabelRenderer[]; private _nodeRenderers: ShapeRenderer[]; // Managers private _nodeManager: NodeManager; private _edgeManager: EdgeManager; private _dragManager: DragManager | null; // Sizing, scaling, interactivity private _width: number; private _height: number; private _x: d3.ScaleLinear; private _y: d3.ScaleLinear; private _transform: d3.ZoomTransform; // Settings private _axesVisible: boolean; constructor (canvas?: HTMLCanvasElement) { // Canvas rendering this._canvas = canvas || null; this._context = null; // Renderers this._axes = new AxesRenderer(); this._linkLabelRenderers = []; this._linkRenderers = []; this._nodeLabelRenderers = []; this._nodeRenderers = []; // Sizing, scaling this._width = 0; this._height = 0; this._x = d3.scaleLinear(); this._y = d3.scaleLinear(); this._transform = d3.zoomIdentity; // Managers this._nodeManager = new NodeManager(); this._edgeManager = new EdgeManager(); this._dragManager = null; // Settings this._axesVisible = true; // Attempt to initialize the canvas this._initializeCanvas(); } axesVisible (visible: boolean): this { this._axes.axesVisible(visible); return this; } boundingBox (): [number, number, number, number] { const t = this._transform.invertY(0); const r = this._transform.invertX(this._width); const b = this._transform.invertY(this._height); const l = this._transform.invertX(0); return [ this._y.invert(t), this._x.invert(r), this._y.invert(b), this._x.invert(l) ]; } bundleFactor (): number | undefined; bundleFactor (factor: number): this; bundleFactor (factor?: number): number | undefined | this { const router = this._edgeManager.getRouter(); if (!arguments.length) return router instanceof BundleRouter ? router.getFactor() : undefined; if (router instanceof BundleRouter) router.setFactor(factor!); return this; } canvas (canvas: HTMLCanvasElement | null): this { this._canvas = canvas; this._initializeCanvas(); return this; } edgeLabelPlacement (): LabelPlacement; edgeLabelPlacement (placement: LabelPlacement): this; edgeLabelPlacement (placement?: LabelPlacement): LabelPlacement | this { return arguments.length ? (this._edgeManager.setLabelPlacement(placement!), this) : this._edgeManager.getLabelPlacement(); } edges (): Edge[]; edges (edges: Edge[]): this; edges (edges?: Edge[]): Edge[] | this { if (!edges) return this._edgeManager.getEdges(); this._edgeManager.setEdges(edges); return this; } edgeStyles (styles: EdgeStyle[]): this { this._edgeManager.setStyles(styles); return this; } gridVisible (visible: boolean): this { this._axes.gridVisible(visible); return this; } resize (): void { if (this._canvas) { const styles = getComputedStyle(this._canvas); const width: number = parseInt(styles.getPropertyValue('width')); const height: number = parseInt(styles.getPropertyValue('height')); this._setDimensions(width, height); this._render(); } } nodes (): Node[]; nodes (nodes: (Node | string)[]): this; nodes (nodes?: (Node | string)[]): Node[] | this { if (!nodes) return this._nodeManager.nodes(); // Set the nodes, converting all strings to node objects this._nodeManager.nodes(nodes); // Get the node objects const nodeobjs = this._nodeManager.nodes(); // Tell everyone else about the nodes this._edgeManager.setNodes(nodeobjs); return this; } nodeStyles (styles: NodeStyle[]): this { this._nodeManager.setStyles(styles); return this; } update (): void { // Get the node renderers this._nodeManager.update(); this._nodeRenderers = this._nodeManager.shapeRenderers(); // Tell the edge manager which shapes will be rendering the nodes this._edgeManager.setShapes(this._nodeRenderers); this._edgeManager.update(); // Tell the drag manager which shapes will be rendering the nodes if (this._dragManager) this._dragManager.setShapes(this._nodeRenderers); // Get the link renderers this._linkRenderers = this._edgeManager.linkRenderers(); // Get the label renderers this._linkLabelRenderers = this._edgeManager.labelRenderers(); this._nodeLabelRenderers = this._nodeManager.labelRenderers(); this._render(); } private _initializeCanvas (): void { const canvas = this._canvas; if (canvas) { // Get the 2D rendering context const context = canvas.getContext('2d'); if (context === null) throw Error('Unable to initialize canvas'); this._context = context; // Create the drag manager this._dragManager = new DragManager(canvas, this._x, this._y); // Initialize interactivity const zoom = d3.zoom() .scaleExtent([0.1, 500]) .on('zoom', this._zoomed.bind(this)); d3.select(canvas) .call(zoom) .call(zoom.transform, this._transform); this._dragManager.addEventListener('drag', event => { this._edgeManager.updateEdgesConnectedTo(event.node); this._render(); }); this.update(); this.resize(); } else { this._context = null; this._dragManager = null; } } private _render (): void { const context = this._context; if (context) { window.requestAnimationFrame(() => { const x = this._transform.rescaleX(this._x); const y = this._transform.rescaleY(this._y); // Clear the context context.clearRect(0, 0, this._width, this._height); // Render axes if (this._axesVisible) this._axes.render(context, x, y); // Render links this._linkRenderers.forEach(renderer => { renderer.render(context, x, y); }); // Render link labels this._linkLabelRenderers.forEach(renderer => { renderer.render(context, x, y); }); // Render nodes this._nodeRenderers.forEach(renderer => { renderer.render(context, x, y); }); // Render labels this._nodeLabelRenderers.forEach(renderer => { renderer.render(context, x, y); }); }); } } private _setDimensions (width: number, height: number): void { if (this._canvas) { this._canvas.setAttribute('width', width.toString()); this._canvas.setAttribute('height', height.toString()); this._width = width; this._height = height; this._x.domain([-width / 2, width / 2]).range([0, width]); this._y.domain([height / 2, -height / 2]).range([0, height]); } } private _setTransform (transform: d3.ZoomTransform): void { this._transform = transform; if (this._dragManager) this._dragManager.setTransform(transform); } private _zoomed () { this._setTransform(d3.event.transform); this._render(); } } export { Graph }