Broken port behavior

I used the flow chart example to first create a simple chart output without editing possibilities. The JSON is generated by the server and everything works fine.
After that, I tried to add some editing possibilities again, but failed at the ports so far. New links start with a wrong direction and go through the rectangle. Relinking existing links from the JSON source is not possible at all.
I checked the code a few times but cannot find the problem.

I use GoJS 1.6.7, Firefox 44 on Windows 10.

The first screenshot shows the generated graph. It looks exactly like I want it to:

In the second screenshot, I tried to add two links (top and bottom). The routing is just broken:

The third screenshot shows me trying to relink. I don’t get any other proposals than the whole rectangle. The ports are in the middle of all four sides:

The Code (I make some use of jQuery):
var make = go.GraphObject.make;

// Define a function for creating a "port" that is normally transparent.
// The "name" is used as the GraphObject.portId, the "spot" is used to control how links connect
// and where the port is positioned on the node, and the boolean "output" and "input" arguments
// control whether the user can draw links from or to the port.
function makePort(name, spot, output, input) {
    // the port is basically just a small circle that has a white stroke when it is made visible
    return make(go.Shape, "Circle",
        {
            fill: "transparent",
            stroke: null,
            strokeWidth: 1,
            desiredSize: new go.Size(8, 8),
            alignment: spot, alignmentFocus: spot,
            portId: name,
            fromSpot: spot, toSpot: spot,
            fromLinkable: output, toLinkable: input,
            cursor: "pointer"
        }
    );
}

// helper definitions for node templates
function nodeStyle() {
    return [
        // The Node.location comes from the "loc" property of the node data,
        // converted by the Point.parse static method.
        // If the Node.location is changed, it updates the "loc" property of the node data,
        // converting back using the Point.stringify static method.
        new go.Binding("location", "position", go.Point.parse).makeTwoWay(go.Point.stringify),
            {
                // the Node.location is at the center of each node
                locationSpot: go.Spot.Center,
                //isShadowed: true,
                //shadowColor: "#888",
                // handle mouse enter/leave events to show/hide the ports
                mouseEnter: function (e, obj) { showPorts(obj.part, true); },
                mouseLeave: function (e, obj) { showPorts(obj.part, false); }
            }
    ];
}

// Make all ports on a node visible when the mouse is over the node
function showPorts(node, show) {
    var diagram = node.diagram;
    if (!diagram || diagram.isReadOnly || !diagram.allowLink) return;
    node.ports.each(function(port) {
        if (show) {
            port.stroke = "#333333";
            port.fill = "#0078d0";
            port.mouseEnter = function (e, obj) {
                obj.fill = "#e00034";
            };
            port.mouseLeave = function (e, obj) {
                obj.fill = "#0078d0";
            };
        } else {
            port.stroke = null;
            port.fill = null;
            port.mouseEnter = null;
            port.mouseLeave = null;
        }
    });
}

