Diagram not appearing for Radial Layout in React

Hello,

I have been using React GoJS for a project and need to use the Radial Layout. I mostly followed and modified the gojs-react-basic example in GitHub. I followed the Radial Script for examples. I have copied the RadialLayout.ts into the components folder.

However, when I ran the project, my diagram does not appear and it is completely empty as shown in the screenshot below. All other layouts work so I am not sure why this does not.

Gojs-help

I am not sure why this happens but I suspect it is because I have no root nodes? I have tried to put nodeClicked function into the componentDidMount() with the first nodeDataArray prop item as the node but it does not appear to work. Here is the code I put inside componentDidMount(): nodeClicked(null, this.props.nodeDataArray[0])

I am unable to figure out how to solve these two problems:

  1. How to make Radial Layout appear in my diagram?
  2. How do I make sure that there is already a root node upon loading?

Thank you!

DiagramWrapper.tsx for reference:

import * as go from 'gojs';
import { ReactDiagram } from 'gojs-react';
import * as React from 'react';
import { RadialLayout } from './RadialLayout';

import './Diagram.css';

class CustomRadialLayout extends RadialLayout {
  rotateNode(node: go.Node, angle: number, sweep: number, radius: number) {
    // rotate the nodes and make sure the text is not upside-down
    node.angle = angle;
    const label = node.findObject('TEXTBLOCK');
    if (label !== null) {
      label.angle = ((angle > 90 && angle < 270 || angle < -90) ? 180 : 0);
    }
  }

  commitLayers() {
    // optional: add circles in the background
    // need to remove any old ones first
    const diagram = this.diagram;
    if (diagram === null) return;
    const gridlayer = diagram.findLayer('Grid');
    if (gridlayer === null) return;
    const root = this.root;
    if (root === null) return;
    const circles = new go.Set<go.Part>();
    gridlayer.parts.each(function(circle) {
      if (circle.name === 'CIRCLE') circles.add(circle);
    });
    circles.each(function(circle) {
      diagram.remove(circle);
    });
    // add circles centered at the root
    const $$ = go.GraphObject.make;  // for conciseness in defining templates
    for (let lay = 1; lay <= this.maxLayers; lay++) {
      const radius = lay * this.layerThickness;
      const circle =
        $$(go.Part,
          { name: 'CIRCLE', layerName: 'Grid' },
          { locationSpot: go.Spot.Center, location: root.location },
          $$(go.Shape, 'Circle',
            { width: radius * 2, height: radius * 2 },
            { fill: 'rgba(200,200,200,0.2)', stroke: null }));
      diagram.add(circle);
    }
  }
}
interface DiagramProps {
  nodeDataArray: Array<go.ObjectData>;
  linkDataArray: Array<go.ObjectData>;
  modelData: go.ObjectData;
  skipsDiagramUpdate: boolean;
  onDiagramEvent: (e: go.DiagramEvent) => void;
  onModelChange: (e: go.IncrementalData) => void;
}

function nodeClicked(e: go.InputEvent | null, root: go.GraphObject| null) {
  if (!(root instanceof go.Node)) return;
  const diagram = root.diagram;
  if (diagram === null) return;
  // all other nodes should be visible and use the default category
  diagram.nodes.each(function(n) {
    n.visible = true;
    if (n !== root) n.category = '';
  });
  // make this Node the root
  root.category = 'Root';
  // tell the RadialLayout what the root node should be
  (diagram.layout as RadialLayout).root = root;
  console.log(root);
  diagram.layoutDiagram(true);
}

export class DiagramWrapper extends React.Component<DiagramProps, {}> {
  /**
   * Ref to keep a reference to the Diagram component, which provides access to the GoJS diagram via getDiagram().
   */
  private diagramRef: React.RefObject<ReactDiagram>;

  /** @internal */
  constructor(props: DiagramProps) {
    super(props);
    this.diagramRef = React.createRef();
  }

