Link breaks through group and his nodes

Hi, i have an issue with linked nodes in certain circumstances within my diagram.

I have a TableLayout that organize groups in a grid. Each group consists in an Header title and it is a “one column table” that collect below his nodes.
Each node is linked with other nodes. Of course.

The issue occurs when two nodes of the same group are linked together; they display like this:

As you can see, the right box is “splitting” from Group Header to Nodes list just to create a path for the link…

Is there a way to force another link path? Or keeping thight the distance betweend groups and nodes?
Thank you.

What routing do your Links use? How would you like those links connecting two nodes in the same group to be routed?

I’m using the AvoidNodes ruoting.
I would like to see those connections outside groups, no matter if they came up on top or bottom… just not in the middle of it :)

What’s interesting is that there is special effort being made to have AvoidsNodes routing of Links that are in Groups to ignore the Group that they are in. That is because those Groups are often intended to be avoided by links that do not belong to the group. Whereas links that are internal to the group should be able to be routed normally, as if the (avoidable) containing group were not there to affect the routing.

You could try setting Node | GoJS API to something like:

  { . . .,
    avoidableMargin: new go.Margin(30, 2, 2, 2),
    . . .

This is on your regular Node template(s), not on the Group. I’m not sure about the value you should use at the top margin, where I used 30 as a guess. Maybe it should be more, such as 40.

Thanks Walter, it worked properly!

I want to ask you one more thing… As you can see in the previous image, links that routes from a Group to the same Group are drawn too near the source/destination node side.

How can I define “margin” beside these nodes?
I tried with “from/toEndSegmentLength” but it doesn’t works.

Thank you

Yes, increasing the fromEndSegmentLength and toEndSegmentLength on the port element should work. You also might want to increase the Node.avoidableMargin.

Working with the endSegmentLength on the port element did the trick. Thanks!

Hi Walter, i’m facing a similar issue… When rendering a Group with several nodes, the “recursion lines” seems to break inside the group layout, and routing fromSpot -> toSpot upon nodes even if it’s explicitly setted on AvoidNodes.

This is the result:

Group Layout:

avoidable: true,
selectionAdornmentTemplate: $(go.Adornment, "Auto"),
layout: $(go.GridLayout, {
    wrappingColumn: 1,
    spacing: go.Size.parse("0 0") 
}),
margin: new go.Margin(0, 100, 30, 0),
layerName: "Background"

Node Layout:

fromSpot: go.Spot.RightCenter, 
toSpot: go.Spot.LeftCenter,
avoidableMargin: new go.Margin(30, 2, 2, 2),
cursor: "pointer",
layerName: "Background"

Link Layout:

{
    selectable: false,
    routing: go.Link.AvoidsNodes,
    fromSpot: go.Spot.Right,
    toSpot: go.Spot.Left,
    layerName: "Middle"
},
new go.Binding("fromEndSegmentLength", "", function (link) {
    if(link.fromGroup === link.toGroup) {
        return 20;
    }  else {
        return 40;
    }
}),
new go.Binding("toEndSegmentLength", "", function (link) {
    if(link.fromGroup === link.toGroup) {
        return 20;
    }  else {
        return 40;
    }
})

Is there a way to force these links to follow the “main route link”, or specify that nodes cannot be overlapped by links?

Thank you

Could you show how you want one or two of the links to be routed, using different colors?

Thanks for the summary properties. Unrelated point: if you don’t want selected nodes to have the standard blue rectangle Adornment, set Part.selectionAdorned to false, rather than using any empty selectionAdornmentTemplate.

Thank you Walter for the advice.

In a few nodes Group, the “recursion links” (nodes from/to the same Group) are correctly routed below the Group itself.

image

Each “recursion links” should be routed following the RED link below that particular Group, but as you can see in the previous image, it doesn’t happens.

It still isn’t clear to me for any one of those horizontal green lines that are being drawn through a node how that route is going.

Although the individual Nodes should be avoidable, as they are by default, the Group should not be avoidable.

Let me explain it in a better way. This is what I got:

On the left you can see the starting point… Few nodes linked together with a “clockwise route” and link overlapped on that main route.

When user click on “more” it will add new nodes to the group, as in the middle images.
In that case we got 2 erroneous situations:

  1. a link break through group and the first node
  2. a link break through adjacent nodes

Into the right image we can see in red the actual route of a “breaking link” (the arrow indicates the toSpot route).

I wanted every route follow the “main clockwise route” instead breaking inside (or upon) group/nodes.

I hope i’ve been clearer this time :)

