Text Resize Causes Diagram Resize

When right-clicking on a node, we have a context menu that pops up (HTML Element) above it that allows for editing properties on the node’s data. Once the text size is selected from the context menu, we start a transaction, call findNodeForData(), setDataProperty() and commit the transaction.

We are experiencing an issue when the text is resized, the node changes size and as a result, the diagram seems to update its position (similar to zoomToFit). I am logging the diagram.viewPortBounds and diagram.documentBounds with no changes before and after the transaction. Any ideas of what might be causing this?

By default a change in the size of a Node will invalidate the responsible layout (probably the Diagram.layout) which will result in the performance of an automatic layout towards the end of the transaction. I’m guessing that this is what is causing the action you are seeing in the diagram.

So the first question is whether you have set Diagram.layout and if so, to what?

If you have, you could try setting Layout.isOngoing to false in order to prevent the invalidation of the layout and thus all automatic layouts.

Diagram.layout was not set. Setting Layout.IsOngoing to false seems to have rectified the issue. Thanks!

It looks like this is still occurring in different node arrangements, where setting isOngoing to false did seem to correct the issue initially. Diagram.layout is not set and we are setting isOngoing to false when the diagram is initialized.

Well, if you want us to help you we need a lot more information. Can you reproduce the problem with a minimal stand-alone sample?

Totally understand… I think it would take a great deal of time to reproduce a sample. We will keep looking into the issue and come to that as a last resort. In the mean time, I can provide some snippets in the event that is helpful…

Here is how we are initializing the diagram:

 const dia = $(go.Diagram, {
      allowClipboard: false,
      allowCopy: false,
      allowDelete: true,
      allowDragOut: false,
      allowDrop: false,
      allowGroup: false,
      allowInsert: false,
      allowLink: false,
      allowMove: true,
      allowRelink: false,
      allowReshape: false,
      allowResize: false,
      allowRotate: true,
      allowSelect: true,
      allowTextEdit: false,
      allowUndo: true,
      allowUngroup: false,
      contentAlignment: go.Spot.Center,
      contextMenuTool: new ContextMenuZoomTool(this.contextMenuPositioner, this.editModeService),
      initialAutoScale: go.AutoScale.Uniform,
      scrollMode: go.ScrollMode.Document,
      'animationManager.isEnabled': false,
      padding: Constants.DIAGRAM_PADDING,
      'dragSelectingTool.delay': 250,
      'toolManager.toolTipDuration': Number.POSITIVE_INFINITY,
      draggingTool: this.multiSelectionDraggingTool,
      'undoManager.isEnabled': false,
      'ChangedSelection': this.updateMultiSelectionPart,
      "ModelChanged": e => {
        if (e.isTransactionFinished) this.updateMultiSelectionPart()
      },
      skipsUndoManager: true,
      model: new go.GraphLinksModel({
        nodeKeyProperty: 'key',
        linkKeyProperty: 'key', // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
        skipsUndoManager: true
      }),
    });
    dia.layout.isOngoing = false;

... listeners to follow

When a user right clicks a node, a context menu pops up above it allowing text resize as one of many edit options. When that data is updated from the context menu, we are updating the data as such:

  editNodeProperty(nodeModifier: NodeModifier) {
    const model = this.diagram?.model

    this.diagram?.startTransaction(Constants.EDIT_NODE);
    this.diagram?.selection.each(n => {
      const node = this.diagram?.findNodeForData(n.part?.data);
      if (node?.data.hasOwnProperty(nodeModifier.nodeKey)) {
        model?.setDataProperty(node?.data, nodeModifier.nodeKey, nodeModifier.value);
      }
    })
    this.diagram?.commitTransaction(Constants.EDIT_NODE);
  }

The template for the node we are editing:

const gridNodeTemplate = go.GraphObject.make(
      go.Node,
      {
        locationSpot: go.Spot.TopLeft,
        rotationSpot: go.Spot.Center,
        contextMenu: contextMenu(this.editModeService, this.contextMenuPositioner)
      },
      new go.Binding('location', 'location', (location) => new go.Point(location.item1, location.item2), (point: go.Point) => this.nodeHelperService.createLocation(point.x, point.y)),
      new go.Binding('angle', 'rotationAngle').makeTwoWay(),
      new go.Binding('background', 'fill', (fill) => this.colorService.convertColorStringTo(ColorFormat.Rgba, fill)),
      new go.Binding('zOrder'),
      new go.Binding('movable', 'editMode').ofModel(),
      new go.Binding('selectable', 'editMode').ofModel(),
      new go.Binding('rotatable', 'editMode').ofModel(),
      new go.Binding('deletable', 'editMode').ofModel(),
      go.GraphObject.make(
        go.Panel,
        'Table',
        {
          defaultAlignment: go.Spot.Left,
        },
        go.GraphObject.make(
          go.Panel,
          'TableRow',
          {
            isPanelMain: true,
            alignment: go.Spot.Left,
            toolTip: go.GraphObject.make(
              go.Adornment,
              new go.Binding('visible', 'toolTip', (toolTip?: ToolTipInfo) =>
                this.tooltipHelperService.showToolTip(toolTip)
              ),
              'Auto',
              go.GraphObject.make(
                go.Shape,
                'RoundedRectangle',
                {
                  // Set fixed width of 250 for the tooltip shape node
                  width: Constants.TOOLTIP_SHAPE_WIDTH,
                },
                new go.Binding('fill', 'theme', (theme: string) =>
                  this.apmThemeService.getThemeValue(ThemeConstants.colorFillCommonOverlay, theme)
                ).ofModel(),
                new go.Binding('stroke', 'theme', (theme: string) =>
                  this.apmThemeService.getThemeValue(ThemeConstants.colorFillCtaSecondaryHover, theme)
                ).ofModel()
              ),
              go.GraphObject.make(
                go.Panel,
                'Auto',
                {
                  // Setting the padding here allows for padding between the TextBlock and the Shape
                  padding: new go.Margin(8, 12),
                  alignment: go.Spot.Left,
                },
                go.GraphObject.make(
                  go.TextBlock,
                  {
                    width: Constants.TOOLTIP_TEXTBLOCK_WIDTH,
                    alignment: go.Spot.Left,
                    textAlign: 'left',
                  },
                  new go.Binding('stroke', 'theme', (theme: string) =>
                    this.apmThemeService.getThemeValue(ThemeConstants.colorTextCommonInversePrimary, theme)
                  ).ofModel(),
                  new go.Binding('text', 'toolTip', (toolTip?: ToolTipInfo) =>
                    this.tooltipHelperService.getToolTip(toolTip)
                  )
                )
              )
            ),
            mouseHover: (e, obj) => this.tooltipHelperService.loadMeasurementPointPath(e, obj),
          },
          /**
           * Description: In System1 you can configure the measurement view, that can hide/show the Point and Tag names. If `showName` is false in the get-hmi API results, this binding will hide the Point/Tag Names. See the Developer Notes for a visual breakdown.
           * [GitHub Wiki - Grid Node Template]{@link https://github.com/BentlyNevada-bh/s1-hmi-lib/wiki/Developer-Notes#grid-node-template-information}
           *
           */
          new go.Binding('visible', 'showName'),
          go.GraphObject.make(
            go.Shape,
            'lineV',
            {
              row: 0,
              column: 0,
              desiredSize: new go.Size(5, 32),
              alignment: go.Spot.Center,
            },
            new go.Binding('angle', 'probeAngle'),
            new go.Binding('visible', 'showAngle'),
            new go.Binding('stroke', 'foreground', (foreground): go.Brush => {
              const rgbaForeground = this.colorService.convertColorStringTo(ColorFormat.Rgba, foreground);

              /**
              * this is a custom brush that will display half the height of the lineV shape. 
              * This is used to display to probe angle.
              */
              return go.GraphObject.make(go.Brush, 'Linear', {
                0.0: rgbaForeground,
                0.5: rgbaForeground,
                // From 51% to 100% stroke is to be transparent
                0.51: 'rgba(0,0,0,0)',
                1.0: 'rgba(0,0,0,0)',
              })
            }
            )
          ),
          // Circle for Point-Tag Status Indicator
          go.GraphObject.make(
            go.Shape,
            'Circle',
            {
              row: 0,
              column: 0,
              width: 18,
              strokeWidth: 1,
              margin: 8,
            },
            new go.Binding('fill', 'status'),
            new go.Binding('stroke', 'status', (status) => (status !== Constants.TRANSPARENT ? status : 'grey'))
          ),
          // Channel Block
          go.GraphObject.make(
            go.TextBlock,
            new go.Binding('text', 'text'),
            { column: 1, columnSpan: 3, alignment: go.Spot.Left, margin: new go.Margin(0, 0, 0, 2) },
            new go.Binding('stroke', 'foreground', (foreground: string) =>
              this.colorService.convertColorStringTo(ColorFormat.Rgba, foreground)
            ),
            new go.Binding(
              'font',
              'textInfo',
              (textInfo: TextInfo) =>
                `${textInfo.fontStyle} normal ${textInfo.fontWeight} ${textInfo.fontSize}px 'Noto Sans', sans-serif`
            ),
            new go.Binding('isUnderline', 'fontDecoration', (fontDecoration: string) => fontDecoration === 'Underline'),
            new go.Binding('isDeletable', 'editMode').ofModel(),
          )
        ),
        new go.Binding('itemArray', 'measurementInfo'),
        new go.Binding('visible', 'isStubTable', (isStubTable: boolean) => !isStubTable),
        {
          itemTemplate: go.GraphObject.make(
            go.Panel,
            'TableRow',
            {
              toolTip: go.GraphObject.make(
                go.Adornment,
                new go.Binding('visible', 'toolTip', (toolTip?: ToolTipInfo) =>
                  this.tooltipHelperService.showToolTip(toolTip)
                ),
                'Auto',
                go.GraphObject.make(
                  go.Shape,
                  'RoundedRectangle',
                  {
                    // Set fixed width of 250 for the tooltip shape node
                    width: Constants.TOOLTIP_SHAPE_WIDTH,
                  },
                  new go.Binding('fill', 'theme', (theme: string) =>
                    this.apmThemeService.getThemeValue(ThemeConstants.colorFillCommonOverlay, theme)
                  ).ofModel(),
                  new go.Binding('stroke', 'theme', (theme: string) =>
                    this.apmThemeService.getThemeValue(ThemeConstants.colorFillCtaSecondaryHover, theme)
                  ).ofModel()
                ),
                go.GraphObject.make(
                  go.Panel,
                  'Auto',
                  {
                    // Setting the padding here allows for padding between the TextBlock and the Shape
                    padding: new go.Margin(8, 12),
                    alignment: go.Spot.Left,
                  },
                  go.GraphObject.make(
                    go.TextBlock,
                    {
                      width: Constants.TOOLTIP_TEXTBLOCK_WIDTH,
                      alignment: go.Spot.Left,
                      textAlign: 'left',
                    },
                    new go.Binding('stroke', 'theme', (theme: string) =>
                      this.apmThemeService.getThemeValue(ThemeConstants.colorTextCommonInversePrimary, theme)
                    ).ofModel(),
                    new go.Binding('text', 'toolTip', (toolTip?: ToolTipInfo) =>
                      this.tooltipHelperService.getToolTip(toolTip)
                    )
                  )
                )
              ),
              mouseHover: (e, obj) => this.tooltipHelperService.loadMeasurementValuePath(e, obj),
            },
            go.GraphObject.make(go.TextBlock, { column: 0, margin: new go.Margin(4, 0, 0, 0) }),
            // Circle for Measurement Status Indicator
            go.GraphObject.make(
              go.Shape,
              'Circle',
              {
                column: 1,
                desiredSize: new go.Size(10, 10),
                strokeWidth: 1,
              },
              // This binding will show/hide the Measurement Status Indicator. See the Developer Notes for a visual breakdown
              new go.Binding('visible', 'showStatusSample'),
              new go.Binding('fill', 'measurementSeverityColor'),
              new go.Binding('stroke', 'measurementSeverityColor', (status) =>
                status !== Constants.TRANSPARENT ? status : 'grey'
              )
            ),
            go.GraphObject.make(
              go.TextBlock,
              { column: 2, margin: new go.Margin(0, 2, 0, 6), overflow: go.TextBlock.OverflowEllipsis },
              // This binding will show/hide the Measurement Name. See the Developer Notes for a visual breakdown
              new go.Binding('visible', 'showName'),
              new go.Binding('text', 'name'),
              new go.Binding('height', '', (data, shape) => shape.part.data.fontSize * Constants.TEXTBLOCK_ADDITIONAL_HEIGHT),
              new go.Binding('width', '', (data, shape) => shape.part.data.width),
              new go.Binding(
                'font',
                '',
                (data, shape) =>
                  `${shape.part.data.textInfo.fontStyle} normal ${shape.part.data.textInfo.fontWeight} ${shape.part.data.textInfo.fontSize}px 'Noto Sans', sans-serif`
              ),
              new go.Binding('isUnderline', '', (data, shape) => shape.part.data.fontDecoration === 'Underline'),
              new go.Binding('stroke', '', (data, shape) =>
                this.colorService.convertColorStringTo(ColorFormat.Rgba, shape.part.data.foreground)
              )
            ),
            go.GraphObject.make(
              go.TextBlock,
              { column: 3, margin: 0, minSize: new go.Size(22, 10), alignment: go.Spot.Left },
              new go.Binding('text', '', (data) =>
                data.value !== undefined
                  ? `${this.nodeHelperService.formatMeasurementValue(data.value)} ${data.unit} ${data.subUnit}`
                  : `${this.translationService.getText(Constants.No_Data_Text_Id)} ${data.unit} ${data.subUnit}`
              ),
              new go.Binding('height', '', (data, shape) => shape.part.data.fontSize * Constants.TEXTBLOCK_ADDITIONAL_HEIGHT),
              new go.Binding(
                'font',
                '',
                (data, shape) =>
                  `${shape.part.data.textInfo.fontStyle} normal ${shape.part.data.textInfo.fontWeight} ${shape.part.data.textInfo.fontSize}px 'Noto Sans', sans-serif`
              ),
              new go.Binding('isUnderline', '', (data, shape) => shape.part.data.fontDecoration === 'Underline'),
              new go.Binding('stroke', '', (data, shape) =>
                this.colorService.convertColorStringTo(ColorFormat.Rgba, shape.part.data.foreground)
              )
            )
          ),
        }
      ),
      go.GraphObject.make(
        go.Panel,
        'Table',
        go.GraphObject.make(
          go.Panel,
          'TableRow',
          // Placeholder template if isStubTable is true
          {
            background: Constants.STUB_NODE_BACKGROUND_COLOR,
          },
          go.GraphObject.make(go.TextBlock, {
            row: 0,
            column: 0,
            text: 'Measurement Table Placeholder',
            margin: 4,
          })
        ),
        new go.Binding('visible', 'isStubTable', (isStubTable: boolean) => isStubTable)
      )
    );

You shouldn’t be setting skipsUndoManager. It’s just meant for temporarily setting it to true and then setting it back to its previous value.

It should always be the case that node === n, so don’t bother calling findNodeForData.

I tried running your code, but your template is full of uses of …Service which of course I have no idea of how to implement, and it probably doesn’t matter anyway.

Still, if I were you, the first thing I would try is not setting the Diagram.contentAlignment property. Basically that property will cause the diagram to scroll a bit each time the Diagram.documentBounds changes.

I’m not sure if it is helpful, but in some cases, the viewport/document boundaries in fact do update when the node is resized as a result of larger or smaller text.

When you have set Diagram.contentAlignment, it will always try to center the document bounds in the viewport. Changing the size of a node (due to a change in the size of some GraphObject in the Node) will always cause the Diagram.documentBounds to change if the Node was at the boundary of the documentBounds. If you turn off Diagram.contentAlignment, then the automatic alignment won’t happen as nodes change size.

That setting was disabled when making my last post, but there was a zoomToFit call happening elsewhere in the code which was the culprit. Thank you for your help.