GoJS not rendering the nodes at the specified locations

For my use-case, I have built a nested-treemap layout where all the calculations for the positions are done by an algorithm that sits on top of GoJS. This algorithm returns a nodeDataArray as expected by the GoJS, and includes the dimension data like leftEdge, rightEdge, topEdge & bottomEdge.

This data is then passed to the GoJS, where in the custom layout, I am just creating nodes & groups with these dimensions and positioning them accordingly.

So before explaining the problem, the use-case is that, whenever I expand a treemap group, the area of the whole treemap increases by some factor, and hence my custom algorithm recalculates the new dimensions.

Now that I have the new dimensions, I want these changes to be propagated to the GoJS to redraw the diagram. For this, rather than clearing the whole diagram and recreating it from scratch, I committed the changes in a transaction.

Reasoning for why I didn’t go with the approach of clearing the whole diagram and recreating is that there are a lot of states in the graph itself, like which groups are expanded, which are collapsed, which are highlighted, etc. To go with this approach, I had to either store all these state changes in my component, which defeats the single source of truth, or maybe go with a ReplaySubject kind of an implementation.

For this reason, committing the changes in a transaction made more sense. This would also keep the door open for any animations that might be required in future.

So, the issue now is that, I am committing the changes, have double, or even triple checked the algorithm that it is giving the right dimensions. For some reason, GoJS is not creating the nodes at the right places.

Its a bit weird, since the order in which I expand nodes also changes the way GoJS is rendering the nodes in the canvas.

For instance:


In this case, the Audit Invoice 1 node on the bottom left is rendering outside its parent (green colored group).

But somehow, when I expand the green group on the top left, it fixes its position:

I have checked the dimensions that are being passed to the GoJS, and they are actually right in both the cases. I feel there is some issue in my way of committing the changes to GoJS.


You can safely ignore the isCollapse and assume it to be always false during my actions in the graph

Are you trying to bind the position of groups? If the Group has a Placeholder and some member Nodes, that binding won’t move the Group because the Group’s position and size are determined by the area occupied by its member nodes.

@walter I just realised, I was always working with the groups in this way. It was all working fine till I didnt implement this dynamic scaling on expand. I dont think so this is because of the group sizing, since with the exact same sizing of the children, the child nodes are being rendered differently.

Also, I was never even passing the size or area, or any such property in the NodeDataArray, so I am not totally sure how the group can even evaluate that in the first place.

To add to it, I dont even have a placeholder to work with in the group. All I am doing is setting the value of isSubGraphExpanded to be true in expanded group template, while having it as false in the collapsed group template.

So is there still a question for us?

Yeah! I am not using the group the way you mentioned, but I am still facing the issue. So yah, still a question.

OK, so what is the question? That I can answer.

The nodes are not rendering at the specified position in some cases.

Could you please provide us with a minimal stand-alone sample that demonstrates the problem? And tell us what you want instead?

I am afraid I won’t be able to get that in a standalone sample. So, I can brief you out as to what I am doing.

  1. The treemap custom template straight out of GoJS was not adequate for our use-case, so all of it is a custom functionality.

  2. Consider it to be as simple as, there is a nested-treemap component, to which the consumers pass a list of nodes with a parent-child relationship, which includes a size/area field depicting the size of each of the node.

  3. There is some custom algorithm that computes and adds an object named _dimensionData containing the area, leftEdge, rightEdge, topEdge, bottomEdge for each node.

  4. This data is passed to GoJS, where we create parts for each of them, and just position & size those parts according to the data in the _dimensionData.

Now the thing is, as per the data that the _dimensionData contains, everything should be rendered just fine. But for reason, GoJS is not rendering them at those places in some cases.

What are those “…Edge” values? Numbers? In what coordinate system?

Are you using a TreeModel or a GraphLinksModel as the model for your nodes and links?


This are the dimensions being passed to each of these nodes.

If you see here, The topEdge of the green node is 642.4, while the topEdge of the blue node is 668.4. topEdge meaning the distance from the top of the canvas. But for some reason, the blue node is getting rendered more upwards than the green node.

But somehow, all the other dimensions are getting rendered just fine.

EDIT: Didnt see your reply - I am using GraphLinksModel. The edgeValues are just numbers that I have used to size & position things. They are not in any coordinate system necessarily, just assuming the topLeft of the canvas as 0,0.

Is your Group template using a Placeholder? If so, remove it.

No, my group template is only using a Placeholder in the selection adornment. I don’t think that should be causing any issue

For the expanded group, it looks like this:

return $(
      go.Group,
      "Auto",
      {
        padding: 0,
        isSubGraphExpanded: true,
        isShadowed: false,
        shadowOffset: new go.Point(0, 2),
        shadowBlur: 8,
        shadowColor: NestedTreemapColors.shadow,
        click: (e, obj) => handleClick(e, obj),
      },
      toolTipHandler(padding),
      // Background for the Node
      $(
        go.Shape,
        "RoundedRectangle",
        {
          fill: NestedTreemapColors.porcelain,
          parameter1: 2,
        },
        new go.Binding("stroke", "metaData", function (metaData) {
          return metaData.highlighted
            ? NestedTreemapColors.orchidBlue
            : "transparent";
        }),
        new go.Binding("strokeWidth", "metaData", function (metaData) {
          return metaData.highlighted ? 1 : 2;
        }),
        new go.Binding("fill", "metaData", function (metaData) {
          return metaData.fillColor;
        })
      ),
      // Foreground for the Node
      $(
        go.Panel,
        "Table",
        {
          padding: new go.Margin(2, 8, 2, 8),
          alignment: go.Spot.Top,
          stretch: go.GraphObject.Fill,
          height: 20,
        },
        new go.Binding("visible", "_dimensionData", (data) => {
          return !getHiddenElements(data).includes(
            NestedTreemapElements.Foreground
          );
        }),
        // Collapse Button
        $(
          "Button",
          {
            row: 0,
            column: 0,
            height: 16,
            width: 16,
            click: handleCollapse,
            alignment: go.Spot.Top,
            alignmentFocus: go.Spot.Top,
          },
          $(go.TextBlock, {
            text: "—",
            font: NestedTreemapFonts.ExpandedButton,
            stroke: NestedTreemapColors.blackPanther,
            textAlign: "center",
            verticalAlignment: go.Spot.Center,
            spacingBelow: 1,
            height: 15,
            width: 15,
          }),
          new go.Binding("visible", "_dimensionData", (data) => {
            return !getHiddenElements(data).includes(
              NestedTreemapElements.Button
            );
          })
        ),
        // Text for Title
        $(
          go.TextBlock,
          {
            row: 0,
            column: 1,
            text: "",
            font: NestedTreemapFonts.Title,
            alignment: new go.Spot(0, 0, 4, 0),
            stroke: NestedTreemapColors.blackPanther,
            stretch: go.GraphObject.Horizontal,
            maxLines: 1,
            overflow: go.TextBlock.OverflowEllipsis,
          },
          new go.Binding("text", "data", function (data) {
            return data.title;
          }),
          new go.Binding("visible", "_dimensionData", (data) => {
            return !getHiddenElements(data).includes(
              NestedTreemapElements.Title
            );
          })
        ),
        // Container for L1, L2, L3 stamps
        $(
          go.Panel,
          "Auto",
          {
            row: 0,
            column: 2,
            alignment: go.Spot.Top,
            alignmentFocus: go.Spot.Top,
            margin: new go.Margin(0, 0, 0, 2),
          },
          new go.Binding("visible", "_dimensionData", (data) => {
            return !getHiddenElements(data).includes(
              NestedTreemapElements.Level
            );
          }),
          // Background for L1, L2, L3 stamps
          $(go.Shape, "RoundedRectangle", {
            fill: NestedTreemapColors.porcelain,
            strokeWidth: 1,
            stroke: NestedTreemapColors.grey10,
            parameter1: 2,
            height: 16,
            width: 16,
          }),
          // Text for L1, L2, L3 stamp
          $(
            go.TextBlock,
            {
              text: "",
              font: NestedTreemapFonts.LevelStamp,
              stroke: NestedTreemapColors.blackPanther,
              height: 15,
              textAlign: "center",
              verticalAlignment: go.Spot.Center,
              spacingAbove: 1,
            },
            new go.Binding("text", "data", function (data) {
              return data.level;
            })
          )
        )
      ),
      $(go.Panel, "Spot", {
        stretch: go.GraphObject.Fill,
        background: NestedTreemapColors.porcelain,
        margin: new go.Margin(20, 2, 2, 2),
      }),
      new go.Binding("isShadowed", "metaData", function (metaData) {
        return metaData.highlighted;
      }),
      // Config for Selection Border
      {
        selectionAdornmentTemplate: $(
          go.Adornment,
          "Auto",
          $(go.Shape, "RoundedRectangle", {
            fill: null,
            stroke: NestedTreemapColors.orchidBlue,
            strokeWidth: 2,
            parameter1: 2,
          }),
          new go.Binding("height", "_dimensionData", function (_dimensionData) {
            // Manually setting the height of the node for the selection border adornment
            return _dimensionData.bottomEdge - _dimensionData.topEdge;
          }),
          new go.Binding("width", "_dimensionData", function (_dimensionData) {
            // Manually setting the width of the node for the selection border adornment
            return _dimensionData.rightEdge - _dimensionData.leftEdge;
          }),
          $(go.Placeholder)
        ),
      }
    );

OK, but I don’t see any binding of the position or location for the group, nor for the desiredSize or width and height of anything representing the group.

This is the custom layout where that is being assigned:

export class CustomTreeMapLayout extends go.Layout {
  constructor(...args: []) {
    super(...(args as []));
  }

  /**
   * Performs the operation of laying out the nodes & groups in the diagram
   * @param coll - The diagram or the group for which layout needs to be called
   */
  doLayout(coll: go.Diagram | go.Group | go.Iterable<go.Part>): void {
    if (!(coll instanceof go.Diagram)) {
      throw new Error("TreeMapLayout only works as the Diagram.layout");
    }

    const diagram = coll;

    const tops = new go.Set();
    diagram.nodes.each((n) => {
      if (n.isTopLevel) {
        tops.add(n);
      }
    });
    tops.each((part: go.Node) => {
      this.layoutNode(part);
    });
  }

  /**
   * Recursive method for positioning the nodes & groups
   * @param part - The part which should be positioned with its children
   */
  layoutNode(part: go.Part): void {
    if (part instanceof go.Group) {
      part.memberParts.each((child) => {
        this.layoutNode(child);
      });
    }
    this.positionNode(
      part,
      part.data._dimensionData.leftEdge,
      part.data._dimensionData.topEdge,
      part.data._dimensionData.rightEdge,
      part.data._dimensionData.bottomEdge
    );
  }

  /**
   * Places the node to its calculated position coordinates
   * @param part - The part to be positioned
   * @param leftEdge - Left-edge of the node's position
   * @param topEdge - Top-edge of the node's position
   * @param rightEdge - Right-edge of the node's position
   * @param bottomEdge - Bottom-edge of the node's position
   */
  positionNode(
    part: go.Part,
    leftEdge: number,
    topEdge: number,
    rightEdge: number,
    bottomEdge: number
  ): void {
    part.moveTo(leftEdge, topEdge);
    const width = rightEdge - leftEdge;
    const height = bottomEdge - topEdge;
    part.desiredSize = new go.Size(width, height);
  }
}

Why don’t you just use Bindings?

Try not calling Part.moveTo, because when it moves a Group it also moves all of its member nodes.

1 Like

Ohh wait, it worked!! Thanks a lot!!

@walter There still seems to be some issue with the placement of groups it seems.


Here, if you see, the yellow container has its leftEdge as 20, and topEdge as 96.
The green box inside it has its leftEdge as 28, and topEdge as 124.

topEdge => 124-96 => 28 => 20 (header) + 8 (padding)
leftEdge => 28-20 => 8 (padding)

For some reason, its positioning is fine from left, but not from top.

EDIT: This issue is only there when we first interact with the graph it seems. If I collapse the group, and expand it again, its not there!

I still don’t understand what layouts you are using. Do you need a layout in order to re-arrange nodes as they change size (because they are groups that have been expanded or collapsed)?