ctx.createDiagram = function(el, graph) {
    var diagram = make(go.Diagram, el, {
        initialContentAlignment: go.Spot.Center,
        allowDrop: true,
        mouseWheelBehavior: go.ToolManager.WheelZoom,
        layout: new go.TreeLayout(),
        "undoManager.isEnabled": true
    });
    
    diagram.nodeTemplateMap.add("diamond",
        make(go.Node, "Spot", nodeStyle(),
            make(go.Panel, "Auto",
                make(go.Shape, "Diamond", {fill: "transparent", stroke: "black", strokeWidth: 2, desiredSize:  new go.Size(60, 60)})
            ),
            makePort("T", go.Spot.Top, true, true),
            makePort("L", go.Spot.Left, true, false),
            makePort("R", go.Spot.Right, false, true),
            makePort("B", go.Spot.Bottom, true, true)
        )
    );
    
    diagram.nodeTemplateMap.add("bar",
        make(go.Node, "Spot", nodeStyle(),
            make(go.Panel, "Auto",
                make(go.Shape, "Rectangle", {fill: "black", stroke: null, desiredSize:  new go.Size(15, 80)})
            ),
            makePort("L", go.Spot.Left, true, false),
            makePort("R", go.Spot.Right, false, true)
        )
    );
    
    diagram.nodeTemplateMap.add("activity",
        make(go.Node, "Spot", nodeStyle(),
            make(go.Panel, "Auto",
                make(go.Shape, "RoundedRectangle", {fill: "transparent", stroke: "black", strokeWidth: 2 }),
                make(go.Panel, "Vertical",
                    make(go.TextBlock,
                        {
                            font: "8pt Lato, sans-serif",
                            stroke: "black",
                            margin: 4,
                            maxSize: new go.Size(160, NaN),
                            wrap: go.TextBlock.WrapFit,
                            editable: false,
                            alignment: go.Spot.Left
                        }, new go.Binding("text", "role")
                    ),
                    make(go.TextBlock,
                        {
                            font: "12pt Lato, sans-serif",
                            stroke: "#0078d0",
                            margin: 4,
                            maxSize: new go.Size(160, NaN),
                            wrap: go.TextBlock.WrapFit,
                            editable: false,
                            alignment: go.Spot.Left,
                            click: function(e, target) {
                                var win = window.open("/testcases.html?activity=" + target.part.data.key, '_blank');
                                win.focus();
                            },
                            cursor: "pointer",
                                mouseEnter: function(e, target) {
                            target.stroke = "#e00034";
                            },
                            mouseLeave: function(e, target) {
                                target.stroke = "#0078d0";
                            }
                        }, new go.Binding("text", "name")
                    ), make(go.TextBlock,
                        {
                            font: "10pt Lato, sans-serif",
                            stroke: "black",
                            margin: 4,
                            maxSize: new go.Size(160, NaN),
                            wrap: go.TextBlock.WrapFit,
                            editable: false,
                            visible: false,
                            name: "description",
                        }, new go.Binding("text", "description", function(text) {
                            return $("<div>").html(text).text();
                        })
                    )
                )
            ),
            makePort("T", go.Spot.Top, true, true),
            makePort("L", go.Spot.Left, true, false),
            makePort("R", go.Spot.Right, false, true),
            makePort("B", go.Spot.Bottom, true, true)
        )
    );
    
    diagram.nodeTemplateMap.add("start",
        make(go.Node, "Spot", nodeStyle(),
            make(go.Shape, "Circle", {desiredSize: new go.Size(40, 40), fill: "black", stroke: null }),
            makePort("R", go.Spot.Right, true, false)
        )
    );
    
    diagram.nodeTemplateMap.add("end",
        make(go.Node, "Spot", nodeStyle(),
            make(go.Shape, "Circle", {desiredSize: new go.Size(40, 40), fill: "transparent", stroke: "black", strokeWidth: 2 }),
            make(go.Shape, "Circle", {desiredSize: new go.Size(32, 32), fill: "black", stroke: null }),
            makePort("L", go.Spot.Left, false, true)
        )
    );
    
    diagram.linkTemplate = make(go.Link, {
            routing: go.Link.AvoidsNodes,
            curve: go.Link.JumpOver,
            corner: 5,
            toShortLength: 4,
            relinkableFrom: true,
            relinkableTo: true,
            reshapable: true,
            resegmentable: true,
            mouseEnter: function(e, link) { link.findObject("HIGHLIGHT").stroke = "rgba(30,144,255,0.2)"; },
             mouseLeave: function(e, link) { link.findObject("HIGHLIGHT").stroke = "transparent"; }
        }, new go.Binding("points").makeTwoWay(),
        make(go.Shape, {isPanelMain: true, strokeWidth: 8, stroke: "transparent", name: "HIGHLIGHT"}),
        make(go.Shape, {isPanelMain: true, stroke: "black", strokeWidth: 2}),
        make(go.Shape, {toArrow: "standard", stroke: null, fill: "black"}),
        make(go.Panel, "Auto", {visible: false, name: "label", segmentIndex: 2, segmentFraction: 0.5},
            make(go.Shape, "RoundedRectangle", {fill: "#F8F8F8", stroke: null}),
            make(go.TextBlock,
                {
                    textAlign: "center",
                    font: "10pt Lato, sans-serif",
                    stroke: "black",
                    editable: false
                },
                new go.Binding("text", "text")
            )
        )
    );

    // temporary links used by LinkingTool and RelinkingTool are also orthogonal:
    diagram.toolManager.linkingTool.temporaryLink.routing = go.Link.Orthogonal;
    diagram.toolManager.relinkingTool.temporaryLink.routing = go.Link.Orthogonal;
    
    // Nur die Anfangsanimation soll aus
    diagram.animationManager.isEnabled = false;
    diagram.model = go.Model.fromJson(graph);
    diagram.animationManager.isEnabled = true;
    
    // Show non-empty labels
    var i = diagram.findLinksByExample({text: function(text) {
        return typeof text !== "undefined";
    }});
    while (i.next()) {
        var label = i.value.findObject("label");
        label.visible = true;
    }
    
    // Show non-empty descriptions
    var i = diagram.findNodesByExample({description: function(description) {
        return typeof description !== "undefined";
    }});
    while (i.next()) {
        var label = i.value.findObject("description");
        label.visible = true;
    }
};

