Categorized Dojo TreeGrid in XPages – Add Totals and Counts

In a previous post, I showed how to set up a Dojo TreeGrid to create a categorized grid. In this post, I’ll show how to create a TreeGrid that can calculate entry counts and totals for the categories.

TreeGrid_a_SumOneCol

When expanded, the totals show at the bottom of the category section. When collapsed, the totals show at the category level.

Required Modules and XPage Property

See the previous post for information on including the required dojo modules and style sheets and setting the XPage property to parse dojo on load.

JSON Data Store

As in the previous post, I showed how to use an XAgent to generate my own JSON in the format required by the TreeGrid. In this post, we’ll use the same technique, but the format of the JSON must be different for the categorized view with totals.

Fortunately, this format is simpler to generate.

In thise case, we are no longer building the list of references to child items and then generating the child items separately; it now requires inclusion of the child items as an array of items under a property of the category item.

Here’s a chunk of JSON that populates the first category in the view shown above.

The first item in the list is the ‘AK’ category. It has an array of child items for all of the entries within that category. The property that contains the child items (named childItems in this code) can be whatever you want.

{
identifier: 'id',
label: 'name', 
items: [
{id:'AK', type: 'state', state:'AK', numPeople: 3, childItems:[ 
  {id:'B3093953178C98E905257838007ABC48', firstname:'Bella', lastname: 'Martin', valueToAdd: 2}, 
  {id:'7FDB9CCDE7D6923E05257838007ABC1E', firstname:'Brian', lastname: 'Leggett', valueToAdd: 2}, 
  {id:'8CD685A9D29150D005257838007ABDD7', firstname:'Jesse', lastname: 'Pack', valueToAdd: 2}, 
  {id:'B281D25B6AE91D7A05257838007ABD53', firstname:'Jose', lastname: 'Bibeau', valueToAdd: 2}, 
  {id:'A4CA4CA43B93673605257838007ABC7E', firstname:'Karen', lastname: 'Buss', valueToAdd: 2}, 
  {id:'5656D38EC6DB315E05257838007ABA5D', firstname:'Margarita', lastname: 'Levesque', valueToAdd: 2}, 
  {id:'B2CC1FA115074A9305257838007ABEEA', firstname:'Mary', lastname: 'Witzel', valueToAdd: 2}, 
  {id:'EDDFFA8D2C2169B405257838007ABB03', firstname:'Rufus', lastname: 'Davis', valueToAdd: 2}
] }, 

...

]}

Here is the code that generates the data, based on a view from the FakeNames database, categorized by State. It is very similar to the code in the previous post, but it just builds one string of data per category, rather than two separate strings in the previous code.

var externalContext = facesContext.getExternalContext();
var writer = facesContext.getResponseWriter();
var response = externalContext.getResponse();

response.setContentType('application/json');
response.setHeader('Cache-Control', 'no-cache');

writer.write("{\n");
writer.write("identifier: 'id',\n");
writer.write("label: 'name', \n");
writer.write("items: [\n");

var categoryAndChildren = "";

// Walk the view and build the JSON data
var vw:NotesView = database.getView('ByState');
var nav:NotesViewNavigator = vw.createViewNav();
var ve:NotesViewEntry = nav.getFirst();

while (ve != null) {
  var cv = ve.getColumnValues();

  // When a categorized entry is reached:
  // (a) write out the previous category and children
  // (b) set up the new category element	
  if (ve.isCategory()) {
    // Write out the previous category and child entries		
    if (categoryAndChildren != "") {
      // Trim the trailing comma and space off the category item, close of the childItems array (]) and the category item (})
      categoryAndChildren = categoryAndChildren.substring(0, categoryAndChildren.length - 2);
      writer.write("\n" + categoryAndChildren + "] }, ");
    }	
	
    // Start building the new category and child entries
    categoryAndChildren = "  {id:'" + cv[0] + "', type: 'state', state:'" + cv[0] + "', numPeople: 3, childItems:[ \n";
	
  } else {
    // This isn't a category, so add another child item
    categoryAndChildren += "{id:'" + ve.getUniversalID() + "', firstname:'" + cv[1] + "', lastname: '" + cv[2] + "', valueToAdd: 2}, "
  }	
	
  // Get the next entry and recycle the current one
  var tmpentry = nav.getNext();
  ve.recycle();
  ve = tmpentry;
}

// Write out the last category and children, without the trailing commas
categoryAndChildren = categoryAndChildren.substring(0, categoryAndChildren.length - 2);
writer.write("\n" + categoryAndChildren + "] }, ");

