Is it possible to make the connections coming from or going to the TopRight and BottomLeft ports rounded?

In our produce we allow to create links between corners.
However, in this case they look not consistent, they are not rounded as the rest of the connection.

Is there a way to fix it?

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>GoJS v3 - Connected Rectangular Nodes</title>
    <meta name="description" content="GoJS diagram with two rectangular nodes connected from top-right to bottom-left">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <!-- GoJS v3 from CDN -->
    <script src="https://unpkg.com/gojs@3/release/go.js"></script>
    
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f5f5f5;
        }
        
        #diagramDiv {
            width: 800px;
            height: 600px;
            background-color: white;
            border: 2px solid #ccc;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }
        
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 20px;
        }
        
        .info {
            background-color: #e7f3ff;
            border: 1px solid #b3d9ff;
            border-radius: 4px;
            padding: 10px;
            margin: 20px 0;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <h1>GoJS v3 - Flow Diagram Example</h1>
    
    <div class="info">
        <strong>Features:</strong>
        <ul>
            <li>Two rectangular nodes with custom ports</li>
            <li>Connection from top-right corner of Node 1 to bottom-left corner of Node 2</li>
            <li>Interactive diagram - you can drag nodes around</li>
            <li>Built with GoJS v3</li>
        </ul>
    </div>
    
    <!-- Diagram container -->
    <div id="diagramDiv"></div>
    
    <script>
        // Initialize the diagram
        const $ = go.GraphObject.make;
        const diagram = $(go.Diagram, "diagramDiv", {
            // Enable undo manager
            "undoManager.isEnabled": true,
            // Set initial content alignment
            initialContentAlignment: go.Spot.Center,
            // Disable default layout
            layout: $(go.Layout),
            // Background color
            "grid.visible": true,
            "grid.gridCellSize": new go.Size(10, 10)
        });

        // Define the node template with custom ports
        diagram.nodeTemplate = 
            $(go.Node, "Spot",
                {
                    locationSpot: go.Spot.Center,
                    selectionAdorned: true,
                    // Make nodes draggable
                    movable: true
                },
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
                
                // The main shape - rectangle
                $(go.Shape, "Rectangle",
                    {
                        fill: "lightblue",
                        stroke: "#4a90e2",
                        strokeWidth: 2,
                        width: 120,
                        height: 80
                    },
                    new go.Binding("fill", "color")
                ),
                
                // Text label
                $(go.TextBlock,
                    {
                        font: "bold 14px Arial",
                        stroke: "#333"
                    },
                    new go.Binding("text", "text")
                ),
                
                // Top-right port (for first node)
                $(go.Shape, "Circle",
                    {
                        width: 10,
                        height: 10,
                        fill: "red",
                        stroke: "darkred",
                        strokeWidth: 1,
                        alignment: go.Spot.TopRight,
                        alignmentFocus: go.Spot.Center,
                        portId: "topright",
                        fromSpot: go.Spot.TopRight,
                        fromLinkable: true,
                        cursor: "pointer"
                    },
                    new go.Binding("visible", "showTopRight")
                ),
                
                // Bottom-left port (for second node)
                $(go.Shape, "Circle",
                    {
                        width: 10,
                        height: 10,
                        fill: "green",
                        stroke: "darkgreen",
                        strokeWidth: 1,
                        alignment: go.Spot.BottomLeft,
                        alignmentFocus: go.Spot.Center,
                        portId: "bottomleft",
                        toSpot: go.Spot.BottomLeft,
                        toLinkable: true,
                        cursor: "pointer"
                    },
                    new go.Binding("visible", "showBottomLeft")
                )
            );

        // Define the link template
        diagram.linkTemplate = 
            $(go.Link,
                {
                    routing: go.Routing.AvoidsNodes,
                    curve: go.Curve.JumpOver,
                    corner: 5,
                    toShortLength: 4,
                    toEndSegmentLength: 30,
                    fromEndSegmentLength: 30,
                    relinkableFrom: true,
                    relinkableTo: true,
                    reshapable: true,
                    resegmentable: true
                },
                
                // Link shape
                $(go.Shape,
                    {
                        isPanelMain: true,
                        strokeWidth: 3,
                        stroke: "#4a90e2"
                    }
                ),
                
                // Arrowhead
                $(go.Shape,
                    {
                        toArrow: "Standard",
                        strokeWidth: 0,
                        fill: "#4a90e2",
                        scale: 1.5
                    }
                ),
                
                // Link label (optional)
                $(go.TextBlock,
                    {
                        textAlign: "center",
                        font: "10pt Arial",
                        stroke: "#333",
                        background: "white",
                        margin: 4
                    },
                    new go.Binding("text", "text")
                )
            );

        // Create the model data
        const model = new go.GraphLinksModel(
            [
                // Node data
                {
                    key: 1,
                    text: "Node 1\n(Source)",
                    color: "lightblue",
                    loc: "100 150",
                    showTopRight: true,
                    showBottomLeft: false
                },
                {
                    key: 2,
                    text: "Node 2\n(Target)",
                    color: "lightgreen",
                    loc: "350 300",
                    showTopRight: false,
                    showBottomLeft: true
                }
            ],
            [
                // Link data - connecting from topright port of node 1 to bottomleft port of node 2
                {
                    from: 1,
                    to: 2,
                    fromPort: "topright",
                    toPort: "bottomleft",
                    text: "Connection"
                }
            ]
        );
        
        // Configure the model to use port properties
        model.linkFromPortIdProperty = "fromPort";
        model.linkToPortIdProperty = "toPort";
        
        // Assign the model to the diagram
        diagram.model = model;

        // Add some interaction feedback
        diagram.addDiagramListener("ObjectSingleClicked", function(e) {
            const part = e.subject.part;
            if (part instanceof go.Node) {
                console.log("Clicked on node: " + part.data.text);
            } else if (part instanceof go.Link) {
                console.log("Clicked on link");
            }
        });

        // Log when the diagram is ready
        diagram.addDiagramListener("InitialLayoutCompleted", function(e) {
            console.log("GoJS v3 diagram initialized successfully!");
            console.log("Node 1 has a red port at top-right corner");
            console.log("Node 2 has a green port at bottom-left corner");
            console.log("They are connected as specified");
        });
    </script>
</body>
</html>

In version 3.1, which is in alpha test right now, the value of Link.corner is observed for angles other than 90 degrees.

@walter Thank you! I’m looking forward to the 3.1 version.

[EDIT: the link is now obsolete]

I’ve tried this with the latest 3.1 version, but it looks like the issue still persists.

GoJS v3 - Connected Rectangular Nodes
<!-- GoJS v3.1 from CDN -->
<script src="https://unpkg.com/[email protected]/release/go.js"></script>

<style>
    body {
        font-family: Arial, sans-serif;
        margin: 20px;
        background-color: #f5f5f5;
    }
    
    #diagramDiv {
        width: 800px;
        height: 600px;
        background-color: white;
        border: 2px solid #ccc;
        border-radius: 8px;
        box-shadow: 0 4px 8px rgba(0,0,0,0.1);
    }
    
    h1 {
        color: #333;
        text-align: center;
        margin-bottom: 20px;
    }
    
    .info {
        background-color: #e7f3ff;
        border: 1px solid #b3d9ff;
        border-radius: 4px;
        padding: 10px;
        margin: 20px 0;
        font-size: 14px;
    }
</style>

GoJS v3 - Flow Diagram Example

<div class="info">
    <strong>Features:</strong>
    <ul>
        <li>Two rectangular nodes with custom ports</li>
        <li>Connection from top-right corner of Node 1 to bottom-left corner of Node 2</li>
        <li>Interactive diagram - you can drag nodes around</li>
        <li>Built with GoJS v3</li>
    </ul>
</div>

<!-- Diagram container -->
<div id="diagramDiv"></div>

<script>
    // Initialize the diagram
    const $ = go.GraphObject.make;
    const diagram = $(go.Diagram, "diagramDiv", {
        // Enable undo manager
        "undoManager.isEnabled": true,
        // Set initial content alignment
        initialContentAlignment: go.Spot.Center,
        // Disable default layout
        layout: $(go.Layout),
        // Background color
        "grid.visible": true,
        "grid.gridCellSize": new go.Size(10, 10)
    });

    // Define the node template with custom ports
    diagram.nodeTemplate = 
        $(go.Node, "Spot",
            {
                locationSpot: go.Spot.Center,
                selectionAdorned: true,
                // Make nodes draggable
                movable: true
            },
            new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
            
            // The main shape - rectangle
            $(go.Shape, "Rectangle",
                {
                    fill: "lightblue",
                    stroke: "#4a90e2",
                    strokeWidth: 2,
                    width: 120,
                    height: 80
                },
                new go.Binding("fill", "color")
            ),
            
            // Text label
            $(go.TextBlock,
                {
                    font: "bold 14px Arial",
                    stroke: "#333"
                },
                new go.Binding("text", "text")
            ),
            
            // Top-right port (for first node)
            $(go.Shape, "Circle",
                {
                    width: 10,
                    height: 10,
                    fill: "red",
                    stroke: "darkred",
                    strokeWidth: 1,
                    alignment: go.Spot.TopRight,
                    alignmentFocus: go.Spot.Center,
                    portId: "topright",
                    fromSpot: go.Spot.TopRight,
                    fromLinkable: true,
                    cursor: "pointer"
                },
                new go.Binding("visible", "showTopRight")
            ),
            
            // Bottom-left port (for second node)
            $(go.Shape, "Circle",
                {
                    width: 10,
                    height: 10,
                    fill: "green",
                    stroke: "darkgreen",
                    strokeWidth: 1,
                    alignment: go.Spot.BottomLeft,
                    alignmentFocus: go.Spot.Center,
                    portId: "bottomleft",
                    toSpot: go.Spot.BottomLeft,
                    toLinkable: true,
                    cursor: "pointer"
                },
                new go.Binding("visible", "showBottomLeft")
            )
        );

    // Define the link template
    diagram.linkTemplate = 
        $(go.Link,
            {
                routing: go.Routing.AvoidsNodes,
                curve: go.Curve.JumpOver,
                corner: 5,
                toShortLength: 4,
                toEndSegmentLength: 30,
                fromEndSegmentLength: 30,
                relinkableFrom: true,
                relinkableTo: true,
                reshapable: true,
                resegmentable: true
            },
            
            // Link shape
            $(go.Shape,
                {
                    isPanelMain: true,
                    strokeWidth: 3,
                    stroke: "#4a90e2"
                }
            ),
            
            // Arrowhead
            $(go.Shape,
                {
                    toArrow: "Standard",
                    strokeWidth: 0,
                    fill: "#4a90e2",
                    scale: 1.5
                }
            ),
            
            // Link label (optional)
            $(go.TextBlock,
                {
                    textAlign: "center",
                    font: "10pt Arial",
                    stroke: "#333",
                    background: "white",
                    margin: 4
                },
                new go.Binding("text", "text")
            )
        );

    // Create the model data
    const model = new go.GraphLinksModel(
        [
            // Node data
            {
                key: 1,
                text: "Node 1\n(Source)",
                color: "lightblue",
                loc: "100 150",
                showTopRight: true,
                showBottomLeft: false
            },
            {
                key: 2,
                text: "Node 2\n(Target)",
                color: "lightgreen",
                loc: "350 300",
                showTopRight: false,
                showBottomLeft: true
            }
        ],
        [
            // Link data - connecting from topright port of node 1 to bottomleft port of node 2
            {
                from: 1,
                to: 2,
                fromPort: "topright",
                toPort: "bottomleft",
                text: "Connection"
            }
        ]
    );
    
    // Configure the model to use port properties
    model.linkFromPortIdProperty = "fromPort";
    model.linkToPortIdProperty = "toPort";
    
    // Assign the model to the diagram
    diagram.model = model;

    // Add some interaction feedback
    diagram.addDiagramListener("ObjectSingleClicked", function(e) {
        const part = e.subject.part;
        if (part instanceof go.Node) {
            console.log("Clicked on node: " + part.data.text);
        } else if (part instanceof go.Link) {
            console.log("Clicked on link");
        }
    });

    // Log when the diagram is ready
    diagram.addDiagramListener("InitialLayoutCompleted", function(e) {
        console.log("GoJS v3 diagram initialized successfully!");
        console.log("Node 1 has a red port at top-right corner");
        console.log("Node 2 has a green port at bottom-left corner");
        console.log("They are connected as specified");
    });
