Product Ontology view with RadialLayout suggestions

Hi Walter,
firstly thanks for all the replies to the RadialLayout queries over the span of 3 months. The end game for using GoJS in the project I am working for is providing the end users a visualization for products where the Backend provides data based on Web Ontology.

For instance, when a user inputs HighChair as a product of interest; through a binary tree structure I generate the RadialLayout which provides the following diagram:

the green nodes are attributes that provide filtering options (if any) for the HighChair. While as Red ones, are attributes that provide further options into the attribute itself. For example, Manufacturer has CompanyName, ContactPerson etc.

These properties can be as big as several Hundreds of Layers if we decide to send the complete data and try to show it in the layout. For instance, Legislation can be for HighChair itself, and also for the Manufacturer. This will traverse through a lot of layers if we have a concrete example (which I do not have as such since I am just conceptualizing it)

I am very limited to space on the webpage and a maximum of 3 Layers are what I consider it to be optimal. For example If the User clicks on Manufacturer -> Legislation the following result is produced.

Comparing it to main diagram, I hid all the other nodes and showed the further path to Legislation which has an attribute hasLegislationName (this is the current implementation. This can as well be another red node indicating that the user needs to traverse through more layers to reach some particular attribute of interest)

Interest

At this point I would like to have your advice as to how can I proceed further with the visualization of further layers given the constraint that I can only show 3.

A preferable implementation would be to hide or remove the Manufacturer and the dashed link to Legislation and replace the Legislation Node and its child (hasLegislationName) to the position of Manufacturer i.e.

Current

HighChair - dashedline -> Manufacturer - dashedline -> Legislation - solidLine -> hasLegislationName

Desired

HighChair - dashedLine -> Legislation - solidLine -> hasLegislationName

(in a nutshell, move the child node to the parent’s position)

which brings me to the question:

Is this even possible?

However, thinking over the same, what if the user actually wants to go to the Manufacturer node and click on some other attributes which are for the above example hidden (for e.g. ContactPerson for the Manufacturer)

So hiding or remove the parent node itself is kind of a bad decision in my option.

Any insights as to how you would go further with the diagram ?

Transitions

As mentioned in the RadialLayout example transistions can make sense in the scenario where the user wishes to go back to the previous node. I tried some hacking here are there and an example is shown below:

At this point, how much can transitions work in my favor when the user is already 5 to 6 layers deep keeping in mind that there are still selected nodes from the first layer. for e.g. green node of hasHeight, hasWidth still remains selected and still shown in the same diagram?

Apologies for the lengthy post and no TL:DR.

Thanks.
Shan

Since text seems to be an important part of what you want to display, I would think you would want to keep all of the text horizontal. Are you sure that you want to use RadialLayout at all? A regular horizontal TreeLayout would show the same information more compactly and legibly.

As far as trying to show a subset of the complete graph, have you seen this sample: Local View ? That sample does show the “whole” graph in the upper diagram, which you cannot do, but you could show the context there and the details in the lower diagram.

I appreciate long posts when they are thoughtful, as this was.

As much as I would like to look into other visualization methods, I am current bound to the radial layout for now.

I am still trying to find a way to move the Legislation node and its child a level above i.e. to the Manufacturer node’s position. Do you think that is possible? Maybe create a transition like that in the RadialLayout Example but the node moves one circular layer above not till the root.

Try with Part.move

i store the Manufacturer location in a variable and then try to move Legislation node to its location by

   cNode.move(pNodeLocation);

but the node does not move.

Did you modify RadialLayout to do your extra moves? By default each layout will result in an animation of the nodes’ positions.

When do you call cNode.move(...)?

i left the transistions for now. Forget about it.

  1. I already create the graph

     let recApproach = new RecClass();
     recApproach.generateGraphRecApproach(this.config, this.myDiagram, this.$, lay);
    
  2. I perform hiding of non necessary nodes (Shouldnt be much of interest)

          /*
            perform node hiding here...
          */
    
     let objJson = this.config['objectproperties'];
     for (let eachKey in objJson) {
         if (objJson.hasOwnProperty(eachKey)) {
             if (objJson[eachKey]['concept']['translatedURL'] === pNode) {// find the parent
                 for (let everyObjKeyWithin in objJson[eachKey]['objectproperties']) {
                     // hide object properties first
                     if (objJson[eachKey]['objectproperties'].hasOwnProperty(everyObjKeyWithin)) {
                         if (objJson[eachKey]['objectproperties'][everyObjKeyWithin]['concept']['isHidden']) {
                             for (let itr = this.myDiagram.nodes; itr.next(); ) {
                                 let particularNode = itr.value;
                                 if (particularNode.data.text === objJson[eachKey]['objectproperties'][everyObjKeyWithin]
                                         ['concept']['translatedURL']) {
                                     particularNode.visible = false;
                                     // console.log(particularNode.data.text, 'isHidden');
                                 }
                             }
                         }
                     }
                 }
                 for (let everyDatProp of objJson[eachKey]['dataproperties']) { // hide the data properties..
                     if (everyDatProp['isHidden']) {
                         for (let dataPItr = this.myDiagram.nodes; dataPItr.next(); ) {
                             let particularDatNode = dataPItr.value;
                             if (particularDatNode.data.text === everyDatProp['translatedURL']) {
                                 particularDatNode.visible = false;
                                 // console.log(particularDatNode.data.text, 'DatProp isHidden');
                             }
                         }
                     }
                 }
    
  3. Make the Parent Opacity zero with iteration…

                 let pNodeKey: number;
                 let pNodeLocation: go.Point;
                 let chNode: go.Node;
                 if (objJson[eachKey]['concept']['isHidden']) {
                     let root = this.myDiagram.findNodeForKey(1);
                     this.myDiagram.nodes.each(n => {
                         if (n.data.text === pNode) { // should not show parent node as of now
                             n.opacity = 0;
                             pNodeKey = n.data.key;
                             pNodeLocation = n.part.location;
                             /*let ndgm = n.diagram; // hack for silly transitions
                             ndgm.layout.root = n; */
    
                         }
                     });
                     root.findLinksOutOf().each(link => { // add dash links 
                         if (link.toNode.data.text === pNode) {
                             link.path.strokeDashArray = [4, 4];
                         }
                     });
                     let parentNodeEntity = this.myDiagram.findNodeForKey(pNodeKey);
                     parentNodeEntity.findLinksOutOf().each( linkFromParent => { // add dash links
                         if (linkFromParent.toNode.isVisible()) {
                             linkFromParent.path.strokeDashArray = [4, 4];
                         }
                     });
    
  4. Trying to move the child Legislation and its child to the position to Manufacturer aka. Parent of the node

                     /*
                         parentNodeEntity.findTreeChildrenNodes().each(cNode => {
                         if (cNode.isVisible()) { // within the parent node if the child is visible
                             chNode = this.myDiagram.findNodeForKey(cNode.data.key);
                             chNode.part.move(pNodeLocation); // move it to the parent's location
                         }
                     });*/
                     this.myDiagram.model.setDataProperty(root, 'strokeDashArray', [4, 4]);
                     this.myDiagram.model.setDataProperty(parentNodeEntity, 'strokeDashArray', [4, 4]);
                 }
             }
         }
       }
     }
    
  5. I also tried

     `this.myDiagram.model.setDataProperty(chNode, 'location', pNodeLocation);`
    

with other model calls but no luck.

TL:DR

Trying to do:

Override RadialLayout.makeNetwork to call the super method and then modify the resulting LayoutNetwork to excise the “Manufacturer” LayoutVertex, having the LayoutEdge going from “High Chair” to “Legislation”. Then move the “Manufacturer” Node to be where it would have been and make it not visible.

don’t you mean doLayout() public method in the RadialLayout class you sent me?

'use strict';
/*
*  Copyright (C) 1998-2017 by Northwoods Software Corporation. All Rights Reserved.
*/

import * as go from 'gojs';

/**
* Given a root Node this arranges connected nodes in concentric rings,
* layered by the minimum link distance from the root.
*/
export class RadialLayout extends go.Layout {
  private _root: go.Node = null;
  private _layerThickness: number = 100;  // how thick each ring should be
  private _maxLayers: number = Infinity;

  /**
  * Copies properties to a cloned Layout.
  */
  protected cloneProtected(copy: any) {
    super.cloneProtected(copy);
    // don't copy .root
    copy._layerThickness = this._layerThickness;
    copy._maxLayers = this._maxLayers;
  }

  /*
  * The Node to act as the root or central node of the radial layout.
  */
  get root(): go.Node { return this._root; }
  set root(value: go.Node) {
    if (this._root !== value) {
      this._root = value;
      this.invalidateLayout();
    }
  }

  /*
  * The thickness of each ring representing a layer.
  */
  get layerThickness(): number { return this._layerThickness; }
  set layerThickness(value: number) {
    if (this._layerThickness !== value) {
      this._layerThickness = value;
      this.invalidateLayout();
    }
  }

  /*
  * The maximum number of layers to be shown, in addition to the root node at layer zero.
  * The default value is Infinity.
  */
  get maxLayers(): number { return this._maxLayers; }
  set maxLayers(value: number) {
    if (this._maxLayers !== value) {
      this._maxLayers = value;
      this.invalidateLayout();
    }
  }

  /**
  * Use a LayoutNetwork that always creates RadialVertexes.
  */
  public createNetwork () {
    let net = new go.LayoutNetwork();
    net.createVertex = () => new RadialVertex();
    return net;
  }

  /**
  */
  public doLayout(coll: go.Diagram|go.Group|go.Iterable<go.Part>) {
    if (this.network === null) {
      this.network = this.makeNetwork(coll);
    }

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

    let 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
    let verts: any[] = [];
    let maxlayer = 0;
    let it = this.network.vertexes.iterator;
    while (it.next()) {
      let vv = it.value as RadialVertex;
      vv.laid = false;
      let layer = vv.distance;
      if (layer === Infinity) {continue; } // Infinity used as init value (set in findDistances())
      if (layer > maxlayer) { maxlayer = layer; }
      let layerverts: any = 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;
  }

  /**
  * recursively position vertexes in a radial layout
  */
  private radlay1(vert: RadialVertex, layer: number, angle: number, sweep: number) {
    if (layer > this.maxLayers) {return; } // no need to position nodes outside of maxLayers
    let verts: any[] = []; // array of all RadialVertexes connected to 'vert' in layer 'layer'
    vert.vertexes.each(function(v: RadialVertex) {
      if (v.laid) {return; }
      if (v.distance === layer) {verts.push(v); }
    });
    let found = verts.length;
    if (found === 0) {return; }

    let radius = layer * this.layerThickness;
    let separator = sweep / found; // distance between nodes in their sweep portion
    let start = angle - sweep / 2 + separator / 2;
    // for each vertex in this layer, place it in its correct layer and position
    for (let i = 0; i < found; i++) {
      let v = verts[i];
      let a = start + i * separator; // the angle to rotate the node to
      if (a < 0) {a += 360; } else if (a > 360) {a -= 360; }
      // the point to place the node at -- this corresponds with the layer the node is in
      // all nodes in the same layer are placed at a constant point, then rotated accordingly
      let p = new go.Point(radius, 0);
      p.rotate(a);
      v.centerX = p.x + this.arrangementOrigin.x;
      v.centerY = p.y + this.arrangementOrigin.y;
      v.laid = true;
      v.angle = a;
      v.sweep = separator;
      v.radius = radius;
      // keep going for all layers
      this.radlay1(v, layer + 1, a, sweep / found);
    }
  }

  /**
  * Update RadialVertex.distance for every vertex.
  */
  private findDistances(source: RadialVertex) {
    let diagram = this.diagram;
    // keep track of distances from the source node
    this.network.vertexes.each(function(v: RadialVertex) { v.distance = Infinity; });
    // the source node starts with distance 0
    source.distance = 0;
    // keep track of nodes for we have set a non-Infinity distance,
    // but which we have not yet finished examining
    let seen = new go.Set(RadialVertex);
    seen.add(source);

    // local function for finding a vertex with the smallest distance in a given collection
    function leastVertex(coll: any) {
      let bestdist = Infinity;
      let bestvert: any = null;
      let it = coll.iterator;
      while (it.next()) {
        let v = it.value;
        let dist = v.distance;
        if (dist < bestdist) {
          bestdist = dist;
          bestvert = v;
        }
      }
      return bestvert;
    }

    // keep track of vertexes we have finished examining;
    // this avoids unnecessary traversals and helps keep the SEEN collection small
    let finished = new go.Set(RadialVertex);
    while (seen.count > 0) {
      // look at the unfinished vertex with the shortest distance so far
      let least = leastVertex(seen);
      let leastdist = least.distance;
      // by the end of this loop we will have finished examining this LEAST vertex
      seen.remove(least);
      finished.add(least);
      // look at all edges connected with this vertex
      least.edges.each(function(e: any) {
        let neighbor = e.getOtherVertex(least);
        // skip vertexes that we have finished
        if (finished.contains(neighbor)) {return; }
        let neighbordist = neighbor.distance;
        // assume "distance" along a link is unitary, but could be any non-negative number.
        let dist = leastdist + 1;
        if (dist < neighbordist) {
          // if haven't seen that vertex before, add it to the SEEN collection
          if (neighbordist === Infinity) {
            seen.add(neighbor);
          }
          // record the new best distance so far to that node
          neighbor.distance = dist;
        }
      });
    }
  }

  /**
  * This override positions each Node and also calls {@link #rotateNode}.
  */
  commitLayout() {
    super.commitLayout();

    let it = this.network.vertexes.iterator;
    while (it.next()) {
      let v = it.value as RadialVertex;
      let n = v.node;
      if (n !== null) {
        n.visible = (v.distance <= this.maxLayers);
        this.rotateNode(n, v.angle, v.sweep, v.radius);
      }
    }

    this.commitLayers();
  }

  /**
  * Override this method in order to modify each node as it is laid out.
  * By default this method does nothing.
  */
  rotateNode(node: go.Node, angle: number, sweep: number, radius: number) {
  }

  /**
  * Override this method in order to create background circles indicating the layers of the radial layout.
  * By default this method does nothing.
  */
  commitLayers() {
  }
} // end RadialLayout


/**
* @ignore
* @constructor
* @extends LayoutVertex
* @class
*/
class RadialVertex extends go.LayoutVertex {
  distance: number = Infinity;  // number of layers from the root, non-negative integers
  laid: boolean = false;  // used internally to keep track
  angle: number = 0;  // the direction at which the node is placed relative to the root node
  sweep: number = 0;  // the angle subtended by the vertex
  radius: number = 0;  // the inner radius of the layer containing this vertex
}

No, I meant Layout.makeNetwork. Layout | GoJS API

Those Layouts that use LayoutNetworks and LayoutVertexes and LayoutEdges do so for several reasons. One reason is that it supports an abstraction of the graph that is formed by the actual Nodes and Links. Using a LayoutNetwork allows a layout to operate on a different graph, pretending as if some Nodes and Links don’t exist, and adding pretend or dummy vertexes and edges, in order to produce certain layout effects. You’ll find several examples of this in the samples or extensions folders.

I’m not sure I understand exactly what you want to do, but it seemed like pretending a Node and its two connected Links didn’t exist, replacing them by a new edge, is what would solve your problem for you. That would cause all of the Nodes to be positioned as you had imagined. Then you need to deal with the Node that you are pretending does not exist – either by making it not visible or by making that Node and its two connected Links transparent.

Since I am on time constraints, I mades hacks in the back end data to forcefully remove the Manufacturer Node and display the desired outcome

Unfortunately, I need to redraw the figure completely, losing all the selections made previously.

I am thinking if I can add the completely new nodeDataArray & linkDataArray without rerendering the diagram using model.addLinkCollection and model.addNodeCollection?