Skip to content

Commit

Permalink
feat(graph-layers): More flexible graph loaders (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibgreen authored Jan 4, 2025
1 parent ddaf48b commit da43b65
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 38 deletions.
2 changes: 1 addition & 1 deletion modules/graph-layers/src/graph/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface EdgeOptions {
/** whether the edge is directed or not */
directed?: boolean;
/** origin data reference */
data: Record<string, unknown>;
data?: Record<string, unknown>;
}

/** Basic edge data structure */
Expand Down
25 changes: 20 additions & 5 deletions modules/graph-layers/src/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {Cache} from '../core/cache';
import {Edge} from './edge';
import {Node} from './node';

export type GraphProps = {
name?: string;
nodes?: Node[];
edges?: Edge[];
};

/** Basic graph data structure */
export class Graph extends EventTarget {
/** List object of nodes. */
Expand All @@ -23,19 +29,28 @@ export class Graph extends EventTarget {
/** Cached data: create array data from maps. */
private _cache = new Cache<'nodes' | 'edges', Node[] | Edge[]>();

constructor(props?: GraphProps);
constructor(graph: Graph);

/**
* The constructor of the Graph class.
* @param graph - copy the graph if this exists.
*/
constructor(graph: Graph | null = null) {
constructor(propsOrGraph?: GraphProps | Graph) {
super();

// copy the graph if it exists in the parameter
if (graph) {
// start copying the graph
if (propsOrGraph instanceof Graph) {
// if a Graph instance was supplied, copy the supplied graph into this graph
const graph = propsOrGraph;
this._name = graph?._name || this._name;
this._nodeMap = graph._nodeMap;
this._edgeMap = graph._edgeMap;
this._name = graph && graph._name;
} else {
// If graphProps were supplied, initialize this graph from the supplied props
const props = propsOrGraph;
this._name = props?.name || this._name;
this.batchAddNodes(props?.nodes || []);
this.batchAddEdges(props?.edges || []);
}
}

Expand Down
2 changes: 1 addition & 1 deletion modules/graph-layers/src/graph/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface NodeOptions {
selectable?: boolean;
highlightConnectedEdges?: boolean;
/* origin data reference */
data: Record<string, unknown>;
data?: Record<string, unknown>;
}

/** Basic data structure of a node */
Expand Down
2 changes: 1 addition & 1 deletion modules/graph-layers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
export {Graph} from './graph/graph';
export {Node} from './graph/node';
export {Edge} from './graph/edge';
export {createGraph} from './graph/create-graph';

export {GraphEngine} from './core/graph-engine';

Expand Down Expand Up @@ -39,4 +38,5 @@ export {mixedGetPosition} from './utils/layer-utils';
export {log} from './utils/log';

// DEPRECATED
export {createGraph} from './loaders/create-graph';
export {JSONLoader} from './loaders/simple-json-graph-loader';
110 changes: 87 additions & 23 deletions modules/graph-layers/src/layers/graph-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
import type {CompositeLayerProps} from '@deck.gl/core';
import {COORDINATE_SYSTEM, CompositeLayer} from '@deck.gl/core';

import {Stylesheet} from '../style/style-sheet';
import {NODE_TYPE, EDGE_DECORATOR_TYPE} from '../core/constants';
import {Graph} from '../graph/graph';
import {GraphLayout} from '../core/graph-layout';
import {GraphEngine} from '../core/graph-engine';

import {Stylesheet} from '../style/style-sheet';
import {mixedGetPosition} from '../utils/layer-utils';
import {InteractionManager} from '../core/interaction-manager';

Expand All @@ -18,14 +22,15 @@ import {ImageLayer} from './node-layers/image-layer';
import {LabelLayer} from './node-layers/label-layer';
import {RectangleLayer} from './node-layers/rectangle-layer';
import {RoundedRectangleLayer} from './node-layers/rounded-rectangle-layer';
import {PathBasedRoundedRectangleLayer} from './node-layers/path-rounded-rectange-layer';
import {PathBasedRoundedRectangleLayer} from './node-layers/path-rounded-rectangle-layer';
import {ZoomableMarkerLayer} from './node-layers/zoomable-marker-layer';

// edge layers
import {EdgeLayer} from './edge-layer';
import {EdgeLabelLayer} from './edge-layers/edge-label-layer';
import {FlowLayer} from './edge-layers/flow-layer';
import {GraphEngine} from '../core/graph-engine';

import {JSONLoader} from '../loaders/json-loader';

const NODE_LAYER_MAP = {
[NODE_TYPE.RECTANGLE]: RectangleLayer,
Expand All @@ -49,8 +54,14 @@ const SHARED_LAYER_PROPS = {
}
};

export type GraphLayerProps = {
engine: GraphEngine;
export type GraphLayerProps = CompositeLayerProps & _GraphLayerProps;

export type _GraphLayerProps = {
graph?: Graph;
layout?: GraphLayout;
graphLoader?: (opts: {json: any}) => Graph;
engine?: GraphEngine;

// an array of styles for layers
nodeStyle?: any[];
edgeStyle?: {
Expand All @@ -76,9 +87,14 @@ export type GraphLayerProps = {
export class GraphLayer extends CompositeLayer<GraphLayerProps> {
static layerName = 'GraphLayer';

static defaultProps: Required<GraphLayerProps> = {
static defaultProps: Required<_GraphLayerProps> = {
// Composite layer props
// @ts-expect-error composite layer props
pickable: true,

// Graph props
graphLoader: JSONLoader,

nodeStyle: [],
nodeEvents: {
onMouseLeave: () => {},
Expand All @@ -100,6 +116,12 @@ export class GraphLayer extends CompositeLayer<GraphLayerProps> {
enableDragging: false
};

// @ts-expect-error Some typescript confusion due to override of base class state
state!: CompositeLayer<GraphLayerProps>['state'] & {
interactionManager: InteractionManager;
graphEngine?: GraphEngine;
};

forceUpdate = () => {
if (this.context && this.context.layerManager) {
this.setNeedsUpdate();
Expand All @@ -109,33 +131,74 @@ export class GraphLayer extends CompositeLayer<GraphLayerProps> {

constructor(props: GraphLayerProps & CompositeLayerProps) {
super(props);

// added or removed a node, or in general something layout related changed
props.engine.addEventListener('onLayoutChange', this.forceUpdate);
}

initializeState() {
const interactionManager = new InteractionManager(this.props as any, () => this.forceUpdate());
this.state = {interactionManager};
this.state = {
interactionManager: new InteractionManager(this.props as any, () => this.forceUpdate())
};
const engine = this.props.engine;
this._setGraphEngine(engine);
}

shouldUpdateState({changeFlags}) {
return changeFlags.dataChanged || changeFlags.propsChanged;
}

updateState({props}) {
(this.state.interactionManager as any).updateProps(props);
updateState({props, oldProps, changeFlags}) {
if (
changeFlags.dataChanged &&
props.data &&
!(Array.isArray(props.data) && props.data.length === 0)
) {
// console.log(props.data);
const graph = this.props.graphLoader({json: props.data});
const layout = this.props.layout;
const graphEngine = new GraphEngine({graph, layout});
this._setGraphEngine(graphEngine);
this.state.interactionManager.updateProps(props);
this.forceUpdate();
} else if (changeFlags.propsChanged && props.graph !== oldProps.graph) {
const graphEngine = new GraphEngine({graph: props.graph, layout: props.layout});
this._setGraphEngine(graphEngine);
this.state.interactionManager.updateProps(props);
this.forceUpdate();
}
}

finalize() {
(this.props as any).engine.removeEventListener('onLayoutChange', this.forceUpdate);
this._removeGraphEngine();
}

_setGraphEngine(graphEngine: GraphEngine) {
if (graphEngine === this.state.graphEngine) {
return;
}

this._removeGraphEngine();
if (graphEngine) {
this.state.graphEngine = graphEngine;
this.state.graphEngine.run();
// added or removed a node, or in general something layout related changed
this.state.graphEngine.addEventListener('onLayoutChange', this.forceUpdate);
}
}

_removeGraphEngine() {
if (this.state.graphEngine) {
this.state.graphEngine.removeEventListener('onLayoutChange', this.forceUpdate);
this.state.graphEngine.clear();
this.state.graphEngine = null;
}
}

createNodeLayers() {
const {engine, nodeStyle} = this.props;
if (!nodeStyle || !Array.isArray(nodeStyle) || nodeStyle.length === 0) {
const engine = this.state.graphEngine;
const {nodeStyle} = this.props;
if (!engine || !nodeStyle || !Array.isArray(nodeStyle) || nodeStyle.length === 0) {
return [];
}

return nodeStyle.filter(Boolean).map((style, idx) => {
const {pickable = true, visible = true, data = (nodes) => nodes, ...restStyle} = style;
const LayerType = NODE_LAYER_MAP[style.type];
Expand Down Expand Up @@ -165,9 +228,10 @@ export class GraphLayer extends CompositeLayer<GraphLayerProps> {
}

createEdgeLayers() {
const {edgeStyle, engine} = this.props as any;
const engine = this.state.graphEngine;
const {edgeStyle} = this.props;

if (!edgeStyle) {
if (!engine || !edgeStyle) {
return [];
}

Expand Down Expand Up @@ -223,23 +287,23 @@ export class GraphLayer extends CompositeLayer<GraphLayerProps> {
}

onClick(info, event): boolean {
return (this.state.interactionManager as any).onClick(info, event) || false;
return (this.state.interactionManager.onClick(info, event) as unknown as boolean) || false;
}

onHover(info, event): boolean {
return (this.state.interactionManager as any).onHover(info, event) || false;
return (this.state.interactionManager.onHover(info, event) as unknown as boolean) || false;
}

onDragStart(info, event) {
(this.state.interactionManager as any).onDragStart(info, event);
this.state.interactionManager.onDragStart(info, event);
}

onDrag(info, event) {
(this.state.interactionManager as any).onDrag(info, event);
this.state.interactionManager.onDrag(info, event);
}

onDragEnd(info, event) {
(this.state.interactionManager as any).onDragEnd(info, event);
this.state.interactionManager.onDragEnd(info, event);
}

renderLayers() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {Edge} from './edge';
import {Node} from './node';
import {Graph} from './graph';
import {Edge} from '../graph/edge';
import {Node} from '../graph/node';
import {Graph} from '../graph/graph';

/** Create a graph from a list of Nodes and edges */
/**
* @deprecated Use `new Graph(name, nodes, edges)`
* Create a graph from a list of Nodes and edges
*/
export function createGraph(props: {name; nodes; edges; nodeParser; edgeParser}) {
const {name, nodes, edges, nodeParser, edgeParser} = props;
// create a new empty graph
Expand Down
2 changes: 1 addition & 1 deletion modules/graph-layers/src/loaders/json-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {createGraph} from '../graph/create-graph';
import {createGraph} from './create-graph';
import {basicNodeParser} from './node-parsers';
import {basicEdgeParser} from './edge-parsers';
import {log} from '../utils/log';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {createGraph} from '../graph/create-graph';
import {createGraph} from './create-graph';
import {log} from '../utils/log';
import {basicNodeParser} from './node-parsers';
import {basicEdgeParser} from './edge-parsers';
Expand Down
Loading

0 comments on commit da43b65

Please sign in to comment.