Merging diagram

I have few diagrams (for eq 3) information stored in database with their nodes and location.
Now assume all 3 diagrams some node position is at location 100,100.

At the same time, i have another diagram which shows global overview of these 3 diagrams. My requirement is any change in individual diagram should get visible in global overview also.
I was thinking of merging the nodes and links collection (in all 3 diagrams nodes and links are unique)
and display them with some layout.

I haven’t tried this yet but see a problem of locations of nodes. Any thought on how this should be resolved? Display the individual diagram nodes in global overview using some offset mechanism.
In individual diagram, i have a single container node which contains all other nodes.
With global overview , all containers should be visible using some layouts and there internal nodes should get the correct position.

Yes, use a binding conversion function that adds an offset depending on which original diagram it is in. You can even allow users to move nodes in your overview if the back-conversion first subtracts that offset.

Hi Walter,
Good to know that it is possible. Do you have any example to share - that would be great help to get me the expected result quickly.

In your scenario are there four Diagrams showing at the same time on the same page? Or just one Diagram that happens to be showing three models at the same time?

And is the user supposed to be able to edit in this one overview diagram?

User is seeing one diagram at a time. There are 4 diagrams.
Three diagrams named D1, D2, D3.
Each diagram outermost element itself is container and holds child nodes with relations.

Now when user clicks on some button all 3 diagrams should get displayed as a single diagram. In this global overview user can make connection between the container nodes.
Any change inside the containers should get reflected in individual diagrams when user clicks on D1 or D2 or D3.

One problem is making sure the keys (identifiers) remain unique across your little diagrams

Another problem is making sure that undo and redo work across little diagrams.

So I think it’s better to have a single GoJS Model in a single GoJS Diagram. Each of your little diagrams is implemented by a Group. One can show all of the groups at the same time, or just show one of them. Or I suppose you could show a different subset if you wanted. (I don’t know how variable the number of little diagrams will be.) Each Group can also be shown in a separate Layer, so it’s easy to show or hide them.

In any case, I assume you do not want to allow a link from one node to a node in a different group. That’s achieved by specifying a linkValidation predicate.

The node template and the link template have Part.containingGroupChanged event handler so that they can change their Part.layerName appropriately. I also have the link look different for links between groups than for links between nodes within a group.

It wasn’t clear how many ports you wanted on simple nodes or on groups, so I just decided to have one input and one output port per node or group. You can easily change that decision to do whatever you want. Same goes for the appearance and behavior of the nodes and the groups.

Here is the complete code, HTML and all:

<!DOCTYPE html>
<html>
<head>
<title>Minimal GoJS Sample</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="https://unpkg.com/gojs"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          "undoManager.isEnabled": true,
          // restrict links to either be within a single group or between groups
          "linkingTool.linkValidation": function(fromnode, fromport, tonode, toport) {
            return fromnode && tonode && fromnode.containingGroup === tonode.containingGroup;
          },
          "relinkingTool.linkValidation": function(fromnode, fromport, tonode, toport) {
            return fromnode && tonode && fromnode.containingGroup === tonode.containingGroup;
          },
          "ModelChanged": function(e) {     // just for demonstration purposes,
            if (e.isTransactionFinished) {  // show the model data in the page's TextArea
              document.getElementById("mySavedModel").textContent = e.model.toJson();
            }
          }
        });

    // have each group (i.e. diagram) be shown in a different layer
    myDiagram.addLayerBefore($(go.Layer, { name: "g1" }), myDiagram.findLayer(""));
    myDiagram.addLayerBefore($(go.Layer, { name: "g2" }), myDiagram.findLayer(""));

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        { // make sure each node that belongs to a group is in the same layer
          containingGroupChanged: function(node, oldgrp, newgrp) {
            node.layerName = newgrp ? newgrp.key : "";
          }
        },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        { locationSpot: go.Spot.Center },
        $(go.Panel, "Spot",
          $(go.Panel, "Auto",
            $(go.Shape,
              { fill: "white" },
              new go.Binding("fill", "color")),
            $(go.TextBlock,
              { margin: 8, editable: true },
              new go.Binding("text").makeTwoWay())
          ),
          $(go.Shape, "Circle",
            {
              alignment: go.Spot.Left, toSpot: go.Spot.Left,
              width: 8, height: 8, fill: "white",
              portId: "in", toLinkable: true, cursor: "pointer"
            }
          ),
          $(go.Shape, "Circle",
            {
              alignment: go.Spot.Right, fromSpot: go.Spot.Right,
              width: 8, height: 8,
              portId: "out", fromLinkable: true, cursor: "pointer"
            }
          )
        )
      );

    myDiagram.groupTemplate =
      $(go.Group, "Vertical",
        new go.Binding("layerName", "key"),
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        { locationSpot: go.Spot.Center },
        $(go.TextBlock,
          { font: "bold 12pt sans-serif" },
          new go.Binding("text")),
        $(go.Panel, "Spot",
          $(go.Panel, "Auto",
            $(go.Shape, { strokeWidth: 2, fill: "lightgray" }),
            $(go.Placeholder, { padding: 10 })
          ),
          $(go.Shape, "Circle",
            {
              alignment: go.Spot.Left, toSpot: go.Spot.Left,
              width: 10, height: 10, fill: "white",
              portId: "in", toLinkable: true, cursor: "pointer"
            }
          ),
          $(go.Shape, "Circle",
            {
              alignment: go.Spot.Right, fromSpot: go.Spot.Right,
              width: 10, height: 10,
              portId: "out", fromLinkable: true, cursor: "pointer"
            }
          )
        )
      );

    myDiagram.linkTemplate =
      $(go.Link,
        {
          fromPortId: "out", toPortId: "in",
          relinkableFrom: true, relinkableTo: true,
          containingGroupChanged: function(link, oldgrp, newgrp) {
            link.path.strokeWidth = newgrp ? 1 : 2;  // thicker if between groups
            link.layerName = newgrp ? newgrp.key : "";  // make sure link belongs to correct layer
          }
        },
        $(go.Shape, { strokeWidth: 2 }),
        $(go.Shape, { toArrow: "OpenTriangle" })
      );

    myDiagram.model =
      $(go.GraphLinksModel,
        {
          linkKeyProperty: "key",
          linkFromPortIdProperty: "fp",
          linkToPortIdProperty: "tp",
          nodeDataArray:
            [
              { key: "g1", text: "Model 1", isGroup: true },
              { key: 1, text: "Alpha", color: "lightblue", model: "1", group: "g1" },
              { key: 2, text: "Beta", color: "orange", group: "g1" },
              { key: "g2", text: "Model 2", isGroup: true },
              { key: 3, text: "Gamma", color: "lightgreen", group: "g2" },
              { key: 4, text: "Delta", color: "pink", group: "g2" },
            ],
          linkDataArray:
            [
              { from: 1, to: 2 },
              { from: 3, to: 4 },
              { from: "g1", to: "g2" }
            ]
        });
  }

  function showGroup(key) {
    myDiagram.commit(function(diag) {
      diag.layers.each(function(layer) {
        layer.visible = (!key || layer.name === key);
      })
    }, null);  // don't record these changes in the UndoManager
  }
