Freeze selected node when expanding

I have a use case that user need to expand and explore graph from selected node. Consider following example.

when click on expand from “Train Section” node graph is redrawn as below.

As you can see initial position of “Train Section” is change now. It would be better user experience if the user can first freeze the target node and then explore it. Is possible to freeze/unfreeze a selected node into a position?

I assume you mean “Track Section”, not “Train”.

If you are using ForceDirectedLayout, you can override ForceDirectedLayout.isFixed to return true for the ForceDirectedVertex corresponding to that Node. An example is in the InteractiveForce sample:

  // This variation on ForceDirectedLayout does not move any selected Nodes
  // but does move all other nodes (vertexes).
  class ContinuousForceDirectedLayout extends go.ForceDirectedLayout {
    isFixed(v) {
      return v.node.isSelected;
    }

However you may want a different predicate method – I do not know how you want to decide which nodes should stay “fixed” in location.

Thanks for your reply.

Apologies it’s “Track Section”.

The logic is to decide which node select is the lock adornment button click.

Lock button is a toggle button and once click on the lock button node should be freeze until unlock it by clicking the same lock button again.

I am using ForceDirectedLayout but in my case nodes are not predefined to use in override isFixed() method.

OK, so that button is toggling some state, probably a property on the node data object in the model. So your override of ForceDirectedLayout.isFixed should check that state.

Hi Walter,

I overrided ForceDirectedLayout.isFixed by hardcoding to “Track Section” as below

I can see “track section frozen” in console logs but as soon as I expand graph again “Track Section” location is moved. Not sure I missed anything else.

Did you use FixedForceDirectedLayout to initialize the Diagram.layout?
What was the Node.location both before and after the layout?

Hi Walter,

I used FixedForceDirectedLayout as follows,

before location : (0,0)
after location: (-7.1, -7.1)

I just tried this, and it worked as I expected:

class FixedForceDirectedLayout extends go.ForceDirectedLayout {
  isFixed(v) {
    return v.node.data.fixed || false;
  }
}

In order to test it I added a doubleClick event handler to the node template to toggle the data.fixed property:

myDiagram.nodeTemplate =
  $(go.Node, "Auto",  // the whole node panel
    {
      doubleClick: (e, node) => e.diagram.model.commit(m => m.set(node.data, "fixed", !node.data.fixed))
    },
    . . .

Strange. How did you verify node is not moved?

Because I tried the same you did (bind to doubleClick event) . After I double click and then expand node I can see in the logs isFixed method is called and it return true. But still double clicked node is moved after expand.

<!DOCTYPE html>
<html><body>
  <div id="sample">
    <div id="myDiagramDiv" style="background-color: whitesmoke; border: solid 1px black; width: 100%; height: 800px"></div>
  </div>
  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
class FixedForceDirectedLayout extends go.ForceDirectedLayout {
  isFixed(v) {
    return v.node.data.fixed || false;
  }
}

const $ = go.GraphObject.make;  // for conciseness in defining templates

myDiagram =
  $(go.Diagram, "myDiagramDiv",  // must name or refer to the DIV HTML element
    {
      layout:
        $(FixedForceDirectedLayout,  // automatically spread nodes apart while dragging
          { defaultSpringLength: 30, defaultElectricalCharge: 100 }),
      // do another layout at the end of a move
      "SelectionMoved": e => {
        e.diagram.layoutDiagram(true);
        e.diagram.nodes.each(n => {
          if (n.data.fixed) console.log(n.location.toString(), n.data.text);
        });
      }
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      doubleClick: (e, node) => e.diagram.model.commit(m => m.set(node.data, "fixed", !node.data.fixed))
    },
    $(go.Shape, "Circle",
      { fill: "CornflowerBlue", stroke: "black", spot1: new go.Spot(0, 0, 5, 5), spot2: new go.Spot(1, 1, -5, -5) },
      new go.Binding("fill", "fixed", f => f ? "red" : "cornflowerblue")),
    $(go.TextBlock,
      { font: "bold 10pt helvetica, bold arial, sans-serif", textAlign: "center", maxSize: new go.Size(100, NaN) },
      new go.Binding("text", "text"))
  );

// the rest of this app is the same as samples/conceptMap.html

// create the model for the concept map
var nodeDataArray = [
  { key: 1, text: "Concept Maps" },
  { key: 2, text: "Organized Knowledge" },
  { key: 3, text: "Context Dependent" },
  { key: 4, text: "Concepts" },
  { key: 5, text: "Propositions" },
  { key: 6, text: "Associated Feelings or Affect" },
  { key: 7, text: "Perceived Regularities" },
  { key: 8, text: "Labeled" },
  { key: 9, text: "Hierarchically Structured" },
  { key: 10, text: "Effective Teaching" },
  { key: 11, text: "Crosslinks" },
  { key: 12, text: "Effective Learning" },
  { key: 13, text: "Events (Happenings)" },
  { key: 14, text: "Objects (Things)" },
  { key: 15, text: "Symbols" },
  { key: 16, text: "Words" },
  { key: 17, text: "Creativity" },
  { key: 18, text: "Interrelationships" },
  { key: 19, text: "Infants" },
  { key: 20, text: "Different Map Segments" }
];
var linkDataArray = [
  { from: 1, to: 2, text: "represent" },
  { from: 2, to: 3, text: "is" },
  { from: 2, to: 4, text: "is" },
  { from: 2, to: 5, text: "is" },
  { from: 2, to: 6, text: "includes" },
  { from: 2, to: 10, text: "necessary\nfor" },
  { from: 2, to: 12, text: "necessary\nfor" },
  { from: 4, to: 5, text: "combine\nto form" },
  { from: 4, to: 6, text: "include" },
  { from: 4, to: 7, text: "are" },
  { from: 4, to: 8, text: "are" },
  { from: 4, to: 9, text: "are" },
  { from: 5, to: 9, text: "are" },
  { from: 5, to: 11, text: "may be" },
  { from: 7, to: 13, text: "in" },
  { from: 7, to: 14, text: "in" },
  { from: 7, to: 19, text: "begin\nwith" },
  { from: 8, to: 15, text: "with" },
  { from: 8, to: 16, text: "with" },
  { from: 9, to: 17, text: "aids" },
  { from: 11, to: 18, text: "show" },
  { from: 12, to: 19, text: "begins\nwith" },
  { from: 17, to: 18, text: "needed\nto see" },
  { from: 18, to: 20, text: "between" }
];
myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
  </script>
</body></html>

Hi Walter,

Thank you for the sample. With the help of this sample I’ve manage to fix the issue.

However I have following layouts to apply the same requirement.

  1. CircularLayout
  2. TreeLayout
  3. SerpentineLayout

Will the same fix work for above layouts?

No, those layouts do not have that design, so they do not have such a method that you could override.

Is there any workaround that I can try for these layouts?

Could you please show screenshots or sketches for the behavior that you want after the user has moved several nodes and then adds and removes some nodes or links, causing a layout to happen again?

So initial CircularLayout diagram is look like below

Now I want further explore “Track Section”. I would move “Track section” to free space and then explore it.

After expand diagram is redrawn as below

It would be better if can retain the location of “Track Section” and “AWS” (as in second screenshot) after expand

OK, then the easy thing to do is to set Node.isLayoutPositioned to false on that “Track Section” node. You could set it permanently or temporarily, and whether it’s recorded in the model and there’s a data binding is your choice too.

But note that if there are nodes connected with “Track Section” after expansion (other than “AWS”) they will be put in the circle, because that’s what CircularLayout does. Not around the “Track Section” node. If you wanted that, one possibiility is to use ForceDirectedLayout again, as you can see in Incremental Tree

Yep, It did the trick. I am amazed with all the rich configurations of gojs.

Thanks Walter.