Adornment rotates when node rotates

I have an adornment attached to a node. When I rotate this node by changing its angle value, both the node and its adornment gets rotated too. This makes reading any texts inside the adornment or interacting with buttons inside the adornment very inconvenient.


Is there a way to do this while still using adornments or would I have to manually attach a new node to act as an adornment?

How have you defined your Adornment?

Here’s an example. Basically I have wrapped whatever you have defined as the node template in a simple “Position” Panel that is the new Node. (“Position” is the default type of Panel.) But the user rotates the contents of that Node Panel – which is whatever you had defined as the whole Node before.

<!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();
        }
      }
    });

myDiagram.nodeTemplate =
  new go.Node({
      locationSpot: go.Spot.Center,
      rotatable: true, rotateObjectName: "ROT", rotationSpot: go.Spot.Center
    })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      // the details of this Panel don't matter, just that it's named by the rotateObjectName
      new go.Panel("Auto", { name: "ROT" })
        .bindTwoWay("angle")
        .add(
          new go.Shape({ fill: "white" })
            .bind("fill", "color"),
          new go.TextBlock({ margin: 8 })
            .bind("text")
        )
    );

myDiagram.nodeTemplate.selectionAdornmentTemplate =
  new go.Adornment("Vertical")
    .add(
      new go.Placeholder(),
      new go.TextBlock()
        .bind("text", "color")
    )

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

Adornment is created in a separate file and added into node using something like

myNode.selectionAdornmentTemplate = createAdornmentTemplate(GraphObject.make)

There doesn’t seem to be any issues with adding adornment as it’s done similarly to your example, but I’m encountering some problems with myNode

  1. I’ve tried declaring new go.Node({ rotateObjectName : 'ROT',... }) like you did but apparently none of the properties that was used exist in PanelLayout type? I am able to add these properties in later using iconNode.add(…) it seems

  2. The way myNode is currently implemented, all of the binding and properties are defined within the myNode (including ports) instead of being separated into a wrapper and the node panel itself. This is the code we use for our iconNode with some details omitted

function makeIconNode() {
  const iconNode = new Node(Panel.Vertical)
  iconNode.locationSpot = Spot.Center
  iconNode.locationObjectName = 'icon'
  iconNode.selectionObjectName = 'icon' // use for SVG
  iconNode.selectable = true
  iconNode.selectionAdorned = false

  iconNode.bind(new Binding('cursor', 'storeData').ofModel())

  iconNode.background = 'transparent'
  iconNode.movable = !readonly
  iconNode.deletable = !readonly

  iconNode.bind(
    new Binding('location', 'location', ({ x, y }) => {
      return new Point(x, y)
    })
  );
  iconNode.bind(new Binding('layerName', 'layerName'))
  iconNode.bind(new Binding('angle', 'angle'))
  iconNode.selectionChanged = (part: Part) => { /* Selection Stuff */ }

  iconNode.mouseEnter = (some function)
  iconNode.mouseLeave = (some function)

  iconNode.add(/* some ports */)

  return iconNode;
}

If I’m understanding you correctly, conceptually this is what you’re suggesting

I’ve tried to move whatever properties and bindings I can into a separate Panel called iconPanel, naming the Panel ‘ROT’, then add the Panel into iconNode. I’ve also added rotateObjectName: 'ROT'and the related properties you’ve shown into iconNode but, evidently, I’m not entirely sure which properties/bindings should be assigned to which.

Yes, all Part and Node properties that you set or bind must remain on the Node and cannot move down to the nested Panel.

I’ll update the sample to demonstrate how the location and locationSpot properties remain on the Node (actually those properties are defined on the Part class).

BTW, if you are using a recent version of GoJS, you can replace:

obj.bind(new Binding(. . .))

with one of these two:

obj.bind(. . .)
obj.bindTwoWay(. . .)