GoJS layout options

Hi,

I am a newbie to GoJS and have a basic question regarding GoJS layout.

I have a simple graph. If I am using TreeLayout, the default display is as below

What I really want is the following: (by removing the layout option and specifying the locations of all the nodes)

I also tried LayeredDigraphLayout, but the display is not quite similar to the second display either.

My question is: can I use the provided GoJS layouts and their configurations/customizations to mimic the second display without the location binding.

I am posting my index.html (including the JavaScript code) below. You can tweak the LOC_BINDING and LAYOUT values to reproduce the three displays above.

Any guidance would be really appreciated!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="myDiagramDiv" style="width:1500px; height:800px; background-color: #dae4e4;"></div>
    <script src="../../release/go-debug.js"></script>    
    <script>
const $ = go.GraphObject.make;

const LAYOUT_TREE = "tree";
const LAYOUT_LAYERED = "layered";
const LAYOUT_NONE = "";

const LOC_BINDING = true;
const LAYOUT = LAYOUT_NONE;

let layout = {};
switch (LAYOUT) {
  case LAYOUT_TREE:
    layout = {
      layout: $(go.TreeLayout, {
        angle: 90,
        nodeSpacing: 50,
        layerSpacing: 50,
        setsChildPortSpot: false,
        setsPortSpot: false,
        alignment: go.TreeLayout.AlignmentStart,
      }),
    };
    break;
  case LAYOUT_LAYERED:
    layout = {
      layout: $(go.LayeredDigraphLayout, {
        direction: 90,
        layerSpacing: 50,
        columnSpacing: 50,
        setsPortSpots: false,
      }),
    };
    break;
}

const myDiagram = $(go.Diagram, "myDiagramDiv", layout);

const nodeTemplate = $(
  go.Node,
  "Position",
  $(
    go.Panel,
    "Auto",
    $(go.Shape, "Rectangle", {
      fill: "#ffffff",
      stroke: "#E4E6E7",
      width: 300,
      height: 50,
    }),
    $(go.TextBlock, new go.Binding("text", "text"))
  ),

  // ports
  $(go.Shape, "Circle", {
    width: 8,
    height: 8,
    fill: "#bebebe",
    stroke: null,
    position: new go.Point(146, 0),
    portId: "enter",
    toSpot: go.Spot.Top,
  }),
  $(go.Shape, "Circle", {
    width: 8,
    height: 8,
    fill: "#bebebe",
    stroke: null,
    position: new go.Point(146, 42),
    portId: "exit",
    fromSpot: go.Spot.Bottom,
  }),
  $(go.Shape, "Circle", {
    width: 8,
    height: 8,
    fill: "#bebebe",
    stroke: null,
    position: new go.Point(292, 10),
    portId: "second-enter",
    toSpot: go.Spot.Right,
  }),
  $(go.Shape, "Circle", {
    width: 8,
    height: 8,
    fill: "#bebebe",
    stroke: null,
    position: new go.Point(292, 32),
    portId: "second-exit",
    fromSpot: go.Spot.Right,
  })
);

if (LOC_BINDING) {
  nodeTemplate.bind(new go.Binding("location", "loc", go.Point.parse));
}

myDiagram.nodeTemplate = nodeTemplate;

myDiagram.linkTemplate = $(
  go.Link,
  { routing: go.Link.AvoidsNodes },
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

const nodeArray = [
  {
    key: "1",
    text: "Log",
    loc: "0 0",
  },
  {
    key: "2",
    text: "If",
    loc: "0 100",
  },
  {
    key: "3",
    text: "Else If",
    loc: "350 150",
  },
  {
    key: "4",
    text: "Else If 2",
    loc: "700 200",
  },
  {
    key: "5",
    text: "End",
    loc: "0 300",
  },
];

const linkArray = [
  {
    from: "1",
    to: "2",
    fromPort: "exit",
    toPort: "enter",
  },
  {
    from: "2",
    to: "5",
    fromPort: "exit",
    toPort: "enter",
  },
  {
    from: "2",
    to: "3",
    fromPort: "second-exit",
    toPort: "enter",
  },
  {
    from: "3",
    to: "4",
    fromPort: "second-exit",
    toPort: "enter",
  },
  {
    from: "3",
    to: "5",
    fromPort: "exit",
    toPort: "enter",
  },
  {
    from: "4",
    to: "5",
    fromPort: "exit",
    toPort: "enter",
  },
];

myDiagram.model = $(go.GraphLinksModel, {
  linkFromPortIdProperty: "fromPort",
  linkToPortIdProperty: "toPort",
  nodeDataArray: nodeArray,
  linkDataArray: linkArray,
});
    </script>
</body>
</html>

And how should nodes be laid out when they are connected with the top port on the right side of a node?

Hi Walter, thanks so much for your reply.

We are trying to do for now is to graph the if-else logic using GoJS. A simple example is illustrated below.

We have properly connected all the nodes. The only issue for now is to properly lay out the nodes. To answer your question, if node Y is on the right side of node X and connected from node X, node Y will be lower then node X and the link should come out from the right side of node X and go into the top side of node Y.

As you can see, we want the nodes are left-aligned based on its logic levels.

We can calculate the location of each node and use binding. But it will defeats the good features from GoJS layout. We are wondering if GoJS layout could provide some flexibility so that we could customize the layout calculation a bit but not the whole thing. Does it make sense?

Please let me know if I have answered your question.

So you don’t want there to be separate ports on the right side of each node. In fact there seems to be no reason to have multiple ports on a node at all, since you are assuming that for conditionals downwards is “false” and rightwards is “true”.

And I assume downwards is the default for non-conditionals.

OK, as soon as I get some free time I can look into an appropriate layout for you.

Hi Walter, actually we need one more port on the right-hand side for the loop. But the two ports on the right side are different since one is fromPort (false condition) and the other is toPort (loop back). Does the number of ports affect the node layout?

I include the loop display just for your reference.

Thank you so much for your help. Looking forward to hearing from your advice!

Have you seen Flowgrammer ?

Yes. I saw that. Two questions:

  1. I need the ParallelLayout extension to support it, right? I saw your reply in a different post saying the standard TreeLayout should work.
  2. How to make the node left-aligned?

I will investigate the code more. Thanks for the link!

One more thing: we want users to be able to delete nodes. in the flowgrammer example, user should be able to delete the if node and the whole if structure will be gone and action 1 will connect to action 5 directly. But in the explanation, deletion of such critical nodes are not possible if I understand correctly.

I read a bit about ParallelLayout, which needs a split node and a merge node and those two special nodes are not deletable. That may be a blocker.

Never mind. It says the group can be deleted. Let’s me play with the code more and let you know if I have more questions.

The Flowgrammer sample provides structured visual programming. If the graphs were unstructured, the user can easily get into a mess whose semantics might not be clear.

Hi Walter,

I tweaked the flowdiagrammer code and use my own data and style. However, the default behavior of the ParallelLayout is not ideal, as shown below.

The layout that I want is the following (Please ignore the edges. For now, I just concern about the node alignment):

At least, Log, If and its merge circle, and End should be vertically aligned. Moreover, the if and two Else If nodes should not be overlapped horizontally.

Is it doable by tweaking the layout? Looking forward to hearing from you!

Below is the HTML with JS code in it.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div
      id="myDiagramDiv"
      style="width: 1500px; height: 800px; background-color: #dae4e4"
    ></div>
    <script src="../../release/go-debug.js"></script>
    <script src="../../extensions/ParallelLayout.js"></script>
    <script>
      const $ = go.GraphObject.make;

      const parallelLayoutConfig = {
        angle: 90,
        layerSpacing: 50,
        nodeSpacing: 100,
        setsChildPortSpot: false,
        setsPortSpot: false,
      };

      const myDiagram = $(go.Diagram, "myDiagramDiv", {
        layout: $(ParallelLayout, parallelLayoutConfig),
      });

      const nodeTemplate = $(
        go.Node,
        "Position",
        $(
          go.Panel,
          "Auto",
          $(go.Shape, "Rectangle", {
            fill: "#ffffff",
            stroke: "#E4E6E7",
            width: 300,
            height: 50,
          }),
          $(go.TextBlock, new go.Binding("text", "text"))
        ),

        // ports
        $(go.Shape, "Circle", {
          width: 8,
          height: 8,
          fill: "#bebebe",
          stroke: null,
          position: new go.Point(146, 0),
          portId: "enter",
          toSpot: go.Spot.Top,
        }),
        $(go.Shape, "Circle", {
          width: 8,
          height: 8,
          fill: "#bebebe",
          stroke: null,
          position: new go.Point(146, 42),
          portId: "exit",
          fromSpot: go.Spot.Bottom,
        }),
        $(go.Shape, "Circle", {
          width: 8,
          height: 8,
          fill: "#bebebe",
          stroke: null,
          position: new go.Point(292, 10),
          portId: "second-enter",
          toSpot: go.Spot.Right,
        }),
        $(go.Shape, "Circle", {
          width: 8,
          height: 8,
          fill: "#bebebe",
          stroke: null,
          position: new go.Point(292, 32),
          portId: "second-exit",
          fromSpot: go.Spot.Right,
        })
      );

      myDiagram.nodeTemplateMap.add("Normal", nodeTemplate);
      myDiagram.nodeTemplateMap.add("Split", nodeTemplate);
      myDiagram.nodeTemplateMap.add("Start", nodeTemplate);
      myDiagram.nodeTemplateMap.add("End", nodeTemplate);
      myDiagram.nodeTemplateMap.add(
        "Merge",
        $(
          go.Node,
          {
            locationSpot: go.Spot.Center,
          },
          $(
            go.Shape,
            "Circle",
            { fill: "white" },
            { desiredSize: new go.Size(4, 4) }
          )
        )
      );

      myDiagram.linkTemplate = $(
        go.Link,
        { routing: go.Link.AvoidsNodes },
        $(go.Shape),
        $(go.Shape, { toArrow: "Standard" })
      );

      myDiagram.groupTemplate = $(
        go.Group,
        "Auto",
        {
          locationSpot: go.Spot.Center,
          layout: $(ParallelLayout, parallelLayoutConfig),
        },
        $(go.Placeholder)
      );
      const nodeArray = [
        { key: "1", text: "Log", category: "Start" },
        { key: "9", text: "End", category: "End" },
        {
          key: "grp-2",
          isGroup: true,
        },
        {
          key: "grp-3",
          isGroup: true,
          group: "grp-2",
        },
        {
          key: "grp-4",
          isGroup: true,
          group: "grp-3",
        },
        {
          key: "2",
          text: "If",
          category: "Split",
          group: "grp-2",
        },
        {
          key: "3",
          text: "Else If",
          category: "Split",
          group: "grp-3",
        },
        {
          key: "4",
          text: "Else If 2",
          category: "Split",
          group: "grp-4",
        },
        {
          key: "6",
          category: "Merge",
          group: "grp-2",
        },
        {
          key: "7",
          category: "Merge",
          group: "grp-3",
        },
        {
          key: "8",
          category: "Merge",
          group: "grp-4",
        },
      ];

      const linkArray = [
        {
          from: "1",
          to: "2",
          fromPort: "exit",
          toPort: "enter",
        },
        {
          from: "2",
          to: "6",
          fromPort: "exit",
        },
        {
          from: "2",
          to: "3",
          fromPort: "second-exit",
          toPort: "enter",
        },
        {
          from: "3",
          to: "4",
          fromPort: "second-exit",
          toPort: "enter",
        },
        {
          from: "3",
          to: "7",
          fromPort: "exit",
        },
        {
          from: "4",
          to: "8",
          fromPort: "exit",
        },
        {
          from: "4",
          to: "8",
          fromPort: "second-exit",
        },
        {
          from: "8",
          to: "7",
        },
        {
          from: "7",
          to: "6",
        },
        {
          from: "6",
          to: "9",
          toPort: "enter",
        },
      ];

      myDiagram.model = $(go.GraphLinksModel, {
        linkFromPortIdProperty: "fromPort",
        linkToPortIdProperty: "toPort",
        nodeDataArray: nodeArray,
        linkDataArray: linkArray,
      });
    </script>
  </body>
</html>

Thanks,
Min

Try setting TreeLayout.alignment to go.TreeLayout.AlignmentStart. This is what that other forum topic has done to line up the nodes vertically on the left side, assuming that the nodes are each the same width.

A better layout will take some thought and effort.

Hi Walter,

Since the layout configuration is used in both the top-level diagram and the group template, I tried all the combinations but neither provides good display.

When I added the alignment to both using the following JS object

const parallelLayoutConfig = {
        angle: 90,
        layerSpacing: 50,
        nodeSpacing: 100,
        setsChildPortSpot: false,
        setsPortSpot: false,
        alignment: go.TreeLayout.AlignmentStart,
      };

The display is below

When I applied alignment only to the top-level diagram but not in the group template, the display is below. This is the best but the End node is not left aligned and the three if-else nodes are overlapped horizontally.

When I applied alignment only to the group template but not the top-level diagram, the display is below

Your help is really appreciated!

Also, try setting Group.computesBoundsIncludesLinks to false.

You’re probably not interested in ProcessLayout: Process Layout

But you might be interested in BinaryTreeLayout:

Maybe I can adapt BinaryTreeLayout for your purposes.

Hi Walter, thank you so much for the links. Does BinaryTreeLayout support loop back link?

In the meantime, I am investigating the subclass of Layout, similar to parseTree Parse Tree

But if you could help me adapt the BinaryTreeLayout, that would be great!

Here you go: Flow Tree Layout 2

Also, if you are interested: Simple Flow Editor

Thank you so much, Walter! I will study both pages and let you know if I have any more questions.

Have a great weekend!

I have updated the Flow Tree Layout 2 sample to handle loop-backs.

image

But it’s still the case that it’s ambiguous in the example above, whether “Epsilon” should be under “Beta”, under “Gamma”, or under “Delta”, since all three of those nodes have direct links to “Epsilon”.

Hi Walter, Thank you so much for your help. I will follow your approach to override the doLayout method based on our specific layout requirements.

Hi Walter, overriding the doLayout method perfectly aligns the nodes based on the requirements. Thank you so much for your tremendous help!

But I have one more question related to the links. As you can see in the image below. The current link from node 6 (small label left to a node) to node 8 (pointed by a red arrow) is across another link from node 6 to node 7. I understand that GoJS tries to arrange the links with the shortest distance. But I prefer to have the link routed as the red line in the image.

I am wondering if there is any configuration to avoid cross-over links. If not, how can I add extra code in the doLayout method to specifying the route of links. I know that we can bind positions with links. But how to update the linkArray in doLayout? And is it the recommended way?

Thanks so much for your help again!