NorthwoodsSoftware / GoJS

JavaScript diagramming library for interactive flowcharts, org charts, design tools, planning tools, visual languages.
http://gojs.net
Other
7.7k stars 2.86k forks source link

The findNodesOutOf function is returning unexpected results. #219

Closed soonJ817 closed 2 months ago

soonJ817 commented 2 months ago

Hey, I think I found a bug. I'm using the latest version of GoJS with Vue 3. (gojs": "^3.0.4) I implemented it so that when the watched variable in Vue 3 is modified, the node's data is updated as follows.

export default defineComponent({
   setup(props, {emit}) {
      ...

      props.controlPointList.forEach((controlPoint, index) => {
        watch(
          () => controlPoint.value.value,
          (newValue, oldValue) => {
            diagram.startTransaction('updateNode');
            diagram.findNodesByExample({name: controlPoint.name}).each(node => {
              updateLinkedNodes(node, newValue);
            });
            diagram.commitTransaction('updateNode');
          },
        );
      });

      ...   

      const updateLinkedNodes = (node: go.Node, newValue: any) => {
        diagram.model.setDataProperty(node.data, 'value', newValue);
        console.log(node.data.name, node.findNodesOutOf().first()?.data.name, node.findLinksOutOf().first()?.data);
        console.log(node.findNodesOutOf().first()?.data.name);
        node.findNodesOutOf().each(nextNode => {
          console.log("It doesn't enter here!");
          const newValue = nextNode.data.instance.handleLogic(nextNode);
          updateLinkedNodes(nextNode, newValue);
        });
      };

      ...
   }
})

If I remove the two console logs in the updateLinkedNodes function, the "It doesn't enter here!" console doesn't get printed. When I run it with the two console logs, the first node.findNodesOutOf().first()?.data.name comes out as undefined, but right after that, the name gets printed in the console below. If I remove node.findLinksOutOf().first()?.data from the console, the second console also prints undefined. I have absolutely no idea why it behaves like this.

soonJ817 commented 2 months ago

https://github.com/NorthwoodsSoftware/GoJS/assets/45219854/d4af5773-1c08-4a05-bcbe-6b717146782b

WalterNorthwoods commented 2 months ago

First I should point out that executing a transaction within a loop, just as with any database application, is probably not a good idea. I'm concerned that there might be unexpected (to you) side-effects that happen towards the end of each transaction.

What are controlPointList and controlPoint, and what is the purpose of your code? Why are you calling findNodesByExample, which is an unreliable way to find some Nodes, unless you have been very careful, instead of Diagram.findNodeForKey? Why are you iterating over all of the Nodes reached from a Node and using that newValue?

soonJ817 commented 2 months ago
  1. ControlPoint refers to the specific sensor that I want to detect. (This value changes in real time.)
  2. The purpose is to detect changes in ControlPoint, change the value, and reflect this in the diagram.
  3. I could not find a suitable way to process transactions in response to simultaneously changing values.
  4. The reason for using findNodesByExample is to find cases among numerous nodes where the name is the same as the ControlPoint whose value has been changed. (The key is a random uuid.)
  5. In order to update the values ​​of all nodes out of the node of the controlPoint whose value has changed, newValue was calculated and setDataProperty was executed.

In this case, only one controlPoint value change event occurred. Afterwards, I found this case while tracking value changes. I did not expect that calling the findLinksOutOf function would change the result of findNodesOutOf as printed in the log.

WalterNorthwoods commented 2 months ago

Do you happen to know if the problem does not occur when using GoJS v2.3.17?

WalterNorthwoods commented 2 months ago

I'm unable to reproduce a bug. Perhaps I'm not testing exactly the situation that you have. Here's my code, as a complete stand-alone sample that highlights paths of nodes.

Two of the buttons start just with the "Omega" node; the other two buttons start with both nodes whose name starts with "O".

Two of the buttons highlight all of the nodes reachable from the starting node(s); the other two buttons only highlight those nodes whose name satisfies a predicate that checks for "e" in the name.

If you check the "Highlight Links" checkbox, it also highlights the links between the nodes. I think the code when not highlighting links is most like your code.

My code includes two console.log statements that are like yours. Commenting out one of them or both of them does not change the behavior.

So, how is my code meaningfully different from yours? How can I reproduce the problem?

<!DOCTYPE html>
<html>
<head>
  <title>Highlighting paths</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:200px"></div>
  <button id="myTestButton3">Root is "Omega" (no predicate)</button>
  <button id="myTestButton4">Root is "Omega", "e" predicate</button>
  <button id="myTestButton">Start with "O" (no predicate)</button>
  <button id="myTestButton2">Start with "O", "e" predicate</button>
  <input type=checkbox id="myHighlightLinks"></input>
  <label for="myHighlightLinks">Highlight Links</label>
  <textarea id="myOutput" style="width:100%;height:300px"></textarea>

  <script src="https://unpkg.com/gojs@3.0"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      layout: new go.LayeredDigraphLayout(),
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Node()
    .bindObject("background", "isHighlighted", h => h ? "lime" : null)
    .add(
      new go.TextBlock()
        .bind("text", "name")
    );

myDiagram.linkTemplate =
  new go.Link()
    .add(
      new go.Shape({ strokeWidth: 1.5 })
        .bindObject("stroke", "isHighlighted", h => h ? "lime" : "black"),
      new go.Shape({ toArrow: "Standard", strokeWidth: 0 })
        .bindObject("fill", "isHighlighted", h => h ? "lime" : "black")
    );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, name: "Omicron" },
  { key: 2, name: "Rho" },
  { key: 3, name: "Omega" },
  { key: 4, name: "Delta" },
  { key: 5, name: "Zeta" },
  { key: 6, name: "Phi" },
  { key: 7, name: "Chi" },
],
[
  { from: 1, to: 2 },
  { from: 1, to: 4 },
  { from: 1, to: 7 },
  { from: 3, to: 2 },
  { from: 3, to: 4 },
  { from: 3, to: 7 },
  { from: 2, to: 5 },
  { from: 2, to: 6 },
  { from: 4, to: 5 },
  { from: 4, to: 6 },
]);

function follow(node, pred, indent, msg) {
  if (indent === undefined) indent = 0;
  if (msg === undefined) msg = "";

  node.isHighlighted = true;
  msg += "\n" + "  ".repeat(indent) + node.data.name;

  // Your code is like this:
  console.log(node.data.name, node.findNodesOutOf().first()?.data.name, node.findLinksOutOf().first()?.data);
  console.log(node.findNodesOutOf().first()?.data.name);

  if (!document.getElementById("myHighlightLinks").checked) {
    // Your code is like this:
    node.findNodesOutOf().each(n => {
      if (pred && !pred(n)) return;
      msg = follow(n, pred, indent+1, msg);
    });
  } else {
    // Alternatively, this also highlights links as well as nodes:
    node.findLinksOutOf().each(l => {
      const to = l.toNode;
      if (pred && !pred(to)) return;
      l.isHighlighted = true;
      msg += "\n" + "  ".repeat(indent+1) + l.toString();
      msg = follow(to, pred, indent+2, msg);
    });
  }
  return msg;
}

function highlightAndLog(startPattern, filterPredicate) {
  myDiagram.commit(diag => {
    diag.clearHighlighteds();
    let msg = "";
    diag.findNodesByExample({ name: startPattern }).each(root => {
      msg += follow(root, filterPredicate);
    });
    document.getElementById("myOutput").textContent = msg;
  });
}

document.getElementById("myTestButton").addEventListener("click", e => {
  highlightAndLog(/O/);
});

document.getElementById("myTestButton2").addEventListener("click", e => {
  highlightAndLog(/O/, n => n.data.name.indexOf("e") > 0);  // predicate checks for "e" in data.name
});

document.getElementById("myTestButton3").addEventListener("click", e => {
  highlightAndLog("Omega");
});

document.getElementById("myTestButton4").addEventListener("click", e => {
  highlightAndLog("Omega", n => n.data.name.indexOf("e") > 0);
});
  </script>
</body>
</html>
soonJ817 commented 2 months ago

Thank you sincerely for your kindness. Lemme try.

soonJ817 commented 2 months ago

Can you help me a little more? It's been less than a month since I started working with JavaScript and Vue code, so I'm not very good at it yet. I also couldn't reproduce it from the code you gave me. However, the same issue reappeared in v2.3.17.

Let me explain the behavior before that situation to understand more accurately. I initialized the diagram as follows:

const createDiagram = () =>
      new go.Diagram(
        diagramRef.value as HTMLDivElement,
        {
          "draggingTool.isGridSnapEnabled": true,
          "draggingTool.gridSnapCellSize": go.Size.parse("5 5"),
          initialContentAlignment: go.Spot.Center,
          "undoManager.isEnabled": true,
        } as DiagramInitOptions
      );

...

const initLinkTemplate = () => {
      const $ = go.GraphObject.make;
      diagram.model = new go.GraphLinksModel({
        linkFromPortIdProperty: "fromPort",
        linkToPortIdProperty: "toPort",
      });

      diagram.linkTemplate = $(
        go.Link,
        {
          routing: go.Routing.AvoidsNodes,
          curve: go.Curve.JumpOver,
          fromEndSegmentLength: 15,
          toEndSegmentLength: 15,
          corner: 3,
          selectionAdorned: true, 
        },
        $(go.Shape, { strokeWidth: 2, stroke: "red" }),
        $(go.Shape, { toArrow: "OpenTriangle", stroke: "red" })
      );
    };

Then, data was injected as follows using the diagram.model.addLinkDataCollection and diagram.model.addNodeDataCollection functions.

{
  "nodeDataArray": [
    {
      "key": "6a2312d5-eaee-45fb-bb71-3c8780729ebd",
      "category": "관제점 4(Boolean)",
      "objectType": "ControlPoint",
      "loc": "-115 -280",
      "value": false,
      "name": "관제점 4(Boolean)"
    },
    {
      "key": "ca1d4e2b-f833-4490-8de4-65547d214ecb",
      "category": "관제점 5(Boolean)",
      "objectType": "ControlPoint",
      "loc": "-90 -175",
      "value": false,
      "name": "관제점 5(Boolean)"
    },
    {
      "key": "b9be5628-36ce-4387-8614-c4303a224e84",
      "category": "관제점 6(Boolean)",
      "objectType": "ControlPoint",
      "loc": "-120 -70",
      "value": false,
      "name": "관제점 6(Boolean)"
    },
    {
      "key": "77285f99-bb68-4d9d-b1a4-679fb8666740",
      "category": "관제점 7(Boolean)",
      "objectType": "ControlPoint",
      "loc": "-120 95",
      "value": false,
      "name": "관제점 7(Boolean)"
    },
    {
      "key": "1b82094c-f2f4-4559-8f84-defad21d6c9e",
      "category": "Counter",
      "objectType": "Logic",
      "loc": "120 -130",
      "value": 10,
      "name": "Counter"
    },
    {
      "key": "71bf6ddf-c680-463a-8498-4708ab5ae0c1",
      "category": "관제점 1",
      "objectType": "ControlPoint",
      "loc": "305 -135",
      "value": 11,
      "name": "관제점 1"
    },
    {
      "key": "c8bddc73-7584-49b7-b5b8-53bdad5166a3",
      "category": "LED",
      "objectType": "Graphic",
      "loc": "185 105",
      "value": true,
      "name": "LED",
      "threshold": 10,
      "controlPointName": "관제점 1"
    }
  ],
  "linkDataArray": [
    {
      "from": "6a2312d5-eaee-45fb-bb71-3c8780729ebd",
      "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e",
      "fromPort": "out",
      "toPort": "Count Up"
    },
    {
      "from": "ca1d4e2b-f833-4490-8de4-65547d214ecb",
      "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e",
      "fromPort": "out",
      "toPort": "Count Down"
    },
    {
      "from": "b9be5628-36ce-4387-8614-c4303a224e84",
      "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e",
      "fromPort": "out",
      "toPort": "Preset In"
    },
    {
      "from": "77285f99-bb68-4d9d-b1a4-679fb8666740",
      "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e",
      "fromPort": "out",
      "toPort": "Clear In"
    },
    {
      "from": "1b82094c-f2f4-4559-8f84-defad21d6c9e",
      "to": "71bf6ddf-c680-463a-8498-4708ab5ae0c1",
      "fromPort": "out",
      "toPort": "in"
    }
  ]
}

The following is an example of the Count Up port panel definition. The definitions of all ports are in the same format.

new go.Panel('Table').add(
      this.$(go.TextBlock, {background: 'white', text: "Count Up", width: 100, textAlign: 'center'}),
      this.$(go.Shape, 'Rectangle', {
        desiredSize: new go.Size(6, 6),
        fill: 'black',
        fromSpot: go.Spot.Right,
        fromLinkable: isOut,
        toSpot: go.Spot.Left,
        toLinkable: !isOut,
        cursor: 'pointer',
      }, {portId: "Count Up", alignment: new go.Spot(+isOut, 0.5)}),
    );

I think there's something wrong with using the port. In the problematic part, when I print node.findNodesOutOf() or node.findNodesConnected() to the console, EmptyIterator is output. However, if you call a link-related function (findLinksConnected, etc.), the output is printed as expected, and the expected operation is performed from then on.

WalterNorthwoods commented 2 months ago

It could be that the node really doesn't have any nodes:

On Mon, Jul 15, 2024 at 9:36 PM soonJ817 @.***> wrote:

Can you help me a little more? It's been less than a month since I started working with JavaScript and Vue code, so I'm not very good at it yet. I also couldn't reproduce it from the code you gave me. However, the same issue reappeared in v2.3.17.

Let me explain the behavior before that situation to understand more accurately. I initialized the diagram as follows:

const createDiagram = () => new go.Diagram( diagramRef.value as HTMLDivElement, { "draggingTool.isGridSnapEnabled": true, "draggingTool.gridSnapCellSize": go.Size.parse("5 5"), initialContentAlignment: go.Spot.Center, "undoManager.isEnabled": true, } as DiagramInitOptions );

...

const initLinkTemplate = () => { const $ = go.GraphObject.make; diagram.model = new go.GraphLinksModel({ linkFromPortIdProperty: "fromPort", linkToPortIdProperty: "toPort", });

  diagram.linkTemplate = $(
    go.Link,
    {
      routing: go.Routing.AvoidsNodes,
      curve: go.Curve.JumpOver,
      fromEndSegmentLength: 15,
      toEndSegmentLength: 15,
      corner: 3,
      selectionAdorned: true,
    },
    $(go.Shape, { strokeWidth: 2, stroke: "red" }),
    $(go.Shape, { toArrow: "OpenTriangle", stroke: "red" })
  );
};

Then, data was injected as follows using the diagram.model.addLinkDataCollection and diagram.model.addNodeDataCollection functions.

{ "nodeDataArray": [ { "key": "6a2312d5-eaee-45fb-bb71-3c8780729ebd", "category": "관제점 4(Boolean)", "objectType": "ControlPoint", "loc": "-115 -280", "value": false, "name": "관제점 4(Boolean)" }, { "key": "ca1d4e2b-f833-4490-8de4-65547d214ecb", "category": "관제점 5(Boolean)", "objectType": "ControlPoint", "loc": "-90 -175", "value": false, "name": "관제점 5(Boolean)" }, { "key": "b9be5628-36ce-4387-8614-c4303a224e84", "category": "관제점 6(Boolean)", "objectType": "ControlPoint", "loc": "-120 -70", "value": false, "name": "관제점 6(Boolean)" }, { "key": "77285f99-bb68-4d9d-b1a4-679fb8666740", "category": "관제점 7(Boolean)", "objectType": "ControlPoint", "loc": "-120 95", "value": false, "name": "관제점 7(Boolean)" }, { "key": "1b82094c-f2f4-4559-8f84-defad21d6c9e", "category": "Counter", "objectType": "Logic", "loc": "120 -130", "value": 10, "name": "Counter" }, { "key": "71bf6ddf-c680-463a-8498-4708ab5ae0c1", "category": "관제점 1", "objectType": "ControlPoint", "loc": "305 -135", "value": 11, "name": "관제점 1" }, { "key": "c8bddc73-7584-49b7-b5b8-53bdad5166a3", "category": "LED", "objectType": "Graphic", "loc": "185 105", "value": true, "name": "LED", "threshold": 10, "controlPointName": "관제점 1" } ], "linkDataArray": [ { "from": "6a2312d5-eaee-45fb-bb71-3c8780729ebd", "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e", "fromPort": "out", "toPort": "Count Up" }, { "from": "ca1d4e2b-f833-4490-8de4-65547d214ecb", "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e", "fromPort": "out", "toPort": "Count Down" }, { "from": "b9be5628-36ce-4387-8614-c4303a224e84", "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e", "fromPort": "out", "toPort": "Preset In" }, { "from": "77285f99-bb68-4d9d-b1a4-679fb8666740", "to": "1b82094c-f2f4-4559-8f84-defad21d6c9e", "fromPort": "out", "toPort": "Clear In" }, { "from": "1b82094c-f2f4-4559-8f84-defad21d6c9e", "to": "71bf6ddf-c680-463a-8498-4708ab5ae0c1", "fromPort": "out", "toPort": "in" } ] }

The following is an example of the Count Up port panel definition. The definitions of all ports are in the same format.

new go.Panel('Table').add( this.$(go.TextBlock, {background: 'white', text: "Count Up", width: 100, textAlign: 'center'}), this.$(go.Shape, 'Rectangle', { desiredSize: new go.Size(6, 6), fill: 'black', fromSpot: go.Spot.Right, fromLinkable: isOut, toSpot: go.Spot.Left, toLinkable: !isOut, cursor: 'pointer', }, {portId: "Count Up", alignment: new go.Spot(+isOut, 0.5)}), );

I think there's something wrong with using the port. In the problematic part, when I print node.findNodesOutOf() or node.findNodesConnected() to the console, EmptyIterator is output. However, if you call a link-related function (findLinksConnected, etc.), the output is printed as expected, and the expected operation is performed from then on.

— Reply to this email directly, view it on GitHub https://github.com/NorthwoodsSoftware/GoJS/issues/219#issuecomment-2229817054, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACV7Y52GTLI2TEM3DRJCMG3ZMR2KBAVCNFSM6AAAAABKWQJV7SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMRZHAYTOMBVGQ . You are receiving this because you commented.Message ID: @.***>

soonJ817 commented 2 months ago

If there really are no nodes, then after calling findLinks, findNodes should also show no nodes. When I have time, I will create a stand-alone sample project. Thank you :-)