OK, thanks for that. But I still do not understand why the red link is being routed differently than all of the other green links that you had in the left-most screenshot. Are the node and ports exactly the same?

My guess was that with AvoidsNodes routing the link was trying to find a clear route between the two nodes, and it was unsuccessful, resulting in the default routing that has the long horizontal segment at the mid point between the two nodes. And my guess was that it could not find a clear path because it was treating the Group as avoidable, That’s why I was suggesting that you set Group.avoidable to false.

But then it doesn’t make sense that the route that it finds doesn’t go up and around, since I would think for half of the links that would be shorter than going down and around.

The nodes config and layout is the same for everyone in the group.
The only difference is on links, in fact a recoursive link has a from/toEndLengthSegment setted to 20 instead 40.

Until it reachs ~40 nodes per group everything works fine; over the 40 nodes it breaks more and more…

I’ve setted Group.avoidable to false with no lucky.

Is it possible that the overlapping links became too big and it is trying to split it with different routing?

At this time I cannot think of a reason. Can we get access to a running sample, or can you provide us with enough code to be able to reproduce the problem?

Hi Walter, I was trying to reproduce the problem in a running sample and I found something weird
I’m using a Panel.Table to represent, for each nodes, textual information and 2 circular images.

Moving it to a running sample i removed these images and the unusual routing seems disappeared.

Could you please check the following node template and tell me if there is something wrong or that can be optimized?

