Diagram not loading nodes and links properly

I’m building a diagram editor and I’m having trouble with setting the initial data for both nodes and links.

When I set the initial data for the links, I get the following error:

go.js:18 Uncaught TypeError: Cannot read properties of undefined (reading 'x')
    at Link.lN (go.js:18:116721)
    at Link.makeGeometry (go.js:18:115293)
    at Link.ad (go.js:18:112260)
    at PanelLayoutLink.measure (go.js:13:234694)
    at Link._u (go.js:13:259299)
    at Link.ut (go.js:13:133033)
    at Diagram.l0 (go.js:13:25853)
    at Diagram.bS (go.js:13:78562)
    at Diagram.yr (go.js:13:23626)
    at Diagram.maybeUpdate (go.js:13:22818)

I’m loading my links from a JSON file using the default format for links in GoJS, which is:

{
  "from": 1,
  "to": 2,
  "key": 1
}

When I try to remove the initial data for the links (by setting the linkDataArray to an empty array) and keep only the nodes, the nodes are loaded without text, as shown in the screenshot below:

When this happens, I cannot select any node by clicking on it directly, only by dragging a selection box around it.
If I add a new node, it is created with the text, and works normally, unless I try to connect it to one of the nodes that were loaded without text, which results in a crash (the tab memory usage goes up to 100% and the browser crashes).

Here’s the code (I’ve removed the JSON data for the nodes and links, but I can provide it if necessary, and I also removed the functions that are used to create the context menus and the buttons, as they are not relevant to the problem):

const nodeToolTip = go.GraphObject.build('ToolTip').add(
  new go.TextBlock({ margin: sizes.xxs })
    .bind('text')
);

const diagramContextMenu = go.GraphObject.build('ContextMenu').add(
  makeContextMenuButton(
    'Paste (Ctrl + V)',
    (e) => e.diagram.commandHandler.pasteSelection(e.diagram.toolManager.contextMenuTool.mouseDownPoint),
    (o) => o.diagram.commandHandler.canPasteSelection(o.diagram.toolManager.contextMenuTool.mouseDownPoint)
  ),
  makeContextMenuButton(
    'Undo (Ctrl + Z)',
    (e) => e.diagram.commandHandler.undo(),
    (o) => o.diagram.commandHandler.canUndo()
  ),
  makeContextMenuButton(
    'Redo (Ctrl + Y)',
    (e) => e.diagram.commandHandler.redo(),
    (o) => o.diagram.commandHandler.canRedo()
  ),
);

const nodeSelectionAdornmentTemplate = new go.Adornment('Spot')
  .add(
    new go.Panel('Auto').add(
      new go.Shape({ fill: null, stroke: colors.blue, strokeWidth: 1.5, strokeDashArray: [4, 2] }),
      new go.Placeholder({ margin: 1.5 })
    )
  )
  .add(makeAppendNodeButton(new go.Spot(0.5, 0, 0, -sizes.xs), 'TriangleUp'))
  .add(makeAppendNodeButton(new go.Spot(0, 0.5, -sizes.xs, 0), 'TriangleLeft'))
  .add(makeAppendNodeButton(new go.Spot(1, 0.5, sizes.xs, 0), 'TriangleRight'))
  .add(makeAppendNodeButton(new go.Spot(0.5, 1, 0, sizes.xs), 'TriangleDown'));

const nodeResizeAdornmentTemplate = new go.Adornment('Spot', {
  locationSpot: go.Spot.Right
})
  .add(new go.Placeholder());

const cursorSpotKeyMap = {
  'nw-resize': go.Spot.TopLeft,
  'n-resize': go.Spot.Top,
  'ne-resize': go.Spot.TopRight,
  'w-resize': go.Spot.Left,
  'e-resize': go.Spot.Right,
  'se-resize': go.Spot.BottomLeft,
  's-resize': go.Spot.Bottom,
  'sw-resize': go.Spot.BottomRight,
};

Object.entries(cursorSpotKeyMap).forEach(([cursor, spot]) => {
  const resizeAdornmentShape = new go.Shape({
    alignment: spot,
    cursor: cursor,
    desiredSize: new go.Size(sizes.xs, sizes.xs),
    fill: colors.white,
    stroke: colors.blue,
  });
  nodeResizeAdornmentTemplate.add(resizeAdornmentShape);
});

