microsoft / msagljs

A JavaScript graph layout engine: port of MSAGL
https://microsoft.github.io/msagljs/
MIT License
146 stars 15 forks source link

Problems Using Edge Router Independently #92

Open wolfmcnally opened 1 month ago

wolfmcnally commented 1 month ago

I am using msagljs/core directly, and specifically the SplineRouter class. My nodes are being positioned by a graph layout algorithm not in msagljs, but that has other features I need.

I need to create a hierarchical layout, and then use SplineRouter to route the edges. But I am running into some difficulties.

First I'll show you what works.

I am using the following helper functions:

function setNodeCurve(node: GeomNode, x: number, y: number) {
    node.boundaryCurve = CurveFactory.mkRectangleWithRoundedCorners(30, 30, 3, 3, new Point(x, y));
}

function setCompoundBounds(graph: GeomGraph, size: Size, center: Point) {
    graph.boundingBox = Rectangle.mkSizeCenter(size, center)
    setCornerRadius(graph, 0);
}

function setCornerRadius(graph: GeomGraph, radius: number) {
    const bounds = graph.boundingBox;
    const rrect = new RRect({left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom, radX: radius, radY: radius});
    (graph as any).rrect = rrect;
}

The test below produces this hierarchical layout:

image

Questions continue below code.

test('hierarchy test 1', () => {
    const gg = new GeomGraph(new Graph('graph'));
    gg.labelSize = new Size(0, 0);

    const gg_clusterA = new GeomGraph(new Graph('clusterA'));
    gg.addNode(gg_clusterA);
    setCompoundBounds(gg_clusterA, new Size(200, 100), new Point(300, 91))

    const gg_clusterB = new GeomGraph(new Graph('clusterB'));
    gg.addNode(gg_clusterB);
    setCompoundBounds(gg_clusterB, new Size(180, 210), new Point(70, 91))

    const gg_clusterC = new GeomGraph(new Graph('clusterC'));
    gg.addNode(gg_clusterC); /// ?
    gg_clusterB.addNode(gg_clusterC);
    setCompoundBounds(gg_clusterC, new Size(120, 80), new Point(88, 150))

    const gn_a1 = new GeomNode(new Node('a1'));
    gg.addNode(gn_a1); /// ?
    gg_clusterA.addNode(gn_a1);
    setNodeCurve(gn_a1, 230, 95)

    const gn_a2 = new GeomNode(new Node('a2'));
    gg.addNode(gn_a2); /// ?
    gg_clusterA.addNode(gn_a2);
    setNodeCurve(gn_a2, 280, 70)

    const gn_a3 = new GeomNode(new Node('a3'));
    gg.addNode(gn_a3); /// ?
    gg_clusterA.addNode(gn_a3);
    setNodeCurve(gn_a3, 330, 90)

    const gn_b1 = new GeomNode(new Node('b1'));
    gg.addNode(gn_b1); /// ?
    gg_clusterB.addNode(gn_b1);
    setNodeCurve(gn_b1, 82, 65)

    const gn_b2 = new GeomNode(new Node('b2'));
    gg.addNode(gn_b2); /// ?
    gg_clusterB.addNode(gn_b2);
    setNodeCurve(gn_b2, 95, 10)

    const gn_b3 = new GeomNode(new Node('b3'));
    gg.addNode(gn_b3); /// ?
    gg_clusterB.addNode(gn_b3);
    setNodeCurve(gn_b3, 15, 71)

    const gn_c1 = new GeomNode(new Node('c1'));
    gg.addNode(gn_c1); /// ?
    gg_clusterB.addNode(gn_c1); /// ?
    gg_clusterC.addNode(gn_c1);
    setNodeCurve(gn_c1, 115, 140)

    const gn_c2 = new GeomNode(new Node('c2'));
    gg.addNode(gn_c2); /// ?
    gg_clusterB.addNode(gn_c2); /// ?
    gg_clusterC.addNode(gn_c2);
    setNodeCurve(gn_c2, 60, 140)

    gg.setEdge('a1', 'b2');
    gg.setEdge('c2', 'a3');
    gg.setEdge('b3', 'a3');
    gg.setEdge('b3', 'b2');
    gg.setEdge('b1', 'a3');
    gg.setEdge('b2', 'c1');
    gg.setEdge('c2', 'b2');
    gg.setEdge('c1', 'a2');

    const sr = SplineRouter.mk4(gg, 2, 4, Math.PI / 6);
    sr.run();

    SvgDebugWriter.writeGeomGraph('./tmp/hierarchy_test_1.svg', gg);
})

