Hierarchical Headers

How to manage advanced column headers?

 

 

A very common task in user applications is to display data coming from relational database tables. Such table-displays are very well suited to show the data, but suffer limitations when it comes to advanced presentation and complex header management which is normally required in today's business-oriented GUIs.

This paper describes a solution that shows how to move columns around and organize structured headers using the advanced capabilities of the JSmartGrid component.

Introduction

Displaying data from a database is a very common action and is usually done through a table component such as the Swing JTable from Sun Microsystems. This type of component is well suited for standard and straightforward data display, but its one drawback is that it does not allow for advanced column header organization.

For example, the following screenshots show how we can easily create column header names using the Meta-model of a SQL query (see JSmartGrid tutorial: HeaderGridDemo):

Picture 1

We would like to see:

Picture 2

and we may want to allow the user to interact with the data and change the displayed column order as shown below :

Picture 3

 

As you notice in the example, we can build a hierarchical header structure, grouping the columns by common theme, and simplifying or translating the names of the columns for improved user readability. Moreover, we are able to interact with the display to modify the way the information is seen.

In this document we focus on the following topics :

 

 

Topics

Introduction

1. Concepts

  1.1 Definitions
  1.2 Why use a Grid?
 

2. Application Architecture

  2.1 Context
  2.2 Header Model
  2.3 Spanning of cells
  2.3.1 Span Models
  2.3.2 Choice of Span Model
  2.4 Caching Mechanism
 

3. Implementation

  3.1 Creation of a Tree Data Model
  3.2 Creation of Header data structures
  3.3 Spanned Area Caching Mechanism
  3.3.1 Matrix Cell Model
  3.3.2 Implementation of Matrix Cell Model
  3.3.3 Algorithm to obtain Spanned Areas from the Tree Data Model
  3.3.4 Cache Matrix
  3.4 Column Header
  3.4.1 Header GridModel
  3.4.2 Header SpanModel
  3.5 Dragging of Header
  3.6 Customizing the Column Header Style Model and Renderer
 

4. Annex

  4.1 Try it !
  4.2 Project Files
  4.3 What is the JSmartGrid?
 

 

 

Concepts

1.1 Definitions

In this document, we deal with Headers that can be used to display either the name of a single column or the title given to a set of columns. A Hierarchy of headers is built with sets of column headers that belong to a group with a common name. This hierarchy can be drawn as a tree structure such as the one you usually find in a file browser.

The example in this document tries to keep the tree structure unchanged, but allows a header or a group of headers to be moved inside the tree. We implement this example using a grid data structure.

A Grid is a two-dimensional matrix of cells defined by rulers. A Ruler is a set of elements, called columns or rows according to its orientation, that handle the position and size of each element. The intersection of these Rulers is a Cell, an abstract object that contains any type of data value. In the Grid, a Cell is the atomic unit that can be selected and given a renderer and an editor.

We can merge of a rectangular set of Cells that we call a Spanned Cell. The Span is defined for the top-left cell of the rectangle. This notion is associated with the Cell concept in a Grid.

1.2 Why use a Grid?

A Grid allows the developer to use the cells independently and, consequently, to build rich header display structures. 

The JSmartGrid, the Eliad Technologies, Inc. grid component, is very similar to the Swing JTable. They are both components working with Java Swing, but the JSmartGrid has fine grain features, such as models, allowing the developer to implement new specifications. In this example, we design specific Data and Span models that allow columns to be moved, or dragged, in the grid. For more information you can refer to § 4.3 What is JSmartGrid.

Dragging in the JSmartGrid means moving a rule marker in  any Horizontal or Vertical direction (see JSmartGrid JavaDoc: com.eliad.model.RulerModel and  com.eliad.util.RulerConstants). Dragging provides a mechanism that enables the user to have continuous feedback when using an appropriate input device, such as a mouse. Dragging also enables the facilities to provide any negotiation, such as to accept or refuse the movement of a column, according to the specific constraints of your application (see JSmartGrid JavaDoc: VetoableProperty or Constraints bound property, and com.eliad.model.RulerModelListener#beforeItemsMoved).

In our example, we allow a node to be moved in the hierarchy as long as each node keeps the same depth and the same ancestor.

 

 

 

Application Architecture

2.1 Context

Because we don't want to emphasize the data structure involved in the JSmartGrid Model, we use a very simple array-structured Vector. This Vector is loaded from a database through a JdbcAdapter and a SQL select (This will not be described in further detail in the document).

This chapter deals with the Hierarchical Column Headers data structures, the Span and Data Models, which are used in this example. Although these models are applied only to columns, they are generic and can also be applied to rows.

We set our Header hierarchical model in the JSmartGrid using the API setColumnHeader() method. This method assigns a JSmartGrid in column header position in the JScrollPane container (if it exists).

 

The JSmartGrid derives both the HeaderGridModel and HierarchicalColumnHeaderModel from the GridModel, thus our set of headers can be considered as a JSmartGrid itself and inherit all of its facilities.

The performance aspect of the example is dealt with in section 2.4 that describes the caching mechanism that we included.

To use special objects or to obtain special effects, an appropriated GridCellRenderer should be implemented.

Now we'll focus on the main application modules that can be found in the following diagram.

Picture 3: Program Modules Diagram

2.2 Header Model

To create a flexible Hierarchical Headers Data Model that is able to manipulate n-ary tree, we use the DefaultMutableTreeNode class which is used as a Tree Data Model for the Swing JTree.

An n-ary tree means that each node can have 0 to n children. The well-known Binary Tree is a 0 or 2 children Tree. A node without children is called Leaf Child. We can also call each parent a "title" and each child a "subtitle" in our hierarchical column header or theme-grouped column header.

Column headers can be shown with a tree model, in either a vertical or horizontal hierarchical format.

 

Picture 4

Any object can be put in the column tree nodes or Tree Data Model. Each tree node is an instance of DefaultMutableTreeNode. When a node has no parent, that means it is the one and only root of the tree.

2.3 Spanning of cells

The SpanModel is the model of the merged cells. The purpose of this model is to handle some questions like "Is a cell spanned?" or "What's the size of the span?".

The span management in the JSmartGrid makes extensive use of the ExtentCell notion which embodies the idea of the extension of a cell over the "following" cells, that is the cells to the right and below.

int  getColumn() Returns the column of the anchor of this span;
int getColumnCount() Returns the number of columns taken by the span;
int getRow() Returns the row of the anchor of this span;
int getRowCount() Returns the number of rows taken by the span;
Object getIdentifier() Returns the identifier object for this cell.

You will find useful information concerning SpanModels in JSmartGrid JavaDoc.

2.3.1 Span Models

Here we describe the three solutions you have to implement Spanning, as well as our recommendation for this specific example.

DirectSpanModel Should be implemented by SpanModels that are able to answer a question about the presence of a span at some point in O(1) time, using some procedural rules. They do not need to maintain data structures to know where the spans are. 
This interface defines two methods :
ExtentCell getSpanOver(row, column)
Obtains a Rectangle representing the bounds of the span which overlaps the cell at (row, column).
boolean isEmpty()
Returns true if there is no span at all in the model.

If there is no span at (row, column) coordinates, getSpanOver() returns a null value instead of an ExtentCell.
QuerySpanModel Should be implemented by SpanModels that are able to answer a question about the presence of a span at some point. They are not quite as efficient as DirectSpanModel, but still have better access time than O(n), such as O(log n) for example. They usually implement simple data structures to keep some calculated information.
CollectionSpanModel This interface should be implemented by interfaces for which it is easier to list the spans rather than calculate them. It defines the method Iterator() which returns an enumeration of all spanned area. If this interface isn't implemented in an appropriated model, it would be a high cost method for Spanning.

When you already know the spanned areas, the simplest solution is often QuerySpanModel and the most efficient is DirectSpanModel. As the features of your program evolve and improve, you will soon face performance issues which will require you to develop data-structures and algorithms keeping with the QuerySpanModel efficiency level.

2.3.2 Choice of Span Model

When you drag a spanned column like [Order] or [Product], it moves all the children columns (hierarchy of column headers). With a static implementation of the SpanModel, you would lose the structure of the tree and consequently lose the spanned areas. We implement a cache mechanism for the spanned areas and we are able to stay with a DirectSpanModel performance level.

2.4 Caching Mechanism

This mechanism is involved in the performance aspects of JSmartGrid. Accessing data is easy in an array, but becomes more difficult with increased complexity in the data-structure. The direct approach, with no caching, implies that we need to search the whole tree each time we need to know if there is a span over a cell.

To show how to improve timing aspects, we make an advance calculation of all Spanned areas. We use a cache matrix and parse the tree nodes once, creating references to them at appropriate coordinates in a matrix. All calculations are done during the first tree node parsing and then are stored in a matrix. The cache is then recalculated each time the tree is modified.

It is not necessary to save the spanned area information in the nodes of the tree because the parsing function stores this information in another data-structure. This parsing function initializes the column "titles" with visible data, like String or Icon, and creates a cache matrix which will return an ExtentCell for Column Header Spanned Area. The implementation of this cache matrix is shown further at § 3.3 Spanned Area Caching Mechanism.

 

 

 

Implementation

In this chapter we deal with the implementation and examples of code. We'll describe :

3.1 Creation of a Tree Data Model

Here we want to create the hierarchical headers data structure in the same way that we create a classic JTree Data Model. Each tree node is an instance of DefaultMutableTreeNode and may become the parent of another node using the add method.

The code to create the tree shown in picture 2, "Hierarchical Headers", is :

     
 
DefaultMutableTreeNode invoicesNode      = new DefaultMutableTreeNode("INVOICES INFORMATION");

DefaultMutableTreeNode orderNode         = new DefaultMutableTreeNode("ORDER");
DefaultMutableTreeNode productNode       = new DefaultMutableTreeNode("PRODUCT");
DefaultMutableTreeNode priceNode         = new DefaultMutableTreeNode("PRICE");
DefaultMutableTreeNode customerNode      = new DefaultMutableTreeNode("CUSTOMER");

invoicesNode.add(orderNode);
invoicesNode.add(productNode);
invoicesNode.add(priceNode);
invoicesNode.add(customerNode);
...
 

The root of the tree is invoicesNode. The next four lines create the nodes [orderNode, productNode, priceNode, customerNode] and the following lines connect them as the sons of invoicesNode.

If you check in pictures 2 & 3, you will see that priceNode is a Leaf Child vertically spanned over three rows because it is connected directly to the root. You'll also see that orderNode is horizontally spanned over three columns because it has as many sons.

Other methods
Store an object in tree node DefaultMutableTreeNode node = new DefaultMutableTreeNode(new MyClass(p1, p2, p3));
Retrieve the node value MyClass val = (MyClass) node.getUserObject()

3.2 Creation of Header data structures

This section describes the creation of all the data models involved in the header management. The class involved is called MultiDimTable. The skeleton of our MultiDimTable is :
     
 
// We have seen in the preceding section the creation of the tree node in makeMultiHeaderTree()
root_ = makeMultiHeaderTree();

// Instance of the data GridModel and the SQL request that will load the data in the model
MyJdbcGridModel model = new MyJdbcGridModel("jdbc:odbc:EliadExample","sun.jdbc.odbc.JdbcOdbcDriver");
model.setQuery("SELECT * FROM Invoices");

// Instance of the HeaderGridModel using the multiDim class and initialized with the tree that we parsed
headerModel_   = new MyMultiDimColHeaderModel(root_);

// Instance of a JSmartGrid, the Grid component that uses the data model
grid_          = new JSmartGrid(model);
// Place the grid in a ScrollPane
JScrollPane js = new JScrollPane(grid_);
...
// Instance of the Header grid that connects to the main data JSmartGrid
JAdvancedGridHeader smartGridHeader = new JAdvancedGridHeader(grid_, JSmartGrid.HORIZONTAL,
                      headerModel_.getColumnHeaderModel(), headerModel_.getColumnHeaderSpanModel(),
                      headerStyleModel_);
...
// Tells the data grid to use the headers as column headers
grid_.setColumnHeader(smartGridHeader);
 

 You'll notice that the JAdvancedGridHeader uses the same parameters as the standard JSmartGridHeader constructor.

3.3 Spanned Area Caching Mechanism

The information stored in our cache matrix is a reference to the object we want to show in the column header cells (a String value by default) and spanned areas information. We then create a structure to store them.

 

Picture 4: Cache Data Structure

3.3.1 Matrix Cell Model

This paragraph defines the MatrixCellModel skeleton. By implementing this interface you can create your own dynamic structure for Spanned Areas Cache.
 
     
 
public interface MatrixCellModel {
  public boolean    isSpanEmpty();
  public ExtentCell getExtentCell();
  public Object     getValue();
}
 

3.3.2 Implementation of Matrix Cell Model

Here is an example of how to implement a MatrixCellModel interface.

Each time we parse a tree node, we make an instance of this MatrixCellModel  class and store it in the cache matrix. As you will notice, it contains the object value and span information, and we have associated read and write access methods for this data.

The getExtentCell() method creates an instance of ExtentCell using inner class instantiation. The span information is returned by the newly created ExtentCell instance. This getExtentCell() method is called from the classic getSpanOver() in the DirectSpanModel.

     
 
final public class MatrixCell implements MatrixCellModel {
  private int anchorRow_, anchorColumn_, anchorRowCount_, anchorColumnCount_;
  private Object value_;

  public MatrixCell(Object value, int anchorRow, int anchorColumn, int rowCount, int columnCount) {
    value_ = value;
    setSpannedArea(anchorRow, anchorColumn, rowCount, columnCount);
  }

  public boolean isSpanEmpty() {
    return (rowCount_ == 1 && columnCount_ == 1);
  }

  public ExtentCell getExtentCell() {
    if (isSpanEmpty()) return null;

    return new ExtentCell() {
      public int    getRow()         { return anchorRow_; }
      public int    getRowCount()    { return rowCount_; }
      public int    getColumn()      { return anchorColumn_; }
      public int    getColumnCount() { return columnCount_; }
      public Object getIdentifier()  { return null; }
    };
  }
  public void setSpannedArea(int anchorRow, int anchorColumn, int rowCount, int columnCount) {
    anchorRow_    = anchorRow;
    anchorColumn_ = anchorColumn;
    rowCount_     = rowCount;
    columnCount_  = columnCount;
  }
  public Object getValue() {
    return value_;
  }
  public void setValue(Object obj) {
    value_ = obj;
  }
}
 

Notice the functions isSpanEmpty() and getExtentCell(). As we put the spanned area information in our cache, it is easy to return them without any calculation.

3.3.3 Algorithm to obtain Spanned Areas from the Tree Data Model

The span algorithm is simple in this case (written in natural language):
     
 
if it is a leaf child then
  spanRowCount    := height - row
  spanColumnCount := 1
else
  spanRowCount    := 1
  spanColumnCount := LastLeafChildColumn - FirstLeafChildColumn + 1
 

This is done using the function parseHeaderCellTree() in the MyMultiDimColHeaderModel.java file.
According to this method, if there is a Leaf Child whose level is equal to the tree height, its ExtentCell returns 1 for getRowCount() and getColumnCount(). In such a case, there is no span over its cell, and the isSpanEmpty() method returns true while the getExtentCell() method returns null instead of an ExtentCell instance (see 3.3.2).

3.3.4 Cache Matrix

We initialize the cache matrix by calling our parse tree node function.

     
 
private MatrixCellModel[][] headerCacheMatrix_ = null;
...
public MyMultiDimColHeaderModel(DefaultMutableTreeNode root) {
  headerCacheMatrix_ = parseHeaderCellTree(root);
}
 

This method is called in the constructor, thus the complete tree data model has to be built outside, somewhere in GUI Initialization (see MainFrame.java). The tree root  is passed to the MyMultiDimColHeaderModel class constructor when you instantiate it.

Each time a tree node is parsed, a MatrixCell instance is created and put in a cache matrix. As shown below, the cache matrix will hold the objects to render in the header cells (for grid model) and in the spanned area information (for direct span model).

     
 
cacheMatrix[row][column] = new MatrixCell(node.getUserObject(), anchorRow, anchorColumn,
                                          spanRowCount, spanColumnCount);
 

To obtain the width of the tree, we count the number of Leaf children from left to right. This is done using a simple loop, node.getNextLeaf(), that increments an index and assigns the calculated value to the Leaf Child mapped index. 

The height is as easy to compute with treeHeight  = root.getDepth(). The algorithm is to look for the highest level Leaf Child from top to bottom. In the example we find the level of [SHIP].[Date] or [SHIP].[Via] columns. The root of the tree has the lowest level value.

We compute the matrix-like coordinates by mapping the tree into a two-dimensional array (row, column):

3.4 Column Header

3.4.1 Header GridModel

In the preceding section, we have shown how to build header structures according to tree width and height. We are now able to define the columnCount() and rowCount() GridModel public methods.

Thanks to the headerCacheMatrix construction during the parsing, you do not need to look through the whole tree when you search for the value of a cell's coordinates (row, column). You have direct access to information, such as tree width, tree height, node depth or the value in a cell.
     
 
public int getRowCount() {
  return headerCacheMatrix_.length;
}
public int getColumnCount() {
  return headerCacheMatrix_[0].length;
}
public Object getValueAt(int row, int column) {
  return headerCacheMatrix_[row][column].getValue();
}
 

3.4.2 Header SpanModel

To implement the getSpanOver() method  of our DirectSpanModel, we also have a direct access on headerCacheMatrix. We use the MatrixCellModel getExtentCell() method to construct an ExtentCell.
  
     
 
public ExtentCell getSpanOver(final int row, final int column) {
  return headerCacheMatrix_[row][column].getExtentCell();
}
 

3.5 Dragging of Header

One of the objectives of this document is to show how a user can interact with the display of structured headers and modify the column order. 

The user clicks on a column header, drags it to the desired position and releases the mouse button. Since headers are hierarchically structured we have to be able to move an entire subset of columns. This is done by clicking on a spanned column header. When the group of columns  is dragged as a whole, it is considered as one "big" column.

The DirectColDragController is registered as a listener of the grid. The row/column position of the grid, where the dragging starts, is converted to the corresponding node in the header tree.
 
     
 
fromNode_ = null;
firstColumnIndex_ = e.getColumn();
firstRowIndex_    = e.getRow();
DefaultMutableTreeNode dmtn;
int i;
for (dmtn = root_.getFirstLeaf(), i = 0;
     dmtn != null && fromNode_ == null;
     dmtn = dmtn.getNextLeaf(), i++) {
  if (i == firstColumnIndex_) {
    fromNode_ = dmtn;
    for (; fromNode_.getLevel() > firstRowIndex_; )
      fromNode_ = (DefaultMutableTreeNode)fromNode_.getParent();
  }
}
 

During dragging, the current position is converted to a node in the tree. This conversion method is fast because the tree and matrix have equal width and depth. We first search for the tree leaf that matches the column value (e.getColumn()) and then we retrieve the parent that matches the row value (e.getRow()). 

At the end of the dragging the two nodes are then used to check if the desired dragging is valid. To be accepted, the following conditions must be satisfied :

When the user releases the mouse button, the dragging validity is tested, and the first and last positions of the mouse are converted to their ExtentCells. If the user has clicked on a spanned title, or if the target is a group of columns, it must be considered as a group of columns. The dragging distance which is used to compute column moving is calculated using the leftmost columns of the ExtentCells.

In MyJdbcGridModel and MyMultiDimColHeaderModel rulerStructureChanged() methods, we again use the ExtentCells to keep the columns grouped while moving them.

The columns are, in fact, never really "moved". The data is accessed using an index array for the columns in the grid and grid header, and those arrays are then changed to reflect new column positions.

3.6 Customizing the Column Header Style Model and Renderer

In order to distinguish grouped headers from normal headers, we customize the JSmartGrid Header Renderer in order to give them different colors. The text is displayed in bold dark blue when it is a parent node, and in normal plain text light blue when it is a Leaf Child. We can obtain spanned area attributes from the parameter GridContext passed in getComponent(XXX) method.

If you want to show other types of objects (data or components) in your headers, check first in JSmartGrid JavaDoc for default renderers for your particular data type. If you don't find the appropriate one, you will need to write your own.

 

 

 

4. Annex

4.1 Try it !

Compile and run MultiDimTable from the 'tutorial' folder (replace ';' by ':' on Unix):

javac -classpath .;../smartgrid.jar;../advancedgridheader.jar -d ./classes ./src/MultiDimTable/*.java
java -cp ./classes;../smartgrid.jar;../advancedgridheader.jar MultiDimTable.MultiDimTable

To execute, select the column header "LastName" by positioning  the cursor over it and clicking on the left mouse button. Now press the left mouse button and drag the column to the right. Drop your selected column header over the column header "FirstName" by releasing the left mouse button. You see the column header dragging !

Next, drag the "CUSTOMER" column and drop it over the "PRODUCT" column. As you see, all the CUSTOMER's columns group (subtitles) are moved.

If you try to drag Customer.LastName column and drop it over Product.Name column, they won't be swapped because they do not have the same parent. In other words, they don't follow the same hierarchical path.

 

4.2 Project Files

This example uses the following files:

MultiDimTable.java Application file (main)
MainFrame.java GUI initializations
MyJdbcGridModel.java JDBC Adapter for our JSmartGrid Data
MatrixCellModel.java Interface definition for the MatrixCellModel
MatrixCell.java MatrixCell implementation
MyMultiDimColHeaderModel.java Customized GridModel for MultiDimColHeader
MyColHeaderCellRenderer.java Customized cell renderer for the MultiDimColHeader
AboutBox.java About Dialog Box
advancedgridheader.jar Additionnal classes for this example
EliadDB.mdb Database used in this example

In order to compile and run this example, your directory structure should look like (see above for instructions):

advancedgridheader.jar
smartgrid.jar
tutorial/classes/   (must be created before calling javac)
tutorial/ressources/databases/EliadDB.mdb
tutorial/src/MultiDimTable/AboutBox.java
tutorial/src/MultiDimTable/DirectColDragController.java
tutorial/src/MultiDimTable/MainFrame.java
tutorial/src/MultiDimTable/MatrixCell.java
tutorial/src/MultiDimTable/MatrixCellModel.java
tutorial/src/MultiDimTable/MultiDimTable.java
tutorial/src/MultiDimTable/MyColHeaderCellRenderer.java
tutorial/src/MultiDimTable/MyJdbcGridModel.java
tutorial/src/MultiDimTable/MyMultiDimColHeaderModel.java

Before running this example, configure the access to EliadDB.mdb database as described in install.html.

Replace:

 

4.3 What is JSmartGrid?

JSmartGrid™ is a Swing component that has been designed and developed by ELIAD Technologies Inc.
Its main features are :