const diagramNodeTemplate = new go.Node('Auto', {
  contextMenu: nodeContextMenu,
  locationSpot: go.Spot.Center,
  selectable: true,
  selectionAdornmentTemplate: nodeSelectionAdornmentTemplate,
  resizable: true,
  resizeObjectName: 'resize-panel',
  resizeAdornmentTemplate: nodeResizeAdornmentTemplate,
  toolTip: nodeToolTip,
  minSize: new go.Size(sizes.lg, sizes.lg),
})
  .bindTwoWay('location', 'location', go.Point.parse, go.Point.stringify)
  .add(
    new go.Panel('Auto', { name: 'resize-panel' })
      .bind('desiredSize', 'desiredSize', go.Size.parse, go.Size.stringify)
      .add(
        new go.Shape('RoundedRectangle', {
          portId: '',
          fromLinkable: true,
          toLinkable: true,
          stroke: 'transparent',
          fill: colors.white,
          strokeWidth: 0,
        })
          .bind('fill')
          .bind('strokeWidth')
          .bind('strokeDashArray', 'strokeDashArray', parseDashArray, stringifyDashArray)
          .bind('figure'),
        new go.TextBlock({
          margin: sizes.xs,
          wrap: go.Wrap.Fit,
          editable: true,
        })
          .bindTwoWay('text')
          .bind('stroke', 'stroke', getTextColor),
      )
  );

const makeAdornmentPathPattern = (linkStrokeWidth) => {
  const shapeStrokeWidth = 2;
  return new go.Shape({
    stroke: colors.blue,
    strokeWidth: shapeStrokeWidth,
    strokeCap: 'square',
    geometryString: `M0 0 M4 2 H3 M4 ${linkStrokeWidth * 2 + shapeStrokeWidth} H3`,
  });
}

const linkSelectionAdornmentTemplate = new go.Adornment().add(
  new go.Shape({
    isPanelMain: true,
    stroke: null,
    strokeWidth: 6,
    pathPattern: makeAdornmentPathPattern(2) // strokeWidth
  })
    .bind('pathPattern', 'strokeWidth', makeAdornmentPathPattern),
);

const diagramLinkTemplate = new go.Link({
  contextMenu: linkContextMenu,
  selectable: true,
  selectionAdornmentTemplate: linkSelectionAdornmentTemplate,
  relinkableFrom: true,
  relinkableTo: true,
  reshapable: true,
  routing: go.Routing.AvoidsNodes,
  curve: go.Curve.JumpOver,
  corner: sizes.xxs,
})
  .bind('points')
  .add(
    new go.Shape({
      isPanelMain: true,
      strokeWidth: 2,
      stroke: colors.black,
    })
      .bind('stroke')
      .bind('strokeWidth')
      .bind('strokeDashArray', 'strokeDashArray', parseDashArray, stringifyDashArray),
    new go.Shape({
      fromArrow: '',
      stroke: colors.black,
      strokeWidth: 2,
      fill: colors.black,
    })
      .bind('fromArrow'),
    new go.Shape({
      toArrow: 'Standard',
      stroke: colors.black,
      strokeWidth: 2,
      fill: colors.black,
    })
      .bind('toArrow'),
  );

const diagramModel = new go.GraphLinksModel(
  nodeData,
  linkData,
  {
    modelData: {
      user: USER,
      diagramId: DIAGRAM_ID,
    },
    linkKeyProperty: 'key',
    makeUniqueKeyFunction: (model) => {
      let key = 1;
      while (model.findNodeDataForKey(key)) {
        key++;
      }
      return key;
    },
    makeUniqueLinkKeyFunction: (model) => {
      let key = 1;
      while (model.findLinkDataForKey(key)) {
        key++;
      }
      return key;
    },
  },
);

const diagram = new go.Diagram('diagram', {
  initialPosition: new go.Point(0, 0),
  contextMenu: diagramContextMenu,
  nodeTemplate: diagramNodeTemplate,
  linkTemplate: diagramLinkTemplate,
  model: diagramModel,
  'draggingTool.dragsLink': true,
  'linkingTool.isUnconnectedLinkValid': false,
  'linkingTool.portGravity': 20,
  'relinkingTool.isUnconnectedLinkValid': false,
  'relinkingTool.portGravity': 20,
  'relinkingTool.fromHandleArchetype': new go.Shape('Rectangle', {
    segmentIndex: 0,
    desiredSize: new go.Size(sizes.xs, sizes.xs),
    fill: colors.white,
    stroke: colors.blue,
  }),
  'relinkingTool.toHandleArchetype': new go.Shape('Rectangle', {
    segmentIndex: -1,
    desiredSize: new go.Size(sizes.xs, sizes.xs),
    fill: colors.white,
    stroke: colors.blue,
  }),
  'linkReshapingTool.handleArchetype': new go.Shape('Rectangle', {
    desiredSize: new go.Size(sizes.xs, sizes.xs),
    fill: colors.white,
    stroke: colors.blue,
  }),
  'undoManager.isEnabled': true,
});

I’m using the latest version of GoJS (v3.0.20).

Any help is greatly appreciated, please note that I’m new to GoJS and I’m still learning how to use it, and I couldn’t find any similar problems in the documentation or in the forums, so I’m not sure what I’m doing wrong.

Also, I couldn’t find any workaround for this problem, so if you have any suggestions, I would be very grateful.

Thank you in advance!

It appears that your node template has made the Shape the default port for the Node (because it sets GraphObject.portId to the empty string), and that it allows the user to draw a new Link from or to that Shape (because it sets GraphObject.fromLinkable and toLinkable).

That means whenever the user does a mouse-down and -move on that Shape, the LinkingTool will operate and the user will be drawing a new Link. That is why the user cannot just drag the Node – the LinkingTool takes precedence over the DraggingTool in order to allow users to easily draw new links and to easily drag nodes when the port(s) do not occupy the whole area of the Node.

Your design need not be the issue, because the user can always drag a Node by doing a mouse-down on the TextBlock rather than on the Shape.

But that seems to explain your problem because the TextBlock.text property seems to remain the empty string despite the binding of that property on the data.text property. Are you sure that your nodeData Array Objects have “text” property values?

Another problem with your node template is that it is an “Auto” Panel, but there is only one element in it. The purpose of an “Auto” Panel is to wrap the main element (by default the first element) around the rest of the elements in the panel. Yet there are no other elements because the first/main one, so the panel really isn’t useful.

Your nested “Auto” Panel is fine, but there’s no reason to have it if you used the Node as your “Auto” Panel, which is already how it is written. So you can just delete the line defining that inner “Auto” Panel with the name “resize-panel”, and remove the setting of Node.resizeObjectName. Oh, I guess upon deleting that constructor call you need to move the call to bind up to the Node, since it will be the whole Node whose “desiredSize” property will be modified by the ResizingTool, and remove the superfluous call to add.

Hello Walter! Thank you for your swift response, I have done a few tests using the advice you provided, but I still haven’t been able to get the nodes to work as expected. Here’s what I have tried so far:

I have made the following changes:
-I removed the Panel named 'resize-panel'
-I removed the portId property assignment in the Shape
-I removed the resizeObjectName property assignment in the “Auto” Node

Also, I have double checked if my nodeArrayData containted a text property, and it does.

Here’s the updated code for the diagram node template:

const diagramNodeTemplate = new go.Node('Auto', {
  contextMenu: nodeContextMenu,
  locationSpot: go.Spot.Center,
  selectable: true,
  selectionAdornmentTemplate: nodeSelectionAdornmentTemplate,
  resizable: true,
  resizeAdornmentTemplate: nodeResizeAdornmentTemplate,
  toolTip: nodeToolTip,
  minSize: new go.Size(sizes.lg, sizes.lg),
})
  .bindTwoWay('location', 'location', go.Point.parse, go.Point.stringify)
  .add(
    new go.Shape('RoundedRectangle', {
      fromLinkable: true,
      toLinkable: true,
      stroke: 'transparent',
      fill: colors.white,
      strokeWidth: 0,
    })
      .bind('fill')
      .bind('strokeWidth')
      .bind('strokeDashArray', 'strokeDashArray', parseDashArray, stringifyDashArray)
      .bind('figure'),
    new go.TextBlock({
      margin: sizes.xs,
      wrap: go.Wrap.Fit,
      editable: true,
    })
      .bindTwoWay('text')
      .bind('stroke', 'stroke', getTextColor),
  );

These changes resulted in the links being loaded, but the nodes remained without text. I have tried removing the desiredSize property binding on the Auto Node, which resulted in the nodes being displayed with the text, but when resizing the nodes, the text would disappear.

Oh, don’t set the margin on the TextBlock. That way as the node gets too small, the text can still be seen. As it is, the TextBlock is offset by the margin, so because of the “Auto” Panel’s clipping due to its smaller-than-normal size, you can’t see any of its characters.

I removed the margin from the TextBlock, but now, when I reload the application, this is how it is displayed:


After resizing the node, it adjusts to a “normal” size, but the text is missing:

I will also try to review my code and see if I can rewrite it based on an example from the samples index. I will keep a copy of the current version so we can continue troubleshooting if needed.

Here’s what I tried when using your code. It seems to work well. I had to make some assumptions for code and data that you did not supply.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.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();
        }
      }
    });

const sizes =
  {
    xs: 10,
    lg: 20
  };
const colors =
  {
    blue: "blue",
    white: "white"
  }

function makeAppendNodeButton(spot, fig) {
  return new go.Shape({ alignment: spot, figure: fig, width: sizes.xs, height: sizes.xs, fill: "lime" })
}

const nodeSelectionAdornmentTemplate = new go.Adornment('Spot')
  .add(
    new go.Panel('Auto').add(
      new go.Shape({ fill: null, stroke: colors.blue, strokeWidth: 1.5, strokeDashArray: [4, 2] }),
      new go.Placeholder({ margin: 1.5 })
    )
  )
  .add(makeAppendNodeButton(new go.Spot(0.5, 0, 0, -sizes.xs), 'TriangleUp'))
  .add(makeAppendNodeButton(new go.Spot(0, 0.5, -sizes.xs, 0), 'TriangleLeft'))
  .add(makeAppendNodeButton(new go.Spot(1, 0.5, sizes.xs, 0), 'TriangleRight'))
  .add(makeAppendNodeButton(new go.Spot(0.5, 1, 0, sizes.xs), 'TriangleDown'));

const nodeResizeAdornmentTemplate = new go.Adornment('Spot', {
  locationSpot: go.Spot.Right
})
  .add(new go.Placeholder());

const cursorSpotKeyMap = {
  'nw-resize': go.Spot.TopLeft,
  'n-resize': go.Spot.Top,
  'ne-resize': go.Spot.TopRight,
  'w-resize': go.Spot.Left,
  'e-resize': go.Spot.Right,
  'se-resize': go.Spot.BottomLeft,
  's-resize': go.Spot.Bottom,
  'sw-resize': go.Spot.BottomRight,
};

Object.entries(cursorSpotKeyMap).forEach(([cursor, spot]) => {
  const resizeAdornmentShape = new go.Shape({
    alignment: spot,
    cursor: cursor,
    desiredSize: new go.Size(sizes.xs, sizes.xs),
    fill: colors.white,
    stroke: colors.blue,
  });
  nodeResizeAdornmentTemplate.add(resizeAdornmentShape);
});

myDiagram.nodeTemplate =
  new go.Node('Auto', {
      //contextMenu: nodeContextMenu,  // didn't bother with this
      locationSpot: go.Spot.Center,
      //selectable: true,  // this is the default value
      selectionAdornmentTemplate: nodeSelectionAdornmentTemplate,
      resizable: true,
      resizeAdornmentTemplate: nodeResizeAdornmentTemplate,
      //toolTip: nodeToolTip,  // didn't bother with this
      minSize: new go.Size(sizes.lg, sizes.lg),
    })
    .bindTwoWay('location', 'location', go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape('RoundedRectangle', {
        fromLinkable: true,
        toLinkable: true,
        stroke: 'transparent',
        fill: colors.white,
        strokeWidth: 0,
      })
        .bind('fill')
        .bind('strokeWidth')
        .bind('strokeDashArray' /*, 'strokeDashArray', parseDashArray, stringifyDashArray*/)
        .bind('figure'),
      new go.TextBlock({
        //margin: sizes.xs,
        wrap: go.Wrap.Fit,
        editable: true,
      })
        .bindTwoWay('text')
        .bind('stroke' /*, 'stroke', getTextColor*/),
    );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", fill: "lightblue", figure: "Circle" },
  { key: 2, text: "Beta", fill: "orange", figure: "Triangle" },
  { key: 3, text: "Gamma", fill: "lightgreen", figure: "Diamond" },
  { key: 4, text: "Delta", fill: "pink" }
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 2 },
  { from: 3, to: 4 },
  { from: 4, to: 1 }
]);
  </script>
