Port Shifting without a moving Shape

Hi there!

I’d like to talk about a strange effect I’d like to get rid of, that is also in your “Port Shifting” example:

When you take a “not” element (the triangle shape) and shift its left port inside the shape (and back), the shape moves left and right.

I find it very irritating when a “node” (i.e. a node’s visible shape) changes its position because I dragged one of its ports.

Is it possible to prevent that by finding a clever node layout (e.g. with additional “wrappers” with padding or margin)?

Cheers,
Florian

That just depends on how you have defined the location property on your node template(s).

For example, in that latest/samples/PortShifting.html sample, add these two property settings to the nodeStyle function:

        locationSpot: go.Spot.Center,
        locationObjectName: 'NODESHAPE',

Basically you want to make sure that the relative movement of the ports does not change the location of the node, since the ports are aligned relative to that NODESHAPE.

Hi Walter… thank you for your response!
But I’m lost at transferring the “solution” to my setup…

I tried to reproduce a “minimized” version of my nodeTemplate and model, but unfortunately I’m too dumb to modify it in the right places to make the rectangle not jump around.

I’d really appreciate it if you could have a look at it:
Basically I have two separate layers in which I want to create ports from two itemArrays, and one layer also hosts a picture (a rectangle in the minimized version). And the picture jumps around when ports are dragged to the sides of the node… :(

<!DOCTYPE html>
<html>
<body>
<script src="https://unpkg.com/[email protected]/release/go.js"></script>
<div id="allSampleContent" class="p-4 w-full">
<script src="https://unpkg.com/[email protected]/dist/extensions/Figures.js"></script>
<script src="https://unpkg.com/[email protected]/dist/extensions/PortShiftingTool.js"></script>
<script id="code">
  function init() {
    myDiagram = new go.Diagram('myDiagramDiv', {
      'draggingTool.isGridSnapEnabled': true, // dragged nodes will snap to a grid of 10x10 cells
    });

    // install the PortShiftingTool as a "mouse move" tool
    myDiagram.toolManager.mouseMoveTools.insertAt(0, new PortShiftingTool());

    // creates relinkable Links that will avoid crossing Nodes when possible and will jump over other Links in their paths
    myDiagram.linkTemplate = new go.Link({
      routing: go.Routing.AvoidsNodes,
      curve: go.Curve.JumpOver,
      corner: 3,
      relinkableFrom: true,
      relinkableTo: true,
      selectionAdorned: false, // Links are not adorned when selected so that their color remains visible.
      shadowOffset: new go.Point(0, 0),
      shadowBlur: 5,
      shadowColor: 'blue'
    })
      .bindObject('isShadowed', 'isSelected')
      .add(
        new go.Shape({ name: 'SHAPE', strokeWidth: 2, stroke: "red" })
      );
	  
	const $ = go.GraphObject.make;

    // share the template map with the Palette
    myDiagram.nodeTemplate = $(
		go.Node, "Spot", { selectionAdorned: true, selectionObjectName: "SHAPE" },
		$(
			go.Panel, "Spot", { name: "SHAPE" },
			$(
				go.Panel, "Auto", { padding: 4 },
				$( go.Shape, "Rectangle", { desiredSize: new go.Size(100,100), fill: "yellow" } /*go.Picture, new go.Binding("source", "imageUrl")*/ )
			),
			new go.Binding("itemArray", "contactpoints"),
			{
			  itemTemplate: $(
				go.Panel, { fromLinkable: true, toLinkable: true, },
				new go.Binding("alignment", "", (obj) => new go.Spot(obj.x, obj.y)),
				new go.Binding("portId", "id"),
				$(go.Shape, "Rectangle", { width: 4, height: 4, stroke: "blue", strokeWidth: 2, fill: "blue", })
			  ),
			}
		),
		$(
			go.Panel, "Spot",
			$(
				go.Panel, "Auto", { padding: 4 },
				$(go.Panel, { desiredSize: new go.Size(100,100) } ),
			),
			new go.Binding("itemArray", "expansionpoints"),
			{
			  itemTemplate: $(
				go.Panel, { fromLinkable: true, toLinkable: true, },
				new go.Binding("alignment", "", (obj) => new go.Spot(obj.x, obj.y)),
				new go.Binding("portId", "id"),
				$(go.Shape, "Rectangle", { width: 4, height: 4, stroke: "green", strokeWidth: 2, fill: "green", })
			  )
			}
		)
	);
	
	const nodeDataArray = [
			{ 
				key: "1",
				contactpoints: [
					{ id: "1c1", x: 0.3, y: 0.3 },
					{ id: "1c2", x: 0.6, y: 0.3 }
				],
				expansionpoints: [
					{ id: "1e1", x: 0.3, y: 0.6 },
					{ id: "1e2", x: 0.6, y: 0.6 }
				]
			}
		];
	
	myDiagram.model = go.Model.fromJson(JSON.stringify(
		{
			nodeDataArray: nodeDataArray,
			linkDataArray: []
		}
	));

  }

  window.addEventListener('DOMContentLoaded', init);
</script>

<div id="sample">
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div id="myDiagramDiv" style="flex-grow: 1; height: 500px; border: solid 1px black"></div>
  </div>
</div>
</div>
</body>
</html>

Your node template appears to have this structure:

Node "Spot"
    Panel "Spot"  (#1, by default this is the "main" element of the Node)
        Panel "Auto"  (the main element of the inner "Spot" Panel #1)
            Shape
        ... ports ...
    Panel "Spot"  (#2, this is aligned relative to #1)
        Panel "Auto"  (the main element of the inner "Spot" Panel #2)
            Panel "Position"
        ... ports ...

I notice several potential problems with this structure.

  • Do you really want what’s aligned relative to the main element of the Node to itself be a “Spot” Panel, #2? Hmmm, maybe you do because you have two data Arrays that are sources for port elements. Still, this looks more complex and confusing than it needs to be.
  • “Spot” Panel #1 has only one element in it, an “Auto” Panel. This doesn’t make sense. Get rid of the “Auto” Panel, have the Shape be the main (i.e. first) element of “Spot” Panel #1, and the port elements would be in Panel #1 as elements aligned to the Shape.
  • Same criticism for “Spot” Panel #2 – there’s no reason for having an “Auto” Panel.
  • Except it’s worse, because the main element of the “Auto” Panel is a “Position” Panel that has nothing in it!

You’ll note that the Logic Circuit or Port Shifting sample’s node template is of the structure:

Node "Spot"
    Shape
    ... ports ...

Thank you !

With your suggestions I simplified my nodetemplate:

myDiagram.nodeTemplate = $(
		go.Node, "Spot", { selectionAdorned: true, locationSpot: go.Spot.Center, locationObjectName: "SHAPE" },
		$(
			go.Panel, "Auto", { padding: 3 },
			$( go.Shape, "Rectangle", { name: "SHAPE", desiredSize: new go.Size(100,100), fill: "yellow", stroke: null, strokeWidth: 0 } /*go.Picture, new go.Binding("source", "imageUrl")*/ ),
		),
		new go.Binding("itemArray", "contactpoints"),
		{
		  itemTemplate: $(
			go.Panel, { fromLinkable: true, toLinkable: true, },
			new go.Binding("alignment", "", (obj) => new go.Spot(obj.x, obj.y)),
			new go.Binding("portId", "id"),
			$(go.Shape, "Rectangle", { width: 4, height: 4, stroke: "blue", strokeWidth: 2, fill: "blue", })
		  ),
		},
	);

It’s almost as simple as the Port shifting sample, except for a padding panel around the rectangle (so that the ports can be moved outside of the rectangle).

The only question is now: How do I get a second layer of ports from another itemArray to behave exactly like the first layer? Is this even possible?

I played aroung with different wrapper Panels, and values for padding and desiredSize, but to no avail :(

		$(
			go.Panel, "Spot", { padding: 3, alignment: go.Spot.Center, alignmentFocus: go.Spot.Center },
			$(go.Panel, { desiredSize: new go.Size(104,104) } ),
			new go.Binding("itemArray", "expansionpoints"),
			{
			  itemTemplate: $(
				go.Panel, { fromLinkable: true, toLinkable: true, },
				new go.Binding("alignment", "", (obj) => new go.Spot(obj.x, obj.y)),
				new go.Binding("portId", "id"),
				$(go.Shape, "Rectangle", { width: 4, height: 4, stroke: "green", strokeWidth: 2, fill: "green", })
			  )
			}
		)

If there’s no way I have to rework my input model so all ports are of the same type and can fit in a single itemArray… not much of a hassle, really, but I thought using two separate itemArrays would be more ‘elegant’.

Each Panel can only have one Panel.itemArray, so if you really want two Arrays in your node data you will need to use two Panels.

If you combine the item data into one Array, you can have two templates in your Panel.itemTemplateMap, or (easier in your case) use data binding in a single Panel.itemTemplate to make the elements look different.

Could you please share with us a screenshot or sketch how you want the node to look with those one or two Arrays of ports? Maybe we can show you how to define your node template to handle two itemArrays/itemTemplates.

ah, cool… i didn’t know about the itemTemplateMap!
I already implemented everything using bindings, but maybe I’ll look into it in the future.

My experiments with two seperate Panels and itemArrays always made the 2nd panel hop around when dragging ports onto the edges. The ports from the 1st and 2nd array only aligned beautifully when both (or neither) had ports sitting on the same edges.

Yes, the issue is trying to have the main element of both “Spot” Panels refer to the same unchanging element or element area. I need to find some examples of how to do this…

<!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:500px"></div>
  Hold down the Shift key in order to shift the alignment of a port.
  <textarea id="mySavedModel" style="width:100%;height:300px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script src="https://unpkg.com/create-gojs-kit/dist/extensions/PortShiftingTool.js"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "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();
        }
      }
    });

// install the PortShiftingTool as a "mouse move" tool
myDiagram.toolManager.mouseMoveTools.insertAt(0, new PortShiftingTool());

const PortTemplate =
  new go.Panel({ fromLinkable: true, toLinkable: true, cursor: "pointer" })
    .bind("portId", "id")
    .bindTwoWay("alignment", "a", go.Spot.parse, go.Spot.stringify)
    .add(
      new go.Shape({ fill: "gray", width: 10, height: 10, strokeWidth: 0 })
    );

myDiagram.nodeTemplate =
  new go.Node("Spot", { locationObjectName: "BODY" })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      // the details of the "BODY" don't matter, but the size does matter
      // so that the two following panels can have the same size large
      // enough to cover where you want the ports to be on this "BODY" 
      new go.Panel("Auto", { name: "BODY", width: 80, height: 80 })
        .add(
          new go.Shape({ fill: "white" })
            .bind("fill", "color"),
          new go.TextBlock()
            .bind("text")
        ),
      new go.Panel("Spot", { itemTemplate: PortTemplate, alignmentFocusName: "INS" })
        .bind("itemArray", "ins")
        // the size of this Spot Panel's main element should be big enough to cover the area
        // about which the ports should be aligned
        .add(new go.Shape({ name: "INS", width: 90, height: 90, fill: null, strokeWidth: 0 })),
      new go.Panel("Spot", { itemTemplate: PortTemplate, alignmentFocusName: "OUTS" })
        .bind("itemArray", "outs")
        // the size of this Spot Panel's main element should be big enough to cover the area
        // about which the ports should be aligned
        .add(new go.Shape({ name: "OUTS", width: 90, height: 90, fill: null, strokeWidth: 0 }))
    );

myDiagram.linkTemplate =
  new go.Link({ fromSpot: go.Spot.Right, toSpot: go.Spot.Left })
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "Standard" })
    );

myDiagram.model = new go.GraphLinksModel({
  linkFromPortIdProperty: "fpid",
  linkToPortIdProperty: "tpid",
  nodeDataArray: [
    { key: 1, text: "Alpha", color: "lightblue", loc: "0 0", outs: [{ id: "y", a: "1 0.333"}, { id: "z", a: "1 0.667" }] },
    { key: 2, text: "Beta", color: "orange", loc: "200 0", ins: [{ id: "a", a: "0 0.5" }] },
    { key: 3, text: "Gamma", color: "lightgreen", loc: "120 110", ins: [{ id: "a", a: "0 0.25" }, { id: "b", a: "0 0.5" }, { id: "c", a: "0 0.75" }], outs: [{ id: "z", a: "1 0.5" }] },
    { key: 4, text: "Delta", color: "pink", loc: "250 150", ins: [{ id: "a", a: "0 0.333" }, { id: "b", a: "0 0.667" }], outs: [{ id: "z", a: "1 0.5" }] }
  ],
  linkDataArray: [
    { from: 1, fpid: "y", to: 2, tpid: "a" },
    { from: 1, fpid: "z", to: 3, tpid: "b" },
    { from: 3, fpid: "z", to: 4, tpid: "a" }
  ]
});
  </script>
</body>
</html>
1 Like

Brilliant !