$define("TableRowNode", function (args) {
      return $(go.Node, "Spot",
        {
            fromSpot: go.Spot.RightCenter, 
            toSpot: go.Spot.LeftCenter,
            avoidableMargin: new go.Margin(30, 2, 2, 2),
            cursor: "pointer",
            layerName: "Background",
            selectionAdorned: false,
            dragComputation: function (part) {
                return part.location;
            }
        },
        new go.Binding("row")
      );
});
//
$("TableRowNode",
    $(go.Panel, "Auto",
        $(go.Shape, "Rectangle",
            { fill: "#FFFFFF", stroke: '#CCCCCC', strokeWidth: 0.5 },
        ),
        $(go.Panel, "Horizontal", 
        {
            width: nodeWidth,
            height: nodeHeight,
            row: 0,
            column: 0
        },
        $(go.Panel, go.Panel.Table,
            $(go.RowColumnDefinition, { column: 0, width: nodeWidth - 60 }),
            $(go.RowColumnDefinition, { column: 1, width: 30 }),
            $(go.RowColumnDefinition, { column: 2, width: 30 }),
            $(go.TextBlock, 
                {
                    font: "bold 12px Sans-Serif",
                    row: 0,
                    column: 0,
                    margin: 5,
                    alignment: go.Spot.Left,
                    width: nodeWidth - 60,
                    maxLines: 1,
                    overflow: go.TextBlock.OverflowEllipsis,
                    stroke: "#323232"
                },
                new go.Binding("text", "text"),
            ),
            $(go.Picture, 
                {
                    row: 0,
                    column: 1,
                    margin: new go.Margin(10, 0, 10, 10),
                    source: "./assets/images/check-white.png",
                    width: 20,
                    height: 20,
                    visible: false
                },
                new go.Binding("visible", "controllo", function (s) {
                    return !!s;
                })
            ),
            $(go.Shape, "Circle", 
                {
                    width: 20,
                    height: 20,
                    margin: new go.Margin(10, 10, 10, 10),
                    strokeWidth: 0,
                    row: 0,
                    column: 2,
                    fill: 'gray',
                    visible: false
                },
                new go.Binding("visible", "manualita", function (s) {
                    return !!s;
                })
            ),
            $(go.Picture, 
                {
                    width: 15,
                    height: 15,
                    margin: new go.Margin(10, 10, 10, 12),
                    source: "./assets/images/hand-white.png",
                    row: 0,
                    column: 2,
                    visible: false
                },
                new go.Binding("visible", "manualita", function (m) {
                    return !!m;
                })
            )
        )
    )
)

PS. The last image is wrapped in a Circle type Shape.

Thank you.

I think it would help us if you could also provide your group template. It’s hard to make a reproducible example with just what you’ve provided thus far.

If you look at the structure (the visual tree) of what you have declared, you have:

Node, "Spot"
    Panel, "Auto"
        Shape
        Panel, "Horizontal"
            Panel, "Table"
                . . .

So it is both odd and inefficient to have all of those nested Panels for no good reason that I can tell.

A “Spot” Panel ought to have a main element and at least one element to position relative to the main element. But this “Spot” Panel only has one element, the main one, which is an “Auto” Panel.

A “Horizontal” Panel ought to have least two elements to position in a row. But this “Horizontal” Panel only has one element, the main one, which is a “Table” Panel.

The last Picture is not necessarily “wrapped” by the previous Shape. That’s where one uses an “Auto” Panel.

Thank you Walter, I’m trying to optimize that structure.

I’m using the TableLayout extension.
Here is the Group template:

$(go.Group, "Auto",
           {
              avoidable: true,
              selectionAdorned: false,
              layout: $(go.GridLayout, {
                  wrappingColumn: 1,
                  spacing: go.Size.parse("0 0") 
              }),
              margin: new go.Margin(0, 100, 30, 0),
              layerName: "Background"
          },
          new go.Binding("row"),
          new go.Binding("column", "col"),
          $(go.Shape, "Rectangle", { 
              fill: "#F2F2F2", stroke: "transparent", strokeWidth: 0.5,
              portId: "",
              fromSpot: go.Spot.RightCenter,
              toSpot: go.Spot.LeftCenter
          }),
          $(go.Panel, "Table",
              {
                  toolTip:
                      $(go.Adornment, "Auto",
                          $(go.Shape, { fill: "#323232", strokeWidth: 0 }),
                          $(go.TextBlock, { margin: 8, stroke: "#FFFFFF" },
                              new go.Binding("text"))
                      )  
              },
              $(go.Shape, "Rectangle",
                  {
                      stroke: "transparent",
                      strokeWidth: 3,
                      desiredSize: new go.Size(groupWidth, groupHeight)
                  },
                  new go.Binding("fill", "type", function (type) {
                      return type === 2
                          ? "#8BDBF7"
                          : type === 3
                              ? "#BEE5F2"
                              : type === 5 
                                  ? "#FEE6BD"
                                  : "#E3E3E3";
                  }),
                  new go.Binding("stroke", "", function (node) {
                      return node.active
                          ? "#418AA3"
                          : node.type === 2
                              ? "#8BDBF7"
                              : node.type === 3
                                  ? "#BEE5F2"
                                  : node.type === 5 
                                      ? "#FEE6BD"
                                      : "#E3E3E3";
                  })
              ),
          $(go.Panel, "Table",
              $(go.TextBlock, 
                  {
                      row: 0, column: 0, font: "bold 14px Sans-Serif",
                      textAlign: "left",
                      width: groupWidth - 44,
                      stroke: "#323232",
                      maxLines: 2,
                      overflow: go.TextBlock.OverflowEllipsis,
                      margin: 8
                  },
                  new go.Binding("text")
              ),
              $("Button", { row: 0, column: 1, margin: 5, cursor: "pointer" })
          ),
          $(go.Placeholder, 
              { row: 1, columnSpan: 2, padding: 0, margin: 0, alignment: go.Spot.TopLeft }
          )
      )
  );

Here’s what I tried:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Minimal GoJS Sample</title>
<meta name="description" content="An almost minimal diagram using a very simple node template and the default link template." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Copyright 1998-2019 by Northwoods Software Corporation. -->

<script src="go.js"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;
    var $define = go.GraphObject.defineBuilder;
    var nodeWidth = 250;
    var nodeHeight = 30;
    var groupWidth = 250;
    var groupHeight = 30;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          initialContentAlignment: go.Spot.Center,
          "undoManager.isEnabled": true
        }
      );

    $define("TableRowNode", function(args) {
      return $(go.Node, "Spot",
        {
          fromSpot: go.Spot.RightCenter,
          toSpot: go.Spot.LeftCenter,
          avoidableMargin: new go.Margin(30, 2, 2, 2),
          cursor: "pointer",
          layerName: "Background",
          selectionAdorned: false,
          dragComputation: function (part) {
              return part.location;
          }
        },
        new go.Binding("row")
      );
    });

    myDiagram.nodeTemplate =
      $("TableRowNode",
        $(go.Panel, "Auto",
          $(go.Shape, "Rectangle",
            { fill: "#FFFFFF", stroke: '#CCCCCC', strokeWidth: 0.5 },
          ),
          $(go.Panel, "Horizontal",
            {
                width: nodeWidth,
                height: nodeHeight,
                row: 0,
                column: 0
            },
            $(go.Panel, go.Panel.Table,
              $(go.RowColumnDefinition, { column: 0, width: nodeWidth - 60 }),
              $(go.RowColumnDefinition, { column: 1, width: 30 }),
              $(go.RowColumnDefinition, { column: 2, width: 30 }),
              $(go.TextBlock,
                {
                    font: "bold 12px Sans-Serif",
                    row: 0,
                    column: 0,
                    margin: 5,
                    alignment: go.Spot.Left,
                    width: nodeWidth - 60,
                    maxLines: 1,
                    overflow: go.TextBlock.OverflowEllipsis,
                    stroke: "#323232"
                },
                new go.Binding("text", "text"),
              ),
              $(go.Shape, "Circle",
                {
                    width: 20,
                    height: 20,
                    margin: new go.Margin(10, 0, 10, 10),
                    strokeWidth: 0,
                    row: 0,
                    column: 1,
                    fill: 'gray',
                    visible: false
                },
                new go.Binding("visible", "controllo", function (s) {
                    return !!s;
                })
              ),
              // $(go.Picture,
              //   {
              //       row: 0,
              //       column: 1,
              //       margin: new go.Margin(10, 0, 10, 10),
              //       source: "./assets/images/check-white.png",
              //       width: 20,
              //       height: 20,
              //       visible: false
              //   },
              //   new go.Binding("visible", "controllo", function (s) {
              //       return !!s;
              //   })
              // ),
              $(go.Shape, "Circle",
                {
                    width: 20,
                    height: 20,
                    margin: new go.Margin(10, 10, 10, 10),
                    strokeWidth: 0,
                    row: 0,
                    column: 2,
                    fill: 'gray',
                    visible: false
                },
                new go.Binding("visible", "manualita", function (s) {
                    return !!s;
                })
              )
              // $(go.Picture,
              //   {
              //       width: 15,
              //       height: 15,
              //       margin: new go.Margin(10, 10, 10, 12),
              //       source: "./assets/images/hand-white.png",
              //       row: 0,
              //       column: 2,
              //       visible: false
              //   },
              //   new go.Binding("visible", "manualita", function (m) {
              //       return !!m;
              //   })
              // )
            )
          )
        )
      );

    myDiagram.groupTemplate =
      $(go.Group, "Auto",
        {
          avoidable: false,
          computesBoundsIncludingLinks: false,
          selectionAdorned: false,
          layout: $(go.GridLayout, {
            wrappingColumn: 1,
            spacing: go.Size.parse("0 0")
          }),
          margin: new go.Margin(0, 100, 30, 0),
          layerName: "Background"
        },
        new go.Binding("row"),
        new go.Binding("column", "col"),
        $(go.Shape, "Rectangle",
          {
            fill: "#F2F2F2", stroke: "transparent", strokeWidth: 0.5,
            portId: "",
            fromSpot: go.Spot.RightCenter,
            toSpot: go.Spot.LeftCenter
          }
        ),
        $(go.Panel, "Table",
          {
            toolTip:
              $(go.Adornment, "Auto",
                $(go.Shape, { fill: "#323232", strokeWidth: 0 }),
                $(go.TextBlock,
                  { margin: 8, stroke: "#FFFFFF" },
                  new go.Binding("text")
                )
              )
          },
          $(go.Shape, "Rectangle",
            {
              stroke: "transparent",
              strokeWidth: 3,
              desiredSize: new go.Size(groupWidth, groupHeight)
            },
            new go.Binding("fill", "type", function (type) {
              return type === 2
                ? "#8BDBF7"
                  : type === 3
                      ? "#BEE5F2"
                      : type === 5
                          ? "#FEE6BD"
                          : "#E3E3E3";
            }),
            new go.Binding("stroke", "", function (node) {
              return node.active
                ? "#418AA3"
                : node.type === 2
                    ? "#8BDBF7"
                    : node.type === 3
                        ? "#BEE5F2"
                        : node.type === 5
                            ? "#FEE6BD"
                            : "#E3E3E3";
            })
          ),
          $(go.Panel, "Table",
            $(go.TextBlock,
              {
                row: 0, column: 0, font: "bold 14px Sans-Serif",
                textAlign: "left",
                width: groupWidth - 44,
                stroke: "#323232",
                maxLines: 2,
                overflow: go.TextBlock.OverflowEllipsis,
                margin: 8
              },
              new go.Binding("text")
            ),
            $("Button", { row: 0, column: 1, margin: 5, desiredSize: new go.Size(10, 10), cursor: "pointer" })
          ),
          $(go.Placeholder,
            { row: 1, columnSpan: 2, padding: 0, margin: 0, alignment: go.Spot.TopLeft }
          )
        )
      );



    // but use the default Link template, by not setting Diagram.linkTemplate
    myDiagram.linkTemplate =
      $(go.Link,
        {
          selectable: false,
          routing: go.Link.AvoidsNodes,
          fromSpot: go.Spot.Right,
          toSpot: go.Spot.Left
        },
        new go.Binding("fromEndSegmentLength", "", function (link) {
          if(link.fromGroup === link.toGroup) {
            return 20;
          }  else {
            return 40;
          }
        }),
        new go.Binding("toEndSegmentLength", "", function (link) {
          if(link.fromGroup === link.toGroup) {
            return 20;
          }  else {
            return 40;
          }
        }),
        $(go.Shape)
      );

    // create the model data that will be represented by Nodes and Links
    myDiagram.model = new go.GraphLinksModel(
    [
      { key: "Group1", text: "LIQ02 - Post-Processing_OUT", isGroup: true, type: 2 },
      { key: 0, text: "LEVEL1 testest", controllo: true, manualita: true, group: "Group1" },
      { key: 1, text: "text", group: "Group1" },
      { key: 2, text: "lightgreen", group: "Group1" },
      { key: 3, text: "pink", group: "Group1" },
      { key: 4, text: "LEVEL2 testtest", group: "Group1" },
      { key: 5, text: "text", group: "Group1" },
      { key: 6, text: "lightgreen", group: "Group1" },
      { key: 7, text: "pink", group: "Group1" },
      { key: 8, text: "LEVEL3 testtest", manualita: true, group: "Group1" },
      { key: 9, text: "text", group: "Group1" },
      { key: 10, text: "lightgreen", group: "Group1" },
      { key: 11, text: "pink", group: "Group1" },
      { key: 12, text: "LEVEL4 testtest", controllo: true, manualita: true, group: "Group1" },
      { key: 13, text: "text", group: "Group1" },
      { key: 14, text: "lightgreen", group: "Group1" },
      { key: 15, text: "pink", group: "Group1" }
    ],
    [
      { from: 0, to: 1, fromGroup: "Group1", toGroup: "Group1" },
      { from: 0, to: 2, fromGroup: "Group1", toGroup: "Group1" },
      { from: 1, to: 2, fromGroup: "Group1", toGroup: "Group1" },
      { from: 1, to: 5, fromGroup: "Group1", toGroup: "Group1" },
      { from: 4, to: 7, fromGroup: "Group1", toGroup: "Group1" },
      { from: 4, to: 9, fromGroup: "Group1", toGroup: "Group1" },
      { from: 5, to: 8, fromGroup: "Group1", toGroup: "Group1" },
      { from: 8, to: 11, fromGroup: "Group1", toGroup: "Group1" },
      { from: 12, to: 3, fromGroup: "Group1", toGroup: "Group1" },
      { from: 13, to: 0, fromGroup: "Group1", toGroup: "Group1" },
      { from: 15, to: 0, fromGroup: "Group1", toGroup: "Group1" },
    ]);
  }
</script>
</head>
<body onload="init()">
<div id="sample">
  <!-- The DIV for the Diagram needs an explicit size or else we won't see anything.
       This also adds a border to help see the edges of the viewport. -->
  <div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:600px"></div>
</div>
</body>
</html>

I commented out the images and set the group to avoidable: false, computesBoundsIncludingLinks: false. It produced this, with no links crossing the nodes: