A Guide to Diagram – Part 8 – Diagram Container Groups

So far we've been looking at simple diagrams with a single array of related nodes. Even this simple setup affords many possibilities for representing data in interesting ways. We can, however, add a whole new dimension to the diagram using containers.  

Container Grouping Data Model

As the name suggests we need to add some form of grouping relationship into the data mode.  The approach that is taken is one of a hierarchical relationship between nodes.  Thus nodes that share the same parent are grouped together logically under that parent. 
For the sake of illustration, let's go back to the running example that we've been working with so far and add an optional attribute that will reference the node ID of a node's parent. I've also added a new constructor to create a parented node: 
public class SampleNode {
  private int _uniqueNodeId;
  private String _nodeLabel;
  private int _preferredColumn;
  private int _parentNodeId = -1; //-1 means no parent in this case
  public SampleNode(int id, String label, int column) {
    _uniqueNodeId = id;
    _nodeLabel = label;
    _preferredColumn = column;
  }
  public SampleNode(int id, String label, int column, int parent) {
    this(id, label, column);
    _parentNodeId = parent;
  }
  public int getUniqueNodeId() {
    return _uniqueNodeId;
  }
  public String getNodeLabel() {
    return _nodeLabel;
  }
  public int getPreferredColumn() {
    return _preferredColumn;
  }
  public int getParentNodeId() {
    return _parentNodeId;
  }
}
And change the main model class to add some parented nodes using the new constructor. 
public DiagramDataModel(){
  _nodes = new ArrayList<SampleNode>(10);
  _nodes.add(new SampleNode(0,"First Node",0));
  _nodes.add(new SampleNode(1,"Second Node",0));
  _nodes.add(new SampleNode(2,"Third Node",2));
  _nodes.add(new SampleNode(3,"Fourth Node",2));
  _nodes.add(new SampleNode(4,"Fifth Node",1)); 
  //Add some child nodes for Second Node and Forth Node
  _nodes.add(new SampleNode(5,"Second Node Child 1",0,1));
  _nodes.add(new SampleNode(6,"Second Node Child 2",0,1));
  _nodes.add(new SampleNode(7,"Fourth Node Child 1",0,3));
  _nodes.add(new SampleNode(8,"Fourth Node Child 2",0,3));
  _nodes.add(new SampleNode(9,"Fourth Node Child 3",0,3));
  _links = new ArrayList<SampleLink>(8);
  _links.add(new SampleLink(0,1,1,"PLANNED","Alpha"));
  _links.add(new SampleLink(1,2,1,"PLANNED","Beta"));
  _links.add(new SampleLink(2,3,1,"PLANNED","Gamma"));  
  _links.add(new SampleLink(3,4,1,"PLANNED","Delta"));  
  _links.add(new SampleLink(4,0,1,"COMPLETE","Epsilon"));  
  //and links between the new child nodes
  _links.add(new SampleLink(5,6,1,"PLANNED","Group 1 A"));  
  _links.add(new SampleLink(7,8,1,"PLANNED","Group 2 A"));  
  _links.add(new SampleLink(8,9,1,"PLANNED","Group 2 B"));    
}
Having made that change, if I run the diagram again I just see the new nodes laid out in the first column below the other nodes:
Diagram with new nodes added

The Grouping UI

So let's actually take advantage of the new parent / child structure in the UI.  To do this, all we need to do is set the containerId attribute on the <dvt:diagramNode> to be a non-null value which the diagram will use to locate the parent. Here's the amended node definition:
<dvt:diagramNode id="dn1" 
                 nodeId="#{node.uniqueNodeId}"  
                 containerId="#{node.parentNodeId > -1?node.parentNodeId:null}"
                 label="#{node.nodeLabel}" >
  <f:facet name="zoom100">
    <dvt:marker fillColor="#ff0000" 
                height="80" width="80" shape="circle" 
                borderColor="#000000" borderStyle="solid" 
                borderWidth="1.0" />
  </f:facet>
</dvt:diagramNode>
Note how I'm using a ternary expression in the containerId to only use it when there is a valid parent. You can use this EL based approach or, create a separate node definition to apply to containees controlled by the rendered property.
With the containership established the diagram looks a little different, you can see in the screenshot below how nodes "Second Node" and "Fourth Node" have gained the expand icon (highlighted with an arrow):
Now with unexpanded nodes
The user can then click the plus icon to expand the group in place:
Here's an expanded container node
Note the following:
  1. When the node ("Second node") is expanded we now see the containees, and the second node is now displayed as a simple box with a close indicator in the top left corner
  2. The layout of the diagram as a whole has changed slightly. Notice that the gap between the node and label for "First Node" is much larger.  This is an artifact of the layout using the widest node metrics to determine this spacing.  The widest node is now the expanded "Second Node". So if your diagram will include groups then you need to consider this.
  3. Because we calculated the endpoints of the links in the layout based on the source and destination nodes, the link between the first and second nodes is still anchored correctly to the middle of the opposing edges, even though it means that the orientation of the link is no longer vertical.

But I Don't Like / Want that Plus (Expand) Symbol!

The icon that the diagram puts into the top left of the node to expand the container is an optional thing.  You can suppress its display using the showDisclose property on the diagramNode:
  showDisclose="false"
 When the expand symbol has been hidden using this option, the user can still drill into the node using the drill icon that appears when you click the node, although that will reduce the display to just the focused container and hide the other nodes.  (You can also hide this drill behavior by using the showDrill property)
You can also programmatically control the disclosure of the node using the disclosedRowKeys property of the diagramNodes tag. So, for example, you might want all of the containers to start out expanded rather than closed, or you might want your to use your own gestures to control expanding and collapsing rather than the default expand icon.  The disclosedRowKeys collection makes all this possible.

Group Specific Facets

Going back to the article on node contents, I mentioned  that there were some container related facets. So now is a good time to cover those.

Background Facet

Although the background facet can be used for any normal node, it more usually comes into play in the context of a container. It's job is to fill the background on the container node when expanded. 
In this example I have assigned a background facet thus:
<f:facet name="background">
  <dvt:marker fillColor="#FF0000" opacity="0.1" 
              height="80" width="80" 
              shape="circle" borderStyle="none"/>
</f:facet>
Normally this would be hidden behind the Zoom100 marker, however, when the container is expanded this is the effect:
Setting the background on an expanded node
Note how I've reduced the opacity on the background marker to ensure that the child nodes remain readable. Notice, as well, how the marker SVG is stretched to completely fill the container. If you need padding, then you'll have to include that into the SVG image you use.

Container Background Styling 

I'll note at this point, that you do not have to use the background facet to control how the body of an expanded container looks. You can also apply basic CSS style information using  the containerStyle property of the diagramNode. So if all you want to do is color the background of the container, this would be the approach to take. If you have suppressed the default disclosure mechanism using showDisclose=false, then you will not get the default boarder around the whole node and so the containerStyle will allow you to set your own border styling in this case as well.

containerTop/Bottom/Start/End/Left/Right Facets

This set of facets all do the same thing, allowing you to define more content inside of the expanded group container in the assigned position.  Within these facets you can place the same types of content as you might use in a zoom facet. 
So for example I could do something like this:
<f:facet name="containerTop">
  <af:panelGroupLayout layout="horizontal" valign="middle">
    <af:spacer width="25"/>
    <dvt:marker fillColor="#FF0000" 
                height="20" width="20" 
                shape="circle" borderStyle="none" />
    <af:spacer width="10"/>         
    <af:outputText value="#{node.nodeLabel}"/>
  </af:panelGroupLayout>
</f:facet>
This results in a sort of "header" to be shown in the expanded node, but normally invisible:
TopContent facet on a container
The actual "content" area of the container node is separate from the container layout facets so the child nodes will not overlap anything that you place on them.  These layout facets do, however, overlay the content of the background facet (which will always fill the node). 

