Aligning link flexibly around a node

We currently have this shape

image

The highlighted blue dots are ports and some links are already connected to it.
The need here is to align the links on left/right side vertically center to the inner circle shape. similar to the below image

image

To address this, we have set portId: "" and positioned the ports around the inner circle shape rather than the node itself. However, another issue arises as the link at the bottom overlaps the text “End”.

chrome-capture-2024-1-7 (6)

The shape is draggable, and the links are connected flexibly to the shape, meaning no specific portId is provided to the link, allowing it to move freely around the node as needed. Considering these requirements, is there a way to vertically center align the links and not overlap the text at the bottom?

something like this:
image

Thanks.

What are your real requirements? One port or four? It seems you want just one, but I’m not sure.

Might there be multiple lines of text in that TextBlock?

We require 4 ports around the shape, with the links center aligned relative to the inner red circle in all four directions. In case of bottom position link should not overlap to the text below the shape.
Text is single-lined only, the same as in the images above.

Below is the high-level structure of the End shape.

$(
  go.Node,
  "Spot",
  {
    // ...
  },
  $(
    go.Panel,
    go.Panel.Vertical,
    $(
      go.Panel,
      go.Panel.Spot,
      // Outer Circle
      $(
        go.Shape,
        "Circle",
        {
          // ...
        }
      ),
      // Inner circle
      $(go.Shape, "Circle", {
         // ...,
      })
    ),
    $(go.TextBlock, {
      text: "End",
    })
  ),
  

  // Ports
  // iterating over 4 value for top, left, right and bottom
  $(
    go.Shape,
    "Circle",
    {
      alignment: go.Spot.Top, // | Left | Right | Bottom,
      portId: "T", // "L" | "R" | "B"
      fromLinkable: true,
      toLinkable: true,
    }
  )
);

You require those four blue circles around the circle. But my question is whether or not you need four separate ports. GoJS Ports in Nodes-- Northwoods Software The distinction is whether you need “physical” connection points for some links (whether for syntactical or semantic reasons doesn’t matter). But your GIF shows links connecting at whichever side is closest – implying that you only want one port.

Do those four circles have any use other than decoration?

The reason I ask is that the possible solutions are quite different depending on the needed behaviors.

In certain cases, we need to retain four ports instead of just one. For example, we define ‘fromPortId’ and ‘toPortId’ to specify the points where links connect to the node. However, in other cases, we leave these values empty to allow for unrestricted movement (in this scenario, the node itself serves as the port).

Please assume there is only one port for now.

Ah, OK.

For the bottom port you’ll want to set these properties, with values that you can experiment with:

{
  fromEndSegmentLength: 30, fromShortLength: 15,
  toEndSegmentLength: 30, toShortLength: 15
}

Handling the case where there’s just one port but having the connection point be farther away from the port in some but not all cases is more complicated. I’ll create a sample for you.

[EDIT] Actually, if you use this CustomLink, you won’t need the above property settings for bottom ports either, since that case is handled by the override of Link.getLinkPoint.

class CustomLink extends go.Link {
  getLinkPoint(node, port, spot, from, ortho, othernode, otherport) {
    const pt = super.getLinkPoint(node, port, spot, from, ortho, othernode, otherport);
    if (spot.equals(go.Spot.Bottom)) {
      return new go.Point(pt.x, pt.y + 15);
    } else if (spot.isNone()) {
      const dir = this.getLinkDirection(node, port, pt, spot, from, ortho, othernode, otherport);
      if (dir === 90) return new go.Point(pt.x, node.actualBounds.bottom);
    }
    return pt;
  }
}

Here’s the complete sample. It supports five ports on each node, and much like the Draggable Link sample, allows users to draw new links from or to either individual ports or to the whole default port (which in this case is not the whole node but just the bigger circular Shape).

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Vertical",
      {
        locationSpot: go.Spot.Center,
        locationObjectName: "ICON",
        selectionObjectName: "ICON",
        mouseEnter: (e, node) => showSmallPorts(node, true),
        mouseLeave: (e, node) => showSmallPorts(node, false),
      },
      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      $(go.Panel, "Spot",
        $(go.Shape, "Circle",
          {
            name: "ICON",
            width: 48, height: 48, fill: "transparent", stroke: "crimson", strokeWidth: 3,
            portId: "", fromLinkable: true, toLinkable: true, cursor: "pointer"
          }),
        $(go.Shape, "Circle", { width: 36, height: 36, fill: "pink", stroke: "pink" }),
        makePort('T', go.Spot.Top),
        makePort('L', go.Spot.Left),
        makePort('R', go.Spot.Right),
        makePort('B', go.Spot.Bottom)
      ),

      $(go.TextBlock,
        new go.Binding("text"))
    );

function makePort(name, spot) {
    const $ = go.GraphObject.make;
    // the port is basically just a small transparent square
    const port = $(go.Shape, "Circle", {
      fill: null, // not seen, by default; set by showSmallPorts, defined below
      strokeWidth: 0,
      desiredSize: new go.Size(10, 10),
      alignment: spot, // align the port on the main Shape
      //alignmentFocus: spot.opposite(), // just inside the Shape
      portId: name, // declare this object to be a "port"
      fromSpot: spot,
      toSpot: spot, // declare where links may connect at this port
      fromLinkable: name !== "T",  // declare which directions a link may be drawn
      toLinkable: name !== "B",
      cursor: "pointer" // show a different cursor to indicate potential link drawing
    });
    // If you don't use the CustomLink class, you'll need this for the bottom port
    // if (spot.equals(go.Spot.Bottom)) {
    //   port.fromEndSegmentLength = 30;
    //   port.fromShortLength = 15;
    //   port.toEndSegmentLength = 30;
    //   port.toShortLength = 15;
    // }
    return port;
  }

function showSmallPorts(node, show) {
  node.ports.each((port) => {
    // don't change the default port, which is the big shape
    if (port.portId === "") return;
    port.fill = show ? "dodgerblue" : null;
  });
}

class CustomLink extends go.Link {
  getLinkPoint(node, port, spot, from, ortho, othernode, otherport) {
    const pt = super.getLinkPoint(node, port, spot, from, ortho, othernode, otherport);
    if (spot.equals(go.Spot.Bottom)) {
      return new go.Point(pt.x, pt.y + 15);
    } else if (spot.isNone()) {
      const dir = this.getLinkDirection(node, port, pt, spot, from, ortho, othernode, otherport);
      if (dir === 90) return new go.Point(pt.x, node.actualBounds.bottom);
    }
    return pt;
  }
}

myDiagram.linkTemplate =
  $(CustomLink,
    { routing: go.Link.Orthogonal },
    $(go.Shape, { stroke: "#555", strokeWidth: 2 }),
    $(go.Shape, { toArrow: "Triangle", fill: "#555", strokeWidth: 0 })
  );

myDiagram.model = new go.GraphLinksModel({
  linkFromPortIdProperty: "fromPort",
  linkToPortIdProperty: "toPort",
  nodeDataArray: 
    [
      { key: 1, text: "Alpha", loc: "0 0" },
      { key: 2, text: "Beta", loc: "150 150" },
      { key: 3, text: "Gamma", loc: "200 -100" },
      { key: 4, text: "Delta", loc: "-150 50" },
    ],
  linkDataArray:
    [
      { from: 1, fromPort: "B", to: 2, toPort: "T" },
      { from: 3, to: 1 },
      { from: 1, fromPort: "L", to: 4, toPort: "R" },
    ]
});
  </script>
</body>
</html>
1 Like

Thank you for the solution @walter .