memgraph/orb

Add new graph layout: tree

tonilastre opened this issue · 4 comments

It should be fairly simple to switch that using the Orb API.

I believe it's a good idea to create a layout interface with a single method that takes all nodes and returns newly calculated positions for them. These positions could then be applied to the simulator as fixed or sticky through the view, updating the rendered view accordingly.

Something like this implementation with an enum for available layouts, a described interface, and a generic layout implementation for parsing the input string is fairly straightforward to add new layouts and use them:

export enum layouts {
  DEFAULT = 'default',
  CIRCLE = 'circle',
  ...
}

export interface ILayout<N extends INodeBase, E extends IEdgeBase> {
  getPositions(nodes: INode<N, E>[], width: number, height: number): INodePosition[];
}

export class Layout<N extends INodeBase, E extends IEdgeBase> implements ILayout<N, E> {
  private readonly _layout: ILayout<N, E> | null;

  private layoutByLayoutName: Record<string, ILayout<N, E> | null> = {
    [layouts.DEFAULT]: null,
    [layouts.CIRCLE]: new CircleLayout(),
    ...
  };

  constructor(layoutName: string) {
    this._layout = this.layoutByLayoutName[layoutName];
  }

  getPositions(nodes: INode<N, E>[], width: number, height: number): INodePosition[] {
    return this._layout === null ? [] : this._layout.getPositions(nodes, width, height);
  }
}

It has to be integrated into orb view by setting fixed positions for all nodes on data setup or change like this:

if (this._settings.layout !== layouts.DEFAULT) {
  nodePositions = this._layout.getPositions(
    this._graph.getNodes(),
    this._renderer.width,
    this._renderer.height,
  );
}
this._simulator.setupData({ nodes: nodePositions, edges: edgePositions });

Where _layout is a private variable instantiated in the constructor:

this._layout = new Layout(this._settings.layout);

Example of a new layout:

export class CircleLayout<N extends INodeBase, E extends IEdgeBase> implements ILayout<N, E> {

  getPositions(nodes: INode<N, E>[], width: number, height: number): INodePosition[] {
    const nodePositions = /* calculate positions */
    return nodes.map((node, index) => {
      return nodePositions;
    });
  }

}

For now, it is just a proposal of a possible solution for integrating layouts into the orb and it requires a lot of debugging for different edge-cases (adding nodes when a layout is present, check for edges behaviour, adding physics etc.), so feel free to share any ideas and proposals :)

Would love a tree layout! As it is now, I have to integrate an entire other library just for a tree view of my graph - yuck :-( Any plans to add this in?