</script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button onclick="showGroup(null)">Joint View</button>
  <button onclick="showGroup('g1')">Diagram 1</button>
  <button onclick="showGroup('g2')">Diagram 2</button>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
</body>
</html>

Hi Walter,

I do not expect the undo/redo functionality. And the sample do not meet the expectation.
Here you are saving one diagram having different groups. When clicked on individual button and I move them on same position and then clicking on Joint view, both group gets overlapped.

The way i am looking it is - when clicked on individual buttons, the diagram should model should have only 3 nodes (from your example - Model1, Alpha and beta OR Model2, Gamma and Delta).
Alpha, beta, Gamma and Delta should maintain the loc property relative to Model1/Model2 respectively.

On click on “Joint View”, merge both diagrams model (all nodes have unique IDs. No individual models have common key/id) and arrange their group nodes with some layout. That way JointView do not consider the location of container nodes but inner nodes location will change relative to the position of container node in global view.

Possible to make such sample ?

Try setting the Diagram.layout.

Hi Walter,
Still it will be single diagram. My basic requirement is to have different diagrams.

I tried to do something which set child nodes position relative to container nodes.
See the code pen on https://codepen.io/rajeshpatil74/pen/wvMQgVX

With this, nodes are position at the relative to the outer container. But with few issues like -

  • Reposition model node not moving all nodes smoothly always. Mostly outer node gets moved and releasing mouse reposition inner node.
  • Moving inside node is not smooth.
  • Repositioing model node(container) changes the inner nodes location slightly. It should remain same.

Will you please have a look in the code of same.

You probably want to turn off real-time layouts:

          layout: $(go.TreeLayout, { isRealtime: false }),

Why do you need to have different diagrams? You said you wouldn’t be showing more than one diagram at a time. Perhaps you mean that you want to save the models separately? That can be implemented.

I guess, like you suggested, if needed i can save only the model nodes/links of individual container and load all these before displaying OR save a full diagram.
I modified the sample with resizing containers nodes and restrict child nodes insider the container.

As of now, Initial loaded view not showing child nodes insider container. Will work on that.

The other two issues i face are

  1. In joined view only I can resize container nodes and not in individual views.
    This should be possible, because i want to resize the container and then add new child nodes when individual container is shown.

  2. In joined view, whenever container node is resized, the whole diagram perform the layout again.
    Any way to prevent that ? Not sure as of now whether i need this because then whole diagram layout also needs to be preserved and loaded next time again.

It’s hiding temporary Layers, such as the layer that holds Adornments. Change showGroup:

  layer.visible = layer.isTemporary || !key || (layer.name === key);

Set Layout.isOngoing to false, or maybe better, set Group.layoutConditions to go.Part.LayoutStandard & ~go.Part.LayoutNodeSized. Read more about the choices at https://gojs.net/latest/intro/layouts.html#LayoutInvalidation

I changed the container group nodes figure to circle instead of reactangle.

Is it possible that child nodes should not go beyound the circle. As shown in codepen, the nodes some part possible to drag outside the circle.

Your Group still has a Placeholder in it, doesn’t it? All Placeholders are rectangular. But you could change the Group.layout so that it puts its member nodes within a circular area. If you are allowing users to move member nodes, you can restrict where they go by implementing a Part.dragComputation function that controls where the node may go when dragged.