memgraph / orb

Graph visualization library
Apache License 2.0
349 stars 17 forks source link

Add new graph layout: tree #91

Open tonilastre opened 7 months ago

tonilastre commented 7 months ago

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

AlexIchenskiy commented 6 months ago

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 :)

josiahbryan commented 1 month ago

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?

tonilastre commented 2 weeks ago

We will add this after we release orb 1.0.0. It's almost ready, we need to finish up the documentation. The branch is almost ready: https://github.com/memgraph/orb/pull/47

By the way, which library did you use for the tree layout? It will help us compare the layout output when we do the tree layout. Are you happy with the external library position output that you use?

josiahbryan commented 2 weeks ago

@tonilastre I used vis-network - and yes, positioning output somewhat happy - still was playing with the spacing/padding between nodes, but mostly okay. My dataset is somewhat corrupted right, else I'd share a screenshot.

However, I can share this gist that shows my usage of it - https://gist.github.com/josiahbryan/3b43e9be764d545eb7483cfb2cc2525b - the goal of that component was to be hot-swapable with my similarly-named CustomOrbGraph widget which wraps Orb obviously, so it mainly does the data manipulation to massage the data I give usually give Orb into a format that viz-network handles, and update it when it changes.