How to uniquely identify a binding in a Loop Node

In our diagram, we have 3 loop nodes nested like below
Loop Node 1
→ Loop Node 2
→ Loop Node 3
See the screenshot for the same.

We are reusing the same loop node shape for all the loop nodes. We have a binding ‘name’ on each GraphObject.
How can I uniquely identify the data binding of Loop Node 1.
I have done something like below
node = diagram.findPartForKey(“loopnode1key”)
node.findObject(“name”) // This is returning different objects every time, once it returns the loop node 1 object, sometimes it is returning loop node 3 object.

Looking for a solution, where I can get the GraphObject of given nodekey which has name binding.

Really? That would be very surprising. And it should be impossible for Panel.findObject to return an object that is in a different Part.

I assume you aren’t modifying the GraphObject.name property at any time or place once the Parts have been created by the Diagram. That could cause confusion.

I have assumed that Panel.findObject is returning a different object. Looks like it’s a different issue on my side.
Here is the issue which I’m facing
We have given the name binding to the orange circle shape which you can see in the top middle of each node. We are reusing the Shapes for all loop nodes. Our requirement is to scroll to the orange part of the specified given node.
Here I’m trying to scroll to the orange part of the second loop node (for loop (1)) and I see that centerRect with documentBounds is working strange

  1. None of the loop nodes are expanded → The GraphObject (orange part of the second loop node) is not in viewport → I’m doing a diagram.subGraphExapand(second node) then doing diagram.centerRect(obj.getDocumentBounds()) → centres the orange part of second loop node in viewport (WORKS)
  2. First loop node is expanded → The GraphObject(orange part of the second loop node) is not in viewport → I’m doing a diagram.subGraphExapand(second node) then I’m doing diagram.centerRect(obj.getDocumentBounds()) → centres the orange part of second loop node in viewport (WORKS)
  3. First, second and third loop node are expanded → The GraphObject(orange part of the second loop node) is not in viewport → then I’m doing diagram.centerRect(obj.getDocumentBounds()) → It’s not centring to the viewport ( NOT WORKING) // tried with obj.actualBounds as well, even though it is not working.

Here is my code

const part = diagram.findPartForKey(“second loop node key”);
const obj = part.findObject(“name”);

let containingGroupKey = null;
if(part.containingGroup) {
	const { data } = part.containingGroup;
	containingGroupKey = data.key;
	if(containingGroupKey) {
		 if(!part.isSubGraphExpanded) {
			diagram.commandHandler.expandSubGraph(
                            	diagram.findNodeForKey(containingGroupKey),
               		 );
		
               	        diagram.centerRect(obj.getDocumentBounds()); // works
		} else {
			diagram.centerRect(obj.getDocumentBounds()); // not working
		}
	}
}

First, could you try calling CommandHandler.scrollToPart with the group that you want to scroll to, even if it is not (yet) visible due to being inside collapsed subgraphs? That should cause the argument group to become visible and scrolled to be in the center of the viewport, if it’s allowed to scroll that far.

If that works, then it’s just a matter of scrolling a bit so that that orange circle is centered. I’m not sure exactly what’s the best of way of doing that, but I can look into the matter.

So I tried this way
diagram.commandHandler.scrollToPart(node);
// after scrolltopart doing the centerrect
//Just a hack instead of waiting for scroll animation complete added a set timeout
setTimeout(() => {
diagram.centerRect(obj.getDocumentBounds());
}, 1000);

  1. None of the loop nodes are expanded → The GraphObject (orange part of the second loop node) is not in viewport → I’m doing a scrolltopart of second node then doing diagram.centerRect(obj.getDocumentBounds()) → centres the orange part of second loop node in viewport (WORKS)
  2. First and second loop node are expanded → The GraphObject(orange part of the second loop node) is not in viewport → I’m doing a scrolltopart of second node then doing diagram.centerRect(obj.getDocumentBounds()) → Orange part is not visible in the viewport ( NOT WORKING)

The requirement is sometimes the loop node might be very big. When we do scroll to loop node using diagram.commandHandler.scrollToPart, it always brings the center of loop node to the viewport. The orange circle which is on the top might not be visible for big loop nodes. That’s why we want to scroll to specific part of a node.

Are you saying that scrollToPart-wait-centerRect is working for the case that the groups are collapsed beforehand, but does not work when the groups start off expanded?

Yes. It is not working for the case - when the groups are expanded already and then trying to do scrollToPart-wait-centerRect

  1. When the loop nodes are not expanded before, after doing the scrollToPart-wait-centerRect
  2. When the loop nodes are expanded already, then doing scrollToPart-wait-centerRect

You can see in the screenshot that for loop(1) node is in the center of the viewport. But I want the orange circle part to be visible.

Try this, either with Group.IsSubGraphExpanded true or false in the group template. Click the “Test” button to center the “SubGraphExpanderButton” that is at the top of each Group.

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

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

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      layout: $(go.GridLayout),
      "AnimationFinished": e => {
        if (myTarget && myTarget.isVisible()) {
          const focus = myTarget.findObject("FOCUS");
          if (focus) {
            const diag = e.diagram;
            const b = focus.getDocumentBounds();
            const v = diag.viewportBounds;
            new go.Animation()
                .add(diag, "position", diag.position,
                     new go.Point(b.centerX - v.width/2, b.centerY - v.height/2))
                .start();
          }
          myTarget = null;
        }
      },
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    $(go.Shape, { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.TextBlock, { margin: 8 },
      new go.Binding("text"))
  );

myDiagram.groupTemplate =
  $(go.Group, "Auto",
    //{ isSubGraphExpanded: false },
    new go.Binding("isSubGraphExpanded"),
    $(go.Shape, { fill: "rgba(0,0,0,0.1)", strokeWidth: 3 },
      new go.Binding("stroke", "color")),
    $(go.Placeholder, { padding: 100 }),
    $(go.TextBlock, { alignment: go.Spot.Bottom },
      new go.Binding("text", "", d => `${d.text} ${d.key}`)),
    $("SubGraphExpanderButton", { name: "FOCUS", alignment: go.Spot.Top })
  )

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, isGroup: true, text: "Alpha", color: "lightblue" },
  { key: 2, isGroup: true, text: "Beta", color: "orange", group: 1 },
  { key: 3, isGroup: true, text: "Gamma", color: "lightgreen", group: 2 },
  { key: 4, isGroup: true, text: "Delta", color: "pink", group: 3, isSubGraphExpanded: true },
  { key: 11, isGroup: true, text: "Alpha", color: "lightblue" },
  { key: 12, isGroup: true, text: "Beta", color: "orange", group: 11 },
  { key: 13, isGroup: true, text: "Gamma", color: "lightgreen", group: 12 },
  { key: 14, isGroup: true, text: "Delta", color: "pink", group: 13, isSubGraphExpanded: true },
  { key: 21, isGroup: true, text: "Alpha", color: "lightblue" },
  { key: 22, isGroup: true, text: "Beta", color: "orange", group: 21 },
  { key: 23, isGroup: true, text: "Gamma", color: "lightgreen", group: 22 },
  { key: 24, isGroup: true, text: "Delta", color: "pink", group: 23, isSubGraphExpanded: true },
  { key: 31, isGroup: true, text: "Alpha", color: "lightblue" },
  { key: 32, isGroup: true, text: "Beta", color: "orange", group: 31 },
  { key: 33, isGroup: true, text: "Gamma", color: "lightgreen", group: 32 },
  { key: 34, isGroup: true, text: "Delta", color: "pink", group: 33, isSubGraphExpanded: true },
  { key: 41, isGroup: true, text: "Alpha", color: "lightblue" },
  { key: 42, isGroup: true, text: "Beta", color: "orange", group: 41 },
  { key: 43, isGroup: true, text: "Gamma", color: "lightgreen", group: 42 },
  { key: 44, isGroup: true, text: "Delta", color: "pink", group: 43, isSubGraphExpanded: true },
  { key: 51, isGroup: true, text: "Alpha", color: "lightblue" },
  { key: 52, isGroup: true, text: "Beta", color: "orange", group: 51 },
  { key: 53, isGroup: true, text: "Gamma", color: "lightgreen", group: 52 },
  { key: 54, isGroup: true, text: "Delta", color: "pink", group: 53, isSubGraphExpanded: true },
]);

// also try loading with Group.isSubGraphExpanded: false
var myTarget = null;
document.getElementById("myTestButton").addEventListener("click", e => {
    const g = myDiagram.findNodeForKey(54);
    myTarget = g;
    myDiagram.commandHandler.scrollToPart(g);
  });
  </script>
</body>
</html>

Thanks Walter. This worked.

Just an another query, In the code you are using AnimationFinished to know if the scroll to part is completed.
I see in the documentation that AnimationFinished will be triggered for all default animation events. We are using subGraphExpanded and some other animations and we are getting the trigger inside animationFinished. How can we check that AnimationFinished was triggered for scrollToPart

I’m sorry, but I do not understand exactly what question you are asking. Are you not getting an “AnimationFinished” DiagramEvent after calling CommandHandler.scrollToPart?

To Check if the scrollToPart animation is finished, I have written a handler listening to AnimationFinished event. In the handler i have written below code

const name = myTarget.findObject("name");
          if (name) {
            const diag = e.diagram;
            const b = name.getDocumentBounds();
            const v = diag.viewportBounds;
            new go.Animation()
                .add(diag, "position", diag.position,
                     new go.Point(b.centerX - v.width/2, b.centerY - v.height/2))
                .start();
          }

This code is getting executed after scrollToPart animation was finished.
But In our code, we are using other diagram methods like subGraphExpanded and whenever subGraphExpanded is executed we are getting the trigger inside our handler. ( I think its because subGraphExpanded internally does a animation)

So, I wanted to check in my handler that the target is scrollToPart and then only execute the code.

The Group.subGraphExpanded event handler property is called in the property setter, which is well before any layout has happened, which in turn is well before any animation might happen.

Note also that that event handler is called for each Group that has its collapsed/expanded state changed, whereas the “SubGraphCollapsed” and “SubGraphExpanded” DiagramEvents happen just once per call to the respective command.

I hope this explanation helps you understand what’s going on.