The example graph:
{"nodeDataArray":[{"role":"Controller","name":"Report ETA","description":"<p>The controller reports the ETAs to every involved customers of the tour.</p>","category":"activity","key":257628},{"role":"Controller","name":"Plan route","description":"<p>The controller plans the route considering additional orders that can be combined with this one.</p>","category":"activity","key":257626},{"role":"Controller","name":"Replan open tour parts","category":"activity","key":257657},{"role":"Controller","name":"Receive order","category":"activity","key":257616},{"role":"Controller","name":"Check tour / stall fee","category":"activity","key":257664},{"role":"Controller","name":"Send tour","description":"<p>The controller sends the tour to the vehicle.</p>","category":"activity","key":257630},{"role":"Controller","name":"Approve tour for invoice processing","category":"activity","key":257668},{"role":"Driver","name":"Receive tour","category":"activity","key":257652},{"role":"Driver","name":"Drive tour","category":"activity","key":257654},{"role":"Controller","name":"Plan tour","description":"<p>The controller creates a tour containing the order and assigns it to a vehicle/driver.</p>","category":"activity","key":257621},{"category":"diamond","key":257660},{"category":"bar","key":258057},{"category":"bar","key":258061},{"category":"diamond","key":257624},{"category":"start","key":-1},{"category":"end","key":-2}],"linkDataArray":[{"from":257660,"to":257657,"text":"Disturbances occur"},{"from":257657,"to":257624},{"from":257654,"to":257660},{"from":257660,"to":257664,"text":"No disturbances occur"},{"from":257621,"to":258057},{"from":257664,"to":257668},{"from":257628,"to":258061},{"from":257626,"to":257621},{"from":258057,"to":257630},{"from":258057,"to":257628},{"from":257624,"to":257626},{"from":258061,"to":257654},{"from":257630,"to":257652},{"from":257616,"to":257624},{"from":257652,"to":258061},{"from":-1,"to":257616},{"from":257668,"to":-2}],"class":"go.GraphLinksModel","linkFromPortIdProperty":"fromPort","linkToPortIdProperty":"toPort"}

Can you please help me fix those two problems?

Thanks.

Your Diagram.layout is a TreeLayout whose basic direction is rightwards (angle: 0). That means by default it will make sure that links come out from ports going towards the right and go into ports coming from the left. I suspect that is the behavior that you are seeing.

I suggest that you allow the TreeLayout to use the fromSpot and toSpot that are assigned on each port. You can do that by setting TreeLayout.setsPortSpot and TreeLayout.setsChildPortSpot to false. TreeLayout | GoJS API

    var diagram = make(go.Diagram, el, {
        . . .
        layout: make(go.TreeLayout, { setsPortSpot: false, setsChildPortSpot: false }),
        . . .
    });

Thanks. That fixes one of my two problems.
Now I still cannot relink the generated links. But I can relink newly added links.
The difference of the two cases is, that I don’t generate the links with fromPort and toPort because I don’t want to decide which ports to use. I expect GoJS do decide that. What would be a solution for that, concidering that there might be links with and without fromPort/toPort?

And one aditional question: Why does new go.Binding(“points”).makeTwoWay() always create six points, even for very short straight lines?

Sorry, none of the Layouts that I know of (none of the predefined layouts and none of the extensions and none of the sample layouts) will modify the logical connections of the diagram. That means none of the layouts will reconnect a Link to change which port it is connecting with.

You do seem to be using ports (i.e., you have set GraphLinksModel.linkFromPortIdProperty and GraphLinksModel.linkToPortIdProperty to non-empty strings), so you need to make sure all of your link data have those properties set appropriately.

But you say, “I don’t know which ports I want the links to connect with”. In that case allow the layout and/or link routing to determine that, and do not use ports at all. Assume the whole node (or most of it, anyway) forms the one and only port that the node has, and do not set the fromSpot or toSpot on that port to be a specific spot. You could either leave it as Spot.None or use something like Spot.AllSides or Spot.NotLeftSide.

Then if you really want a link to come out from the top of a node, you can set Link.fromSpot to be Spot.Top.

But you’ll still want to use ports if you really want users to start drawing new links from that little area at the top of a node and implicitly mean that the link should always be connected with that port.

Orthogonal links always have 6 points because that was the easiest to implement that can cover all standard routing possibilities.

That’s what I want. Now I added fromPort and toPort to all generated links but I still cannot relink them. So there is either a bug in your code or a bug in my code because this does not look like normal behavior that can be useful in any case.

Can you relink Links in the FlowChart or Logic Circuit samples, both of which use distinct ports on nodes?

Check the GraphObject.fromLinkable and .toLinkable properties, and other properties whose names begin with “from” and “to”.

Also read GoJS Validation -- Northwoods Software.