  /**
   * Get the diagram reference and add any desired diagram listeners.
   * Typically the same function will be used for each listener, with the function using a switch statement to handle the events.
   */
  public componentDidMount() {
    if (!this.diagramRef.current) return;
    const diagram = this.diagramRef.current.getDiagram();
    if (diagram instanceof go.Diagram) {
      diagram.addDiagramListener('ChangedSelection', this.props.onDiagramEvent);
    }
  }

  /**
   * Get the diagram reference and remove listeners that were added during mounting.
   */
  public componentWillUnmount() {
    if (!this.diagramRef.current) return;
    const diagram = this.diagramRef.current.getDiagram();
    if (diagram instanceof go.Diagram) {
      diagram.removeDiagramListener('ChangedSelection', this.props.onDiagramEvent);
    }
  }

  /**
   * Diagram initialization method, which is passed to the ReactDiagram component.
   * This method is responsible for making the diagram and initializing the model, any templates,
   * and maybe doing other initialization tasks like customizing tools.
   * The model's data should not be set here, as the ReactDiagram component handles that.
   */
  private initDiagram(): go.Diagram {
    const $ = go.GraphObject.make;
    // set your license key here before creating the diagram: go.Diagram.licenseKey = "...";
    const diagram =
      $(go.Diagram,
        {
          'undoManager.isEnabled': true,  // must be set to allow for model change listening
          initialAutoScale: go.Diagram.Uniform,
          padding: 10,
          isReadOnly: true,
          layout: $(CustomRadialLayout, { maxLayers: 1 }),
          'animationManager.isEnabled': false,
          model: $(go.GraphLinksModel,
            {
              linkKeyProperty: 'key',  // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
              // positive keys for nodes
              makeUniqueKeyFunction: (m: go.Model, data: any) => {
                let k = data.key || 1;
                while (m.findNodeDataForKey(k)) k++;
                data.key = k;
                return k;
              },
              // negative keys for links
              makeUniqueLinkKeyFunction: (m: go.GraphLinksModel, data: any) => {
                let k = data.key || -1;
                while (m.findLinkDataForKey(k)) k--;
                data.key = k;
                return k;
              }
            })
        });
    // shows when hovering over a node
  const commonToolTip =
  $<go.Adornment>('ToolTip',
    $(go.Panel, 'Vertical',
      { margin: 3 },
      $(go.TextBlock,  // bound to node data
        { margin: 4, font: 'bold 12pt sans-serif' },
        new go.Binding('text')),
      $(go.TextBlock,  // bound to node data
        new go.Binding('text', 'color', function(c) { return 'Color: ' + c; })),
      $(go.TextBlock,  // bound to Adornment because of call to Binding.ofObject
        new go.Binding('text', '', function(ad) { return 'Connections: ' + ad.adornedPart.linksConnected.count; }).ofObject())
    )  // end Vertical Panel
  );  // end Adornment

    // define a simple Node template
    diagram.nodeTemplate =
      $(go.Node, 'Spot',
      {
        locationSpot: go.Spot.Center,
        locationObjectName: 'SHAPE',  // Node.location is the center of the Shape
        selectionAdorned: false,
        doubleClick: nodeClicked,
        toolTip: commonToolTip
      },  // the Shape will go around the TextBlock
        $(go.Shape, 'Circle',
        {
          name: 'SHAPE',
          fill: 'lightgray',  // default value, but also data-bound
          stroke: 'transparent',
          strokeWidth: 2,
          desiredSize: new go.Size(20, 20),
          portId: ''  // so links will go to the shape, not the whole node
        },
        new go.Binding('fill', 'color')),
      $(go.TextBlock,
        {
          name: 'TEXTBLOCK',
          alignment: go.Spot.Right,
          alignmentFocus: go.Spot.Left
        },
        new go.Binding('text').makeTwoWay())
        );
    // this is the root node, at the center of the circular layers
  diagram.nodeTemplateMap.add('Root',
  $(go.Node, 'Auto',
    {
      locationSpot: go.Spot.Center,
      selectionAdorned: false,
      toolTip: commonToolTip
    },
    $(go.Shape, 'Circle',
      { fill: 'white' }),
    $(go.TextBlock,
      { font: 'bold 12pt sans-serif', margin: 5 },
      new go.Binding('text'))
  ));
    // relinking depends on modelData
    diagram.linkTemplate =
    $(go.Link,
      {
        routing: go.Link.Normal,
        curve: go.Link.Bezier,
        selectionAdorned: false,
        layerName: 'Background'
      },
      $(go.Shape,
        {
          stroke: 'black',  // default value, but is data-bound
          strokeWidth: 1
        },
        new go.Binding('stroke', 'color'))
    );
    return diagram;
  }