</script>
Preformatted text

Hmmm, you’re right – in that case the corner is not rounded. We’ll investigate. Thanks for reporting the problem.

If the routing isn’t orthogonal, I think the corners work as you would expect.

Is there any way we can address and resolve this issue?

The geometries that are automatically created for orthogonal links are generated in different code than that for non-orthogonal links. That code also has to deal with jump-overs. The regular end segments (i.e. the horizontal or vertical end segments) are ignored for jump-overs. I’m assuming that should continue to be the case for those diagonal end segments. But if you are expecting rounded corners extending into the end segments, the code gets more complicated, and the results might not look that good anyway, due to the short distances involved. On the other hand, in your case, you did make those end segments longer.

Try this code:

<!DOCTYPE html>
<html>
<head>
  <title>Rounded Corners at Port Corner Spots</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <!-- <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script> -->
  <script src="../out/go-dev.js"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      layout: new go.ForceDirectedLayout(),
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .bind("location", "loc", go.Point.parse)
    .add(
      new go.Shape({ fill: "white", portId: "", fromSpot: go.Spot.TopRight, toSpot: go.Spot.TopLeft })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

// Given an Array of Points, return a polygonal Geometry.
// The Array must have at least two Points; all Points must be real.
// If the corner argument is greater than 0.5, the corners will be rounded off by that radius.
function generatePolyline(arr, corner) {
  if (!Array.isArray(arr)) {
    throw new Error('not an Array: ' + arr);
  }
  const geo = new go.Geometry();
  const len = arr.length;
  if (len < 2) return geo;
  if (corner > 0.5) {
    // line
    const fig = new go.PathFigure(arr[0].x, arr[0].y, false, false);
    for (let i = 1; i < len-1; i++) {
      const ax = arr[i-1].x;
      const ay = arr[i-1].y;
      let bx = arr[i].x;
      let by = arr[i].y;
      while (i < len-1 && Math.abs(bx-ax) < 0.5 && Math.abs(by-ay) < 0.5) {
        i++;
        bx = arr[i].x;
        by = arr[i].y;
      }
      let cx = bx;
      let cy = by;
      if (i < len-1) {
        cx = arr[i+1].x;
        cy = arr[i+1].y;
        while (i < len-1 && Math.abs(cx-bx) < 0.5 && Math.abs(cy-by) < 0.5) {
          i++;
          cx = arr[i+1].x;
          cy = arr[i+1].y;
        }
      }
      const db = Math.sqrt((bx-ax)*(bx-ax) + (by-ay)*(by-ay));
      const dc = Math.sqrt((cx-bx)*(cx-bx) + (cy-by)*(cy-by));
      const r = Math.min(corner, Math.min(db/2, dc/2));
      if (r > 0.5) {  // draw straight line then curve
        const fb = 1 - (r / db);
        const bfx = ax + (bx-ax) * fb;
        const bfy = ay + (by-ay) * fb;
        const fc = r / dc;
        const cfx = bx + (cx-bx) * fc;
        const cfy = by + (cy-by) * fc;
        fig.add(
          new go.PathSegment(go.SegmentType.Line, bfx, bfy)
        );
        fig.add(
          new go.PathSegment(go.SegmentType.Bezier, cfx, cfy, bx, by, bx, by)
        );
      } else {  // don't bother with a curve
        fig.add(
          new go.PathSegment(go.SegmentType.Line, bx, by)
        );
      }
    }
    fig.add(new go.PathSegment(go.SegmentType.Line, arr[len-1].x, arr[len-1].y));
    geo.add(fig);
  } else {
    // no corners: simple polygon or polyline
    const fig = new go.PathFigure(arr[0].x, arr[0].y, false, false);
    for (let i = 1; i < len; i++) {
      fig.add(new go.PathSegment(go.SegmentType.Line, arr[i].x, arr[i].y));
    }
    geo.add(fig);
  }
  return geo;
};

class CorneredLink extends go.Link {
  // ??? doesn't handle JumpOver or JumpGap
  makeGeometry() {
    if (this.pointsCount < 2) return super.makeGeometry();
    if (this.computeCurve() === go.Link.Bezier) return super.makeGeometry();
    const b = this.routeBounds;
    const a = [];
    this.points.each(p => a.push(new go.Point(p.x - b.x, p.y - b.y)));
    const geo = generatePolyline(a, this.computeCorner());
    return geo;
  }
}  // end CorneredLink

myDiagram.linkTemplate =
  new CorneredLink({
      routing: go.Link.Orthogonal,
      corner: 20,
      fromEndSegmentLength: 30, toEndSegmentLength: 20
    })
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "OpenTriangle" })
    );