</body>
</html>

I tested using the example you provided and I found out what was causing the problem.
Sorry for not providing any sample data, here is an example:

{ "class": "GraphLinksModel",
  "linkKeyProperty": "key",
  "modelData": {"Usuario":1,"CodFluxograma":1},
  "nodeDataArray": [
{"key":1,"text":"In\u00edcio","fill":"#5BCD5D","stroke":"transparent","strokeWidth":"0","strokeDashArray":null,"figure":"Ellipse","desiredSize":"75 75","location":"287 340"},
{"key":2,"text":"LODO","fill":"#8B4513","stroke":"transparent","strokeWidth":"0","strokeDashArray":null,"figure":"RoundedRectangle","desiredSize":"104 48","location":"478.817677813346 324"}
],
  "linkDataArray": [{"key":1,"from":2,"to":1,"points":null,"stroke":"#000000","strokeWidth":null,"strokeDashArray":null}]}

After a few tests I noticed the strokeWidth and stroke property bindings to the Shape were also affecting the TextBlock:

myDiagram.nodeTemplate =
  new go.Node('Auto', {
      //contextMenu: nodeContextMenu,  // didn't bother with this
      locationSpot: go.Spot.Center,
      //selectable: true,  // this is the default value
      selectionAdornmentTemplate: nodeSelectionAdornmentTemplate,
      resizable: true,
      resizeAdornmentTemplate: nodeResizeAdornmentTemplate,
      //toolTip: nodeToolTip,  // didn't bother with this
      minSize: new go.Size(sizes.lg, sizes.lg),
    })
    .bindTwoWay('location', 'location', go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape('RoundedRectangle', {
        fromLinkable: true,
        toLinkable: true,
        stroke: 'transparent',
        fill: colors.white,
        strokeWidth: 0,
      })
        .bind('fill')
        //.bind('strokeWidth')
        .bind('strokeDashArray' /*, 'strokeDashArray', parseDashArray, stringifyDashArray*/)
        .bind('figure'),
      new go.TextBlock({
        //margin: sizes.xs,
        wrap: go.Wrap.Fit,
        editable: true,
      })
        .bindTwoWay('text')
        //.bind('stroke' /*, 'stroke', getTextColor*/),
    );

After removing these bindings, it worked as expected. However, should these bindings affect the TextBlock? And how could I prevent that from happening? I expected them to only be applied to the Shape.

For the nodes, you have set "stroke":"transparent" on both of the node data objects. When there is a binding of TextBlock.stroke to the data.stroke property, then the text will be transparent. Which makes it hard to see :-)

Yes 😅 thank you so much for the help, but I’m still noticing that binding strokeWidth to the Shape also affects the TextBlock, do I have to bind a different property name for each one? For instance:

myDiagram.nodeTemplate =
  new go.Node('Auto')
    .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape('RoundedRectangle')
        .bind('strokeWidth', 'figStrokeWidth'),
      new go.TextBlock()
        .bindTwoWay('text')
        .bind('strokeWidth', 'textStrokeWidth')
    );

I’m not sure that’s a good example, because there’s no TextBlock.strokeWidth property.

If you want to specify them independently of each other, then yes you’ll need to use different source properties. But note that you can use binding converter functions to produce different values for the target property. So it really depends on what you want to do.

I see, but apparently binding the strokeWidth property to the Shape is causing issues with the text, I tested using the previous examples, and if I don’t bind it, it works

Are you using the debug version of the GoJS library? I notice that the value of the data.strokeWidth property is a string in both node data objects. Yet the Shape.strokeWidth property must be a number, not a string.

When using the debug version of the library it will catch more mistakes, with errors or warnings going to the console.

No, I was not using the debug version, I will do that.
And that solved the issue! Thank you.