  public render() {
    return (
      <>
      <ReactDiagram
        ref={this.diagramRef}
        divClassName='diagram-component'
        initDiagram={this.initDiagram}
        nodeDataArray={this.props.nodeDataArray}
        linkDataArray={this.props.linkDataArray}
        modelData={this.props.modelData}
        onModelChange={this.props.onModelChange}
        skipsDiagramUpdate={this.props.skipsDiagramUpdate}
      />
      </>
    );
  }
}

If you don’t provide a RadialLayout.root, it will pick one on its own.

I don’t know why the RadialLayout isn’t running – I assume you set a breakpoint in RadialLayout.doLayout and that you have confirmed that it isn’t called after you have provided the node data and link data Arrays in the props.

I have done some testing and found that doLayout ran twice, once before and once after mounting. They both seem to stop at this if statement if (this.network.vertexes.count === 0) return

Thanks for bringing this to our attention. This is a bug with RadialLayout not properly maintaining it’s network, as you found out. The network should be reset before the doLayout method returns. We’ll update RadialLayout, but for now, you can modify your RadialLayout.ts file with this doLayout method:

  /**
   * Find distances between root and vertexes, and then lay out radially.
   * @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
   */
  public doLayout(coll: go.Diagram | go.Group | go.Iterable<go.Part>): void {
    if (this.network === null) {
      this.network = this.makeNetwork(coll);
    }
    if (this.network.vertexes.count === 0) {
      this.network = null;
      return;
    }

    if (this.root === null) {
      // If no root supplied, choose one without any incoming edges
      const rit = this.network.vertexes.iterator;
      while (rit.next()) {
        const v = rit.value;
        if (v.node !== null && v.sourceEdges.count === 0) {
          this.root = v.node;
          break;
        }
      }
    }
    if (this.root === null && this.network !== null) {
      // If could not find any default root, choose a random one
      const first = this.network.vertexes.first();
      this.root = first === null ? null : first.node;
    }
    if (this.root === null) {  // nothing to do
      this.network = null;
      return;
    }

    const rootvert = this.network.findVertex(this.root) as RadialVertex;
    if (rootvert === null) throw new Error('RadialLayout.root must be a Node in the LayoutNetwork that the RadialLayout is operating on');

    this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
    this.findDistances(rootvert);

    // sort all results into Arrays of RadialVertexes with the same distance
    const verts = [];
    let maxlayer = 0;
    const it = this.network.vertexes.iterator;
    while (it.next()) {
      const vv = it.value as RadialVertex;
      vv.laid = false;
      const layer = vv.distance;
      if (layer === Infinity) continue; // Infinity used as init value (set in findDistances())
      if (layer > maxlayer) maxlayer = layer;
      let layerverts: Array<go.LayoutVertex> = verts[layer];
      if (layerverts === undefined) {
        layerverts = [];
        verts[layer] = layerverts;
      }
      layerverts.push(vv);
    }

    // now recursively position nodes (using radlay1()), starting with the root
    rootvert.centerX = this.arrangementOrigin.x;
    rootvert.centerY = this.arrangementOrigin.y;
    this.radlay1(rootvert, 1, 0, 360);

    // Update the "physical" positions of the nodes and links.
    this.updateParts();
    this.network = null;
  }

Ah, I pasted the code and it works now! Thank you for helping me!