Container Nodes and Layouts

In the screenshots above we see that the child nodes within the container are laid out vertically, just like the top level nodes in the diagram.  This is the default behavior, however, it does not have to be this way. 
When we first started looking at this sample (In Part 1 of the series), I alluded to the fact that you could define more than one clientLayout tag within the <dvt:diagram>.  The top level diagram tag only has a single layout attribute which will define this overall default, however, each <dvt:diagramNode> tag also has a layout attribute that can be used to specify that a particular layout should be used if this node happens to end up being an expanded container. 

To illustrate this I've created a second horizontal linear layout and added that to the LearningDiagramLayouts script. This is then assigned to the layout property of the stamped diagramNode. So we now have two layouts defined, with learningVerticalInColumns being applied to the diagram as a whole and learningHorizontal being applied to any expanded container nodes:

<dvt:diagram id="d1" summary="Example Diagram"
             layout="learningVerticalInColumns">
  <dvt:clientLayout name="learningVerticalInColumns"
                    method="LearningDiagramLayouts.columnVertical"
                    featureName="LearningDiagramLayouts">
    <f:attribute name="columns" value="2"/>
    <f:attribute name="columnWidth" value="50"/>
    <dvt:nodeAttributes node="dn1">
      <f:attribute name="displayColumn" 
                   value="#{'preferredColumn'}"/>
    </dvt:nodeAttributes>
  </dvt:clientLayout>     
  <dvt:clientLayout name="learningHorizontal" 
                    method="LearningDiagramLayouts.simpleHorizontal"
                    featureName="LearningDiagramLayouts"/>
  <dvt:diagramNodes id="dns1" 
                    value="#{diagramModel.nodes}" var="node">
    <dvt:diagramNode id="dn1" nodeId="#{node.uniqueNodeId}"  
                     containerId="#{node.parentNodeId > -1?node.parentNodeId:null}"
                     label="#{node.nodeLabel}" 
                     layout="learningHorizontal">
...
Here's the result:
Diagram with two layouts in play

How Container Layouts are Processed

In the example above, we have two layout functions and, as the diagram is rendered, the correct function will be called to layout each container. This means that the simpleHorizontal function layout will be called twice to sort out the content nodes of Second Node and Fourth Node respectively and then columnVertical function will be called to layout the top level diagram.
It's important to understand that each layout is called in isolation and the list of links and nodes passed by the layoutContext; getNodeCount(), getLinkCount(), getNodeByIndex() and getLinkByIndex(), is confined to those specific to this container. Because the innermost layouts are done first, the node metrics that the container node receives will reflect the space allocated to correctly layout the containee nodes. 

Inter-Layout Links

The final twist with containers and layouts is the question of how to handle links that join nodes that are not peers in the same container, i.e. those that cross container boundaries.  The layout code that we have dealt with so far has all be relative to the container being laid out, so APIs like getPosition() on the node return a DvtPoint object that returns those relative coordinates. The solution to this problem lies with the localToGlobal() API on the layoutContext.  This function takes a local coordinate in the context of a particular node and returns a global coordinate value that, when used in the context of the base relative coordinate system, will correctly transform the target point when the layout is complete. To help you to understand if you need to use this API, you can call the getContainerId() API on the node for both the start and end nodes to see if the two nodes are being handled by separate layouts. 

In the Next Article

Now that we have covered groups of nodes in containers we will finish off working with groups of nodes by looking at attributeGroups and the node stacking functionality. 

Comments:

Post a Comment:
  • HTML Syntax: NOT allowed
About

Oracle Data Visualizations provide a broad range of beautiful, interactive components for viewing and understanding data. This blog covers topics on the new features in Oracle Data Visualization components and how-to articles on advanced functionality.

Search

Categories
Archives
« April 2015
SunMonTueWedThuFriSat
   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  
       
Today