Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions docs/modules/graph-layers/api-reference/layouts/graph-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export class MyLayout extends GraphLayout {

We will start with a `RandomLayout` as an example, you can follow the steps one by one and find the source code at the bottom.

### Updating layout props

All layouts expose a `setProps(partialProps)` method that merges new configuration into the current layout instance. The helper validates incoming values and returns `true` when the layout should recompute. Most importantly, the graph data consumed by the layout must now be supplied through this method:

```js
layout.setProps({graph, gap: [10, 10]});
```

Whenever the `graph` prop changes, `GraphLayout` automatically calls the protected `updateGraph(graph)` hook so subclasses can rebuild their internal caches.

## The Layout Lifecycle

For a graph layout, everything goes through a set of events. In each event, the layout will need to take the inputs and do the different computations. Lifecycle methods are various methods which are invoked at different phases of the lifecycle of a graph layout. If you are aware of these lifecycle events, it will enable you to control their entire flow and it will definitely help us to produce better results.
Expand Down Expand Up @@ -74,17 +84,14 @@ startDragging => lockNodePosition => release => unlockNodePosition => resume

### Update the graph data

GraphGL will call `initializeGraph` to pass the graph data into the layout.
If the graph is the same one but part ofthe data is changed, GraphGL will call `updateGraph` method to notify the layout.

In this case, we can just simply update the `this._nodePositionMap` by going through all nodes in the graph.
GraphGL supplies the current graph data through `setProps({graph})` before invoking `initializeGraph`. If you instantiate a layout yourself, follow the same pattern and call `layout.setProps({graph})` to notify the base class about the new data. Override the protected `updateGraph(graph)` hook to rebuild caches when the data changes.

```js
initializeGraph(graph) {
this.updateGraph(graph);
this.setProps({graph});
}

updateGraph(grpah) {
protected updateGraph(graph) {
this._graph = graph;
this._nodePositionMap = graph.getNodes().reduce((res, node) => {
res[node.getId()] = this._nodePositionMap[node.getId()] || [0, 0];
Expand Down Expand Up @@ -153,11 +160,6 @@ export default class RandomLayout extends GraphLayout {
super(options);
// give a name to this layout
this._name = 'RandomLayout';
// combine the default options with user input
this.props = {
...this.defaultProps,
...options
};
// a map to persis the position of nodes.
this._nodePositionMap = {};
}
Expand Down
6 changes: 4 additions & 2 deletions modules/graph-layers/src/core/graph-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,10 @@ export class GraphEngine extends EventTarget {

_updateLayout = () => {
log.log(0, 'GraphEngine: layout update');
this._layout.updateGraph(this._graph);
this._layout.update();
const shouldUpdate = this._layout.setProps({graph: this._graph});
if (shouldUpdate) {
this._layout.update();
}
this._layoutDirty = false;
};

Expand Down
125 changes: 120 additions & 5 deletions modules/graph-layers/src/core/graph-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ export type GraphLayoutEventDetail = {
bounds: Bounds2D | null;
};

export type GraphLayoutProps = {};
export type GraphLayoutProps = {
/**
* Graph data consumed by this layout instance.
*
* External callers should update the layout's graph by calling {@link setProps}
* with a new `graph` value instead of invoking {@link updateGraph} directly.
*/
graph?: LegacyGraph;
};

export type GraphLayoutDefaultProps<PropsT extends GraphLayoutProps> =
Omit<Required<PropsT>, 'graph'> & Pick<GraphLayoutProps, 'graph'>;

/** All the layout classes are extended from this base layout class. */
export abstract class GraphLayout<
Expand All @@ -26,7 +37,9 @@ export abstract class GraphLayout<
/** Name of the layout. */
protected readonly _name: string = 'GraphLayout';
/** Extra configuration props of the layout. */
protected props: Required<PropsT>;
protected props: GraphLayoutDefaultProps<PropsT>;
/** Baseline configuration that new props are merged against. */
private readonly _defaultProps: GraphLayoutDefaultProps<PropsT>;

/**
* Last computed layout bounds in local layout coordinates.
Expand All @@ -43,9 +56,46 @@ export abstract class GraphLayout<
* Constructor of GraphLayout
* @param props extra configuration props of the layout
*/
constructor(props: GraphLayoutProps, defaultProps?: Required<PropsT>) {
constructor(props: GraphLayoutProps, defaultProps?: GraphLayoutDefaultProps<PropsT>) {
super();
this.props = {...defaultProps, ...props};
this._defaultProps = defaultProps
? {...defaultProps}
: ({} as GraphLayoutDefaultProps<PropsT>);
this.props = {...this._defaultProps, ...props} as GraphLayoutDefaultProps<PropsT>;
}

setProps(partial: Partial<PropsT>): boolean {
if (!partial || Object.keys(partial).length === 0) {
return false;
}

const nextProps = {
...this._defaultProps,
...this.props,
...partial
} as GraphLayoutDefaultProps<PropsT>;
const validatedProps = this._validateProps(nextProps);
const previousProps = this.props;
const changedProps = this._getChangedProps(previousProps, validatedProps, partial);

if (!changedProps) {
return false;
}

this.props = validatedProps;

if ('graph' in changedProps) {
const graph = (changedProps as Partial<GraphLayoutProps>).graph;
if (graph) {
this.updateGraph(graph);
}
}

this._onPropsUpdated(previousProps, validatedProps, changedProps);

return 'graph' in changedProps
? true
: this._shouldRecomputeLayout(previousProps, validatedProps, changedProps);
}

/**
Expand Down Expand Up @@ -101,7 +151,7 @@ export abstract class GraphLayout<
/** first time to pass the graph data into this layout */
abstract initializeGraph(graph: LegacyGraph);
/** update the existing graph */
abstract updateGraph(graph: LegacyGraph);
protected abstract updateGraph(graph: LegacyGraph): void;
/** start the layout calculation */
abstract start();
/** update the layout calculation */
Expand All @@ -114,6 +164,71 @@ export abstract class GraphLayout<

// INTERNAL METHODS

/** Allow subclasses to coerce or validate the next props object. */
// eslint-disable-next-line class-methods-use-this
protected _validateProps(
nextProps: GraphLayoutDefaultProps<PropsT>
): GraphLayoutDefaultProps<PropsT> {
return nextProps;
}

/**
* Hook invoked after props are committed. Subclasses can perform additional
* bookkeeping or cache invalidation in response to the change.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
protected _onPropsUpdated(
previousProps: Readonly<GraphLayoutDefaultProps<PropsT>>,
nextProps: Readonly<GraphLayoutDefaultProps<PropsT>>,
changedProps: Partial<PropsT>
): void {}

/**
* Determine whether the layout should recompute after the supplied change.
*
* Subclasses can override to defer work if a prop change does not influence
* the resulting layout.
*/
protected _shouldRecomputeLayout(
previousProps: Readonly<GraphLayoutDefaultProps<PropsT>>,
nextProps: Readonly<GraphLayoutDefaultProps<PropsT>>,
changedProps: Partial<PropsT>
): boolean {
return Object.keys(changedProps).length > 0;
}

private _getChangedProps(
previousProps: Readonly<GraphLayoutDefaultProps<PropsT>>,
nextProps: Readonly<GraphLayoutDefaultProps<PropsT>>,
partial: Partial<PropsT>
): Partial<PropsT> | null {
const changedProps: Partial<PropsT> = {};
let changed = false;

type DefaultKey = keyof GraphLayoutDefaultProps<PropsT>;
type PropsKey = Extract<DefaultKey, keyof PropsT>;

for (const key of Object.keys(partial) as PropsKey[]) {
const defaultKey = key as DefaultKey;
const nextValue = nextProps[defaultKey];
if (key === 'graph') {
// Always treat graph updates as significant so layout caches refresh.
changedProps[key] = nextValue as PropsT[typeof key];
changed = true;
// eslint-disable-next-line no-continue
continue;
}

const previousValue = previousProps[defaultKey];
if (previousValue !== nextValue) {
changedProps[key] = nextValue as PropsT[typeof key];
changed = true;
}
}

return changed ? changedProps : null;
}

/** Hook for subclasses to update bounds prior to emitting events. */
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected _updateBounds(): void {}
Expand Down
2 changes: 1 addition & 1 deletion modules/graph-layers/src/core/graph-runtime-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface GraphRuntimeLayout extends EventTarget {
readonly version: number;
readonly state: GraphLayoutState;
initializeGraph(graph: Graph): void;
updateGraph(graph: Graph): void;
setProps(props: Record<string, unknown>): boolean;
start(): void;
update(): void;
resume(): void;
Expand Down
16 changes: 11 additions & 5 deletions modules/graph-layers/src/graph/legacy-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {warn} from '../utils/log';
import {Cache} from '../core/cache';
import {Edge} from './edge';
import {Node} from './node';
import {GraphLayout, type GraphLayoutState} from '../core/graph-layout';
import {GraphLayout, type GraphLayoutProps, type GraphLayoutState} from '../core/graph-layout';
import type {GraphRuntimeLayout} from '../core/graph-runtime-layout';
import type {EdgeInterface, Graph, NodeInterface} from './graph';

Expand Down Expand Up @@ -396,10 +396,6 @@ export class LegacyGraphLayoutAdapter extends EventTarget implements GraphRuntim
this.layout.initializeGraph(this._assertLegacyGraph(graph));
}

updateGraph(graph: Graph): void {
this.layout.updateGraph(this._assertLegacyGraph(graph));
}

start(): void {
this.layout.start();
}
Expand Down Expand Up @@ -443,6 +439,16 @@ export class LegacyGraphLayoutAdapter extends EventTarget implements GraphRuntim
this.eventForwarders.clear();
}

setProps(props: Record<string, unknown>): boolean {
if ('graph' in props && props.graph) {
return this.layout.setProps({
...(props as Partial<GraphLayoutProps>),
graph: this._assertLegacyGraph(props.graph as Graph)
});
}
return this.layout.setProps(props as Partial<GraphLayoutProps>);
}

private _assertLegacyGraph(graph: Graph): LegacyGraph {
if (graph instanceof LegacyGraph) {
return graph;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import type {LegacyGraph} from '../../graph/legacy-graph';
import type {NodeInterface, EdgeInterface} from '../../graph/graph';
import {GraphLayoutDefaultProps} from '../../core/graph-layout';
import {log} from '../../utils/log';

import {D3DagLayout, type D3DagLayoutProps} from './d3-dag-layout';
Expand All @@ -23,10 +24,10 @@ export type CollapsableD3DagLayoutProps = D3DagLayoutProps & {
}

export class CollapsableD3DagLayout extends D3DagLayout<CollapsableD3DagLayoutProps> {
static override defaultProps: Required<CollapsableD3DagLayoutProps> = {
static override defaultProps = {
...D3DagLayout.defaultProps,
collapseLinearChains: false
}
} as const satisfies GraphLayoutDefaultProps<CollapsableD3DagLayoutProps>;

private _chainDescriptors = new Map<string, CollapsedChainDescriptor>();
private _nodeToChainId = new Map<string | number, string>();
Expand All @@ -37,14 +38,15 @@ export class CollapsableD3DagLayout extends D3DagLayout<CollapsableD3DagLayoutPr
super(props, CollapsableD3DagLayout.defaultProps);
}

override setProps(props: Partial<CollapsableD3DagLayoutProps>): void {
super.setProps(props);
override setProps(props: Partial<CollapsableD3DagLayoutProps>): boolean {
const shouldUpdate = super.setProps(props);
if (props.collapseLinearChains !== undefined && this._graph) {
this._runLayout();
}
return shouldUpdate;
}

override updateGraph(graph: LegacyGraph): void {
protected override updateGraph(graph: LegacyGraph): void {
super.updateGraph(graph);
this._chainDescriptors.clear();
this._nodeToChainId.clear();
Expand Down
Loading
Loading