Question 1: The lines marked /// ? which add a node not just to its parent, but each of its grandparents seem necessary: if I remove these lines the edge layout silently fails. But I don't know whether it is truly necessary for some reason or whether I'm doing it wrong. I also appear to need to add them from the root graph down; reversing the order results in a runtime exception.

Question 2: I don't understand how to get the routed edges to fully avoid nodes. I've attempted to play with various settings, which either appear to be ignored or cause a failure. Please explain how I can put a margin around the nodes that edges will not route through unless they are actually terminating at a given node.

Question 3: I don't understand how to get bundled edge routing to work. Again, I've examined the test code, but I can't seem to get it to work, and I could really use an example.

Question 4: Once I do have bundled routing working, I'd like to know how to set the minimum separation distance for edges within bundles.

I feel like I must be doing something fundamentally wrong here, but I have no idea what, and I haven't been able to figure it out by looking at the test code, which seems to focus on the automated construction of clustered layouts from data files, and does the layout, while I'm looking to hand off just the edge routing job to SplineRouter.

Thanks for your help.

levnach commented 1 month ago

Here is the fixed test with some comments. A couple of points to make here are that Graph contains Graphs and Nodes, and Edge is just a pair of Nodes. GeomGraph, GeomNode, GeomEdge are attributes on the corresponding Graph, Node, Edge objects.

 test('hierarchy` test 1', () => {
  // GeomGraph is an attribute of Graph
  const root_graph = new Graph('graph')
  const gg = new GeomGraph(root_graph);
  gg.labelSize = new Size(0, 0);

  // clusterA is an attribute on "new Graph('clusterA')"
  const clusterA = new GeomGraph(new Graph('clusterA'));  
  gg.addNode(clusterA); //  under the hood it adds clusterA.graph to root_graph 
  setCompoundBounds(clusterA, new Size(200, 100), new Point(300, 91))

  const clusterB = new GeomGraph(new Graph('clusterB'));
  gg.addNode(clusterB);
  setCompoundBounds(clusterB, new Size(180, 210), new Point(70, 91))

  const clusterC = new GeomGraph(new Graph('clusterC'));
  clusterB.addNode(clusterC);
  setCompoundBounds(clusterC, new Size(120, 80), new Point(88, 150))
  // GeomNode is an attribute on Node
  const a1 = new GeomNode(new Node('a1'));
  clusterA.addNode(a1); // under the hood it adds node a1.node:Node to clusterA.graph:Graph
  setNodeCurve(a1, 230, 95)

  const a2 = new GeomNode(new Node('a2'));
  clusterA.addNode(a2);
  setNodeCurve(a2, 280, 70)

  const a3 = new GeomNode(new Node('a3'));
  clusterA.addNode(a3);
  setNodeCurve(a3, 330, 90)

  const b1 = new GeomNode(new Node('b1'));
  clusterB.addNode(b1);
  setNodeCurve(b1, 82, 65)

  const b2 = new GeomNode(new Node('b2'));
  clusterB.addNode(b2);
  setNodeCurve(b2, 95, 10)

  const b3 = new GeomNode(new Node('b3'));
  clusterB.addNode(b3);

  setNodeCurve(b3, 15, 71)

  const c1 = new GeomNode(new Node('c1'));
  clusterC.addNode(c1);
  setNodeCurve(c1, 115, 140)

  const c2 = new GeomNode(new Node('c2'));
  clusterC.addNode(c2);
  setNodeCurve(c2, 60, 140)

  function mke(u:GeomNode, v:GeomNode) {
    const uv = new Edge(u.node, v.node) // create edge u->v
    new GeomEdge(uv)   // add a geometry attribute to it
  }

  mke(a1,b1) 
  mke(c2, a3)
  mke(b3, a3);  
  mke(b3, b2);
  mke(b1, a3);
  mke(b2, c1);
  mke(c2, b2);
  mke(c1, a2);

  const sr = SplineRouter.mk4(gg, 2, 4, Math.PI / 6);
  sr.run();

  SvgDebugWriter.writeGeomGraph('./tmp/hierarchy_test_1.svg', gg);
})

Q2 I will be looking at why the routed edges go over the nodes, but not immediately. Q3 Have you looked an bundleRouter.spec.ts? It has some examples how to create the routing with bundles. Q4 BundlingSettings.edgeSeparation controls the separation between edges. This parameter is ignored when there is no room to route a wide bundle.

levnach commented 3 weeks ago

I think I fixed it with the last commit. I also published the new npm packages.