Strange port linking issue

I have been racking my brain over this problem and hope somebody can shed some light. (we are still in the evaluation stage, but close to final decision). I cannot seem to find anything in the samples or documentation to explain this behavior.

Here is what I am wanting it to do. Drag two objects with ports from the palette to the diagram layout. Create a link from the bottom of “Start” to the top of “Step”.

This works fine as long as the data looks like this
{ “class”: “go.GraphLinksModel”,
“linkFromPortIdProperty”: “fubar1”,
“linkToPortIdProperty”: “fubar2”,
“nodeDataArray”: [
],
“linkDataArray”: [
]}
`
Note the naming convention of “linkFromPortIdProperty” and “linkToPortIdProperty”. I can use any string value I want and it does not have to correlate to anything in the code.

Now, change that data by removing even one of those properties or even just setting one with an empty string:
{ "class": "go.GraphLinksModel", "linkFromPortIdProperty": "fubar1", "linkToPortIdProperty": "", "nodeDataArray": [ ], "linkDataArray": [ ]}

…and perform the very same steps of dragging and linking above, this happens with the link not ending at the top of “Step”, but snaps to the bottom of it.

Here is the javascript which I took from Flowchart with very slight modification:

var myDiagram;
var $GO = go.GraphObject.make; // for conciseness in defining templates

$(document).ready(function() {

myDiagram =
$GO(go.Diagram, “myDiagramDiv”,
{
initialContentAlignment: new go.Spot(0, 0, 0, -50),
allowDrop: true, // must be true to accept drops from the Palette
//“linkingTool.archetypeLinkData”: { category: “paletteIconLinks” }, //this is necessary for palette icons to use a different link group (arrows) when dragged from palette into main diagram
scrollsPageOnFocus: false,
“undoManager.isEnabled”: true
}
);

      var lightText = 'whitesmoke';
  
      myDiagram.nodeTemplateMap.add("",  // the default category
        $GO(go.Node, "Spot", nodeStyle(),
          // the main object is a Panel that surrounds a TextBlock with a rectangular Shape
          $GO(go.Panel, "Auto",
            $GO(go.Shape, "Rectangle",
              { fill: "#00A9C9", stroke: null },
              new go.Binding("figure", "figure")),
            $GO(go.TextBlock,
              {
                font: "bold 11pt Helvetica, Arial, sans-serif",
                stroke: lightText,
                margin: 8,
                maxSize: new go.Size(160, NaN),
                wrap: go.TextBlock.WrapFit,
                editable: true
              },
              new go.Binding("text").makeTwoWay())
          ),
          // four named ports, one on each side:
          makePort("T", go.Spot.Top, false, true),
          makePort("L", go.Spot.Left, true, true),
          makePort("R", go.Spot.Right, true, true),
          makePort("B", go.Spot.Bottom, true, false)
        ));
  
      myDiagram.nodeTemplateMap.add("Start",
        $GO(go.Node, "Spot", nodeStyle(),
          $GO(go.Panel, "Auto",
            $GO(go.Shape, "Circle",
              { minSize: new go.Size(40, 40), fill: "#79C900", stroke: null }),
            $GO(go.TextBlock, "Start",
              { font: "bold 11pt Helvetica, Arial, sans-serif", stroke: lightText },
              new go.Binding("text"))
          ),
          // three named ports, one on each side except the top, all output only:
          makePort("L", go.Spot.Left, true, false),
          makePort("R", go.Spot.Right, true, false),
          makePort("B", go.Spot.Bottom, true, false)
        ));
          
     	
      
  myDiagram.linkTemplate = 
  		$GO(go.Link,  // the whole link panel
  		        {
  		          routing: go.Link.AvoidsNodes,
  		          curve: go.Link.JumpOver,
  		          corner: 5, 
  		          toShortLength: 4,
  		          relinkableFrom: true,
  		          relinkableTo: true,
  		          reshapable: true,
  		          resegmentable: true,
  		          // mouse-overs subtly highlight links:
  		          mouseEnter: function(e, link) { link.findObject("HIGHLIGHT").stroke = "rgba(30,144,255,0.2)"; },
  		          mouseLeave: function(e, link) { link.findObject("HIGHLIGHT").stroke = "transparent"; }
  		        },
  		        $GO(go.Shape,  // the highlight shape, normally transparent
  		        		{isPanelMain: true, 
      		           	 strokeWidth: 8, 
      		           	 stroke: "transparent", 
      		           	 name: "HIGHLIGHT" 
  		        		}
  		        ),
  		        $GO(go.Shape,  // the link path shape
  		        		{isPanelMain: true, 
      		        	 stroke: "gray", 
      		        	 strokeWidth: 2 
  		        		}
  		        ),
  		        $GO(go.Shape,  // the arrowhead
  		        		{toArrow: "standard", 
  		        		 stroke: null, 
  		        		 fill: "gray"}
  		        ),
  		        $GO(go.Panel, "Auto",  // the link label, normally not visible
  		        		{visible: false, 
  		        		 name: "LABEL", 
  		        		 segmentIndex: 2, 
  		        		 segmentFraction: 0.5
  		        		 },
  		        		 new go.Binding("visible", "visible").makeTwoWay()
  				)
  		);

//Execute to load
load();

// initialize the Palette that is on the left side of the page
myPalette =
$GO(go.Palette, “myPaletteDiv”, // must name or refer to the DIV HTML element
{
scrollsPageOnFocus: false,
nodeTemplateMap: myDiagram.nodeTemplateMap, // share the templates used by myDiagram
model: new go.GraphLinksModel([ // specify the contents of the Palette
{ category: “Start”, text: “Start” },
{ text: “Step” }
])
});

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", "loc", 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) {
      port.stroke = (show ? "white" : null);
    });
}
  // 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 $GO(go.Shape, "Circle",
             {
                fill: "transparent",
                stroke: null,  // this is changed to "white" in the showPorts function
                desiredSize: new go.Size(8, 8),
                alignment: spot, alignmentFocus: spot,  // align the port on the main Shape
                portId: name,  // declare this object to be a "port"
                fromSpot: spot, toSpot: spot,  // declare where links may connect at this port
                fromLinkable: output, toLinkable: input,  // declare whether the user may draw links to/from here
                cursor: "pointer"  // show a different cursor to indicate potential link point
             });
  }


//********* LOAD*****************//
  function load() {
    	myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
	}

});

And the HTML:

<div style="width: 100%; display: flex; justify-content: space-between">
 	<div id="myPaletteDiv" style="width: 115px; height:620px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
 	<div id="myDiagramDiv" style="flex-grow: 1; height: 620px; border: solid 1px black"></div>
</div>

<textarea id="mySavedModel" style="width:100%;height:300px">{ "class": "go.GraphLinksModel",
 "linkFromPortIdProperty": "fubar1",
  "linkToPortIdProperty": "",
  "nodeDataArray": [
 ],
  "linkDataArray": [
 ]}
  </textarea>

That’s because when there is no information to identify which port a link should connect to, it will try to connect to the default port. If you haven’t identified an element in your node with the GraphObject.portId set to the default port identifier, the empty string, then it will connect with the whole Node.

This is discussed at GoJS Ports in Nodes-- Northwoods Software

In your case the natural connection point for links would be at the bottom of the “Step” node, because that’s closest to the source node. But you could override that by setting the toSpot. Or by specifying a particular port.

Thanks for the reply. I’m still confused though as I am trying to wrap my head around all things GoJS. :-) When you talk about overriding the behavior of it snapping to the closest node by setting toSpot, if you look at the code, that is being set in the function makePort along with an ID being set.

To clarify, this is not an action of an initial load of a diagram. This is entirely a user action. Here is the first step where you can see that it appears to link as intended while holding the mouse down:

Then when you release the mouse, it snaps to the closest node.

I don’t understand your instruction as it sounds more like for loading the diagram initially which as far as I can tell looks like I am doing in the code. That part I understand fairly well (at least I think so…lol). It’s even more confusing when specifying dummy values like “fubar” in those two properties which of course don’t correlate to anything in the code.

I know it may seem out of the norm to want the links to behave this way, but it is something we are trying to implement for a particular design we are going after. I just provided the basic code above to illustrate the problem in hopes of finding a better solution than setting dummy values for “linkFromPortIdProperty” and “linkToPortIdProperty” in the initial data load. Right now, that is where I am at based on my “padawan” level of mastering and understanding GoJS. :-)

The name specified by GraphLinksModel.linkToPortIdProperty doesn’t matter as long as there is no existing link data and the name is a legitimate property name so that the LinkingTool can save the port identifier on the link data object.

But clearly when that property name is the empty string there is no way to save the port identifier on the link data, so it does not. That means the link data cannot remember the port identifier, so the link assumes it should connect to the default port. That port is the one whose identifier is the default name, the empty string. And if there is no such element in the Node, it uses the whole Node. In your case that might happen to coincide with where there is a port, but that is purely a coincidence depending on the relative positions of tje nodes and the routing properties. And it is clearly not the port that the user tried to connect to, all because the model was unable to save the information.