myDiagram.model = new go.GraphLinksModel(
[
  // { key: 1, text: "Alpha", color: "lightblue" },
  // { key: 2, text: "Beta", color: "orange" },
  { key: 3, text: "Gamma", color: "lightgreen", loc: "200 -70" },
  { key: 4, text: "Delta", color: "pink", loc: "0 0" }
],
[
  // { from: 1, to: 2 },
  // { from: 1, to: 3 },
  { from: 3, to: 4 }
]);
  </script>
</body>
</html>

@walter thank you for the code you’ve provided. It seems that what we need, however, we still need to try to apply it to out code base.

One thing I’ve noticed in some cases a link is not rounded still.

That’s odd. I’ll look into it.

The problem was that sometimes consecutive points of the route are at basically the same point.

I have updated (and simplified) the code in my earlier post.

1 Like

Thank you @walter.

I’ve played with the code you’ve provided. Currently I face the following issues:

I did not correctly ignore points that were almost at the same point. I have updated the generatePolyline code, above.

But now that I’m playing with it, there’s still a big problem with reshaping or resegmenting the link.

[EDIT] Ah, that’s because of a subtle bug combined with the way that the override was implemented. That’s also fixed in the upcoming 3.1.3 release.

Try using 3.1.3, which we have just released: GoJS 3.1.3 released

@walter I will try code you’ve provided as soon as we manage to upgrade to the 3.1.3 version