Setting locations on Group results in shifted layout

Hi there,

I’m currently using GoJS to allow users to map out their ecosystem stakeholders.
These stakeholders are group in a Group template as you can see in the screenshot.

I want to store the locations of these groups so that a user can position them exactly as they want it, however when moving them close together, the positioning isn’t exactly as the user left it.

The layout of GoJs is simply: myDiagram.layout = $(go.Layout)
The layout of the group templates is very long, and has anchors as you can see on the screenshots. I’ve tried to turn down the configuration of the groups to almost nothing, but nothing seems to be changing the fact that a couple of the groups are shifted everytime when reloading.

So in simple terms:

  • GoJS layout is pretty basic
  • The location of the nodes are stored and are exactly the same when they are added using

location: group.location ? new go.Point(group.location.x, group.location.y) : null

  • The group configuration is quite long, I don’t mind sharing, but perhaps there’s already some pointers that people can provide just with the described behaviour and screenshots alone.

Thanks so much in advance!


Is there a TwoWay Binding on every Node and every Group, so that each Node or Group, if moved, will have updated information saved in the model?

What is the Group.layout?

I don’t understand those blue dots. Could you explain them please? (Unless you know it has nothing to do with the problem.)

(By the way, you don’t need to set Diagram.layout to a simple instance of Layout – that is its default value.)

Hi Walter,

Thanks for your quick reply!

  • I don’t think I’ve made a two way binding on the Group, but we do capture the move event and store it in our database + set the location in the data model to the dragged location coordinates. I can see the locations being updated in the console when doing so.

  • The Group.layout is a grid layout:

layout: $(go.GridLayout, { wrappingColumn: wrappingColumnCount, isOngoing: true, alignment: go.GridLayout.Position, }),

  • The blue dots, fair question :), those are anchor points which a user can use to draw lines (links) between groups from. They are added to the group and have the following code:
 const $ = go.GraphObject.make

      return $(go.Shape, 'Circle',
        {
          fill: '#5F94FF',
          margin: new go.Margin(10),
          stroke: null,
          desiredSize: new go.Size(7, 7),
          alignment: spot,
          alignmentFocus: spot.opposite(),
          portId: name,
          fromSpot: spot,
          toSpot: spot,
          fromLinkable: true, //! this.isReadOnly,
          toLinkable: true, //! this.isReadOnly,
          cursor: 'pointer',
        },
        new go.Binding('fill', 'isSelected', (isSelected) => {
          return isSelected ? '#4368B3' : '#5F94FF'
        }).ofObject(),
      )

Thanks for the Diagram.layout hint, I didn’t know that :).

To move on with the issue, I have disabled almost everything we put into the Group template, the issue still presents itself, but is more subtle than before. It makes me however more able to share the code:

Group template = $(go.Group, go.Panel.Spot, this.getSegmentGroupDefaultLayoutPanel())

getSegmentGroupDefaultLayoutPanel: function (): go.Panel {
      const $ = go.GraphObject.make

      return $(go.Panel, go.Panel.Auto,
        $(go.Shape, 'RoundedRectangle',
          { strokeWidth: 4, fill: 'white', margin: new go.Margin(30) },
          new go.Binding('stroke', 'isSelected', (isSelected) => {
            return isSelected ? 'dodgerblue' : 'transparent'
          }).ofObject(),
        ),
        $(go.Panel, go.Panel.Vertical,
          {
            defaultAlignment: go.Spot.Center,
            margin: new go.Margin(30, 30, 35, 30),
          },
          $(
            go.Panel,
            go.Panel.Table,
            { defaultAlignment: go.Spot.Top },
            $(
              go.TextBlock,
              {
                name: 'groupTextBlock',
                isMultiline: false,
                font: '16px Fira Sans',
                margin: 10,
                alignment: go.Spot.Center,
                overflow: go.TextBlock.OverflowEllipsis,
                row: 1,
                maxSize: new go.Size(200, 16),
                cursor: 'text',
                stretch: go.GraphObject.Fill,
                textAlign: 'center',
                editable: false,
                toolTip: $(
                  'ToolTip',
                  $(go.TextBlock, { margin: 4 }, new go.Binding('text', 'text')),
                ),
              },
              new go.Binding('text', 'text'),
            ),
            $(
              go.TextBlock,
              {
                font: '12px Fira Sans',
                name: 'groupDescriptionBlock',
                margin: new go.Margin(0, 0, 10, 0),
                isMultiline: false,
                alignment: go.Spot.Center,
                stroke: '#3B3E548C',
                overflow: go.TextBlock.OverflowEllipsis,
                maxSize: new go.Size(200, 12),
                cursor: 'text',
                stretch: go.GraphObject.Fill,
                textAlign: 'center',
                editable: false,
                row: 2,
                toolTip: $(
                  'ToolTip',
                  $(go.TextBlock, { margin: 4 }, new go.Binding('text', 'description')),
                ),
              },
              new go.Binding('text', 'description'),
            ),
            $(
              go.TextBlock,
              {
                font: 'Medium 12px Fira Sans',
                name: 'groupCountBlock',
                margin: new go.Margin(0, 0, 50, 0),
                isMultiline: false,
                alignment: go.Spot.Center,
                stroke: '#686F8F',
                overflow: go.TextBlock.OverflowEllipsis,
                maxSize: new go.Size(200, 12),
                cursor: 'text',
                stretch: go.GraphObject.Fill,
                textAlign: 'center',
                editable: false,
                row: 2,
                toolTip: $(
                  'ToolTip',
                  $(go.TextBlock, { margin: 4 }, new go.Binding('text', 'count')),
                ),
              },
              new go.Binding('text', '', function (data) {
                return 'This segment has ' + data.count + ' actors'
              }),
            ),
          ),
          // create a placeholder to represent the area where the contents of the group are
          $(go.Placeholder,
            { padding: new go.Margin(0, 10) }),
        ),
      )
    },

Hi Walter,

I tried to debug things further and what I’d like to point out is that the shifting behaviour is much, much more subtle when I comment out the following code that is used in my group template

getSegmentGroupDefaultProperties (wrappingColumnCount = 5) {
      const $ = go.GraphObject.make

      return {
        selectionAdorned: false,
        shadowOffset: new go.Point(0, 2),
        shadowColor: '#00000029',
        // locationSpot: go.Spot.Center, // this decides where the x/y coordinates originate (center of the group)
        shadowBlur: 20,
        resizable: false,
        fromLinkable: false,
        toLinkable: false,
        isShadowed: true,
        computesBoundsAfterDrag: true,
        handlesDragDropForMembers: true,
        // Link to gridlayout documentation: https://gojs.net/latest/samples/gLayout.html
        /* layout: $(go.GridLayout, {
          wrappingColumn: wrappingColumnCount,
          isOngoing: true,
          alignment: go.GridLayout.Position,
        }), */

The location and layout property are causing some groups to shift slightly, where the “location” property does most of the damage. Lowering the go.Margin(30) as used in the getSegmentGroupDefaultLayoutPanel function mentioned in my previous post also helps a bit, but that ofcourse changes the look and feel of our groups.

Any pointers you can provide based on that update?

Using a TwoWay Binding on the Node.location property is a much more reliable way to save changed locations to the model. The GridLayout that is the Group.layout will move the member nodes, but it isn’t clear to me that your code will properly save those updated member node locations.

Having a TwoWay Binding on the Group’s location property is helpful if the group is collapsed (perhaps not possible in your app) or if the group is empty (perhaps also not possible in your app).

Hi Walter,

Wrt to the binding, I just do the following then in my Group template construction:

new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),

I capture the move events as follows

myDiagram.addDiagramListener('SelectionMoved', (event) => {
          event.subject.each(part => {
            if (part instanceof go.Group) {
              this.updateSegmentLocation(part.key, part.location)
            }
          })
        })

The issue seems to be much lighter now that I have removed the locationSpot and the grid layout which might be passable as a fix. I’ll check with our users and get back regardless of what the outcome is.

If you could comment on whether I’m correct using the listeners and the two way binding, that’d be great!

A “SelectionMoved” DiagramEvent listener won’t be invoked when Parts are copied, or when Parts are moved by an automatic layout, or when Parts are moved by other code. Maybe that’s OK in your app, but normally it is not in most apps.

Indeed, it’s ok in our app, the user can add a group and then move it to where he wants it to be. So there are no automatic layouts or shifts.

The solution we went for, while not pixel perfect, is to omit the locationSpot and layout from the segment group template properties as posted before. If it helps anyone out in the future, the original code was the entire object with the commented out lines uncommented. Leaving those 2 properties out, fixes the issue to a near perfect positioning.

getSegmentGroupDefaultProperties (wrappingColumnCount = 5) {
      const $ = go.GraphObject.make

      return {
        selectionAdorned: false,
        shadowOffset: new go.Point(0, 2),
        shadowColor: '#00000029',
        // locationSpot: go.Spot.Center, // this decides where the x/y coordinates originate (center of the group)
        shadowBlur: 20,
        resizable: false,
        fromLinkable: false,
        toLinkable: false,
        isShadowed: true,
        computesBoundsAfterDrag: true,
        handlesDragDropForMembers: true,
        // Link to gridlayout documentation: https://gojs.net/latest/samples/gLayout.html
        /* layout: $(go.GridLayout, {
          wrappingColumn: wrappingColumnCount,
          isOngoing: true,
          alignment: go.GridLayout.Position,
        }), */