// Close the JSON string
writer.write("]}");
writer.endDocument();

Grid Generation Code – Category Totals

The grid generation is also a bit different in this case. In the first categorized grid, all we had to do was define a few columns for the grid layout.

In this case, we need to define a more complex layout.

Lines 1-15 define the layout for the grid. Line 4 defines the State column at the category level. Line 5 defines the property of the category that will contain the array of child documents. I named the field childItems, but that is not a reserved name. However, in line 6, children is a reserved word. That must be the name of the property that contains the array of child documents. Line 11 defines that the grid will add the values in each column and display the sum total for each category.

Line 17 reads the JSON data that is generated by the XAgent page.

Lines 19-26 define the grid itself. The structure property must point to the layout that was defined above and the store property must point to the JSON data store. As mentioned in the previous post, the query property is required or else the grid will display erroneous duplicate entries. Line 26 defines the div tag where the grid will be generated, so that div tag must exist on the page.

var layout = [ 
  { cells: [
    [ 
      {field: "state", name: "State"},
      {field: "childItems",   
        children: [ 
          { field: "firstname", name: "First Name" },
          { field: "lastname", name: "Last Name" },
          { field: "valueToAdd", name: "Totals Col" }  
        ], 
      aggregate: "sum"
      }
    ]] 
  }					
]
			
var jsonStore = new dojo.data.ItemFileWriteStore({ url: "TreeGrid_DataStore_TotalsAndCounts.xsp"});

var grid = new dojox.grid.TreeGrid({
  structure: layout,
  store: jsonStore,
  query: {type: 'state'},
  queryOptions: {deep: true},
  rowSelector: true,
  openAtLevels: [10]
}, dojo.byId("treeGrid"));

grid.startup();
dojo.connect(window, "onresize", grid, "resize");

This code generates the grid shown below:

TreeGrid_b_WithTotals_SumsAllCols

Formatter Functions

By default it will attempt to sum every column in the child items. For numeric values, this is what you want, but for text values, it will just concatenate the values as shown above.

Fortunately, we can prevent that with a formatting function. Each child entry can use a formatter property that takes the name of a function to format the data.

The formatter function automatically accepts two parameters, the value in the current cell and a row index variable that denotes the level of categorization.

The child item rows have a level number that matches the nth category in the view, starting with 0. The category total rows have a negative number that corresponds to the level of categorization. Top level category rows will be -1, second-level category rows will be -2, and so on.

This formatter function will not display anything in the category totals row. To update the grid to only add up the numeric column, we will add this formatter function to the text columns.

function formatText(value, rowIndex) {
  if (rowIndex >= 0) {
    return value;
  } else {
    return '';
  }
}

Then update the text columns (First Name and Last Name, below) to use the formatter function.

var layout = [ 
  { cells: [
    [ 
      {field: "state", name: "State"},
      {field: "childItems",   
        children: [ 
          { field: "firstname", name: "First Name", formatter: formatText},
          { field: "lastname", name: "Last Name", formatter: formatText },
          { field: "valueToAdd", name: "Totals Col" }  
        ], 
      aggregate: "sum"
      }
    ]] 
  }					
]

TreeGrid_a_SumOneCol

Much better.

Row Counts

If you’d like to do a row count instead of a sum, all you need to do is change a couple of lines in the grid layout object.

Line 8 was changed to remove the formatter function so it will display the row count.

Line 10 was changed to set the aggregation type to cnt rather than sum

var layout = [ 
  { cells: [
    [ 
      {field: "state", name: "State"},
      {field: "childItems",   
        children: [ 
          { field: "firstname", name: "First Name", formatter: formatText}, 
          { field: "lastname", name: "Last Name"}
        ], 
      aggregate: "cnt"
      }
    ]] 
  }					
]

TreeGrid_c_count

openAtLevels

You may have noticed the openAtLevels attribute in the grid generation code earlier in this post.

This is a property of the tree grid that can be used to define category expansion at the time of grid generation. The attribute takes an array of values, each of which can be true, false, or a number. If you have multiple levels of categorization, then each element in the array will apply to the corresponding category level. In our case, we only have one level of categorization, so there’s only one element.

If you set it to true, it will expand all categories at that level. If you set it to false, it will collapse all categories at that level. If you give it a number, then it will automatically expand only the categories that have that many child items or less.

This property has no bearing on the grid’s ability to calculate totals or row counts.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: