Skip to main content Skip to footer

Creating Column Groups in a JavaScript Data Grid

Most tables and grids include a single header row that shows the name of the field that the column refers to. In many cases, the table data has a hierarchical nature, and it may be desirable to add several levels of column headers to expose the hierarchy. The W3 specification for the table element shows a simple example:

A test table with merged cells  
 /-----------------------------------------\  
 |          |      Average      |   Red    |  
 |          |-------------------|  eyes    |  
 |          |  height |  weight |          |  
 |-----------------------------------------|  
 |  Males   | 1.9     | 0.003   |   40%    |  
 |-----------------------------------------|  
 | Females  | 1.7     | 0.002   |   43%    |  
 \\-----------------------------------------/

The "height" and "weight" columns are grouped under an "Average" header that makes the table easier to read. You can create this type of table using the FlexGrid (our JavaScript DataGrid), but it requires writing some code. Because this is a fairly common scenario, we wrote some functions that take an object describing the column hierarchy and set everything up automatically. Using these functions, you could create the table in the example above like this:

// define the data  
// http://www.w3.org/TR/html401/struct/tables.html  
var w3Data = [  
{ gender: 'Males', height: 1.9, weight: 0.003, red: .4 },  
{ gender: 'Females', height: 1.7, weight: 0.002, red: .43 },  
];  

// define the columns  
var w3Columns = [  
{ header: ' ', binding: 'gender' },  
{  
header: 'Average', columns: [  
{ header: 'Height', binding: 'height', format: 'n1' },  
{ header: 'Weight', binding: 'weight', format: 'n3' }  
]  
},  
{ header: 'Red Eyes', binding: 'red', format: 'p0' }  
];  

// bind columns and data to a FlexGrid  
bindColumnGroups(flex, w3Columns);  
flex.itemsSource = w3Data;  

The "w3Columns" array describes the column hierarchy using a plain JavaScript object. Each element in the array specifies the properties for a column on the grid, and each may have a "columns" property that specifies child columns. You may nest columns to any depth. The "bindColumnGroups" function creates the hierarchical column structure, including the columns themselves and the merged headers. Here is the result: To further illustrate, consider a table that compares the performance and composition of investment funds. You could have a group of columns to show performance and another to show composition:

// define the columns  
var fundColumns = [  
{ header: 'Name', binding: 'name', width: '2*' },  
{ header: 'Curr', binding: 'currency', width: '*' },  
{  
header: 'Performance', columns: [  
{ header: 'YTD', binding: 'perf.ytd', format: 'p2', width: '*' },  
{ header: '1 M', binding: 'perf.m1', format: 'p2', width: '*' },  
{ header: '6 M', binding: 'perf.m6', format: 'p2', width: '*' },  
{ header: '12 M', binding: 'perf.m12', format: 'p2', width: '*' }]  
},  
{  
header: 'Allocation', columns: [  
{ header: 'Stocks', binding: 'alloc.stock', format: 'p0', width: '*' },  
{ header: 'Bonds', binding: 'alloc.bond', format: 'p0', width: '*' },  
{ header: 'Cash', binding: 'alloc.cash', format: 'p0', width: '*' },  
{ header: 'Other', binding: 'alloc.other', format: 'p0', width: '*' }]  
}  
];  

// bind columns and data to a FlexGrid  
bindColumnGroups(s, fundColumns);  
s.itemsSource = fundData;  

Notice a few interesting points:

  • The column data includes column properties such as “format” and “width”.
  • The column data binds columns to sub-properties of the data items (e.g. “perf.ytd”).

And here is the result: To see both examples live and play with the code, please follow this link: http://jsfiddle.net/Wijmo5/gobtdg7t/ The “bindColumnGroups” function is easy to use and to customize. In the next few sections we will walk through the implementation so you will get a good understanding of how it works, what assumptions it makes, and how you can use or customize it to suit your needs. The “bindColumnGroups” function creates the columns, including their merged headers. It is implemented as follows:

// create column groups and set up grid headers  
function bindColumnGroups(flex, columnGroups) {  

// create the columns  
flex.autoGenerateColumns = false;  
createColumnGroups(flex, columnGroups, 0);  

// merge the headers  
mergeColumnGroups(flex);  

// center-align headers vertically and horizontally  
flex.formatItem.addHandler(function (s, e) {  
if (e.panel == flex.columnHeaders) {  
e.cell.style.display = 'table';  
e.cell.innerHTML = '<div>' + e.cell.innerHTML + '</div>';  
wijmo.setCss(e.cell.children[0], {  
display: 'table-cell',  
verticalAlign: 'middle',  
textAlign: 'center'  
});  
}  
});  
}  

The function starts by setting the grid’s autoGenerateColumns property to false. We will create the columns and don’t want the grid to do it for us when we give it a new itemsSource. Next, the function calls the createColumnGroups to create the columns and extra header cells, and the mergeColumnGroups function to merge the cells in the header. Finally, it uses the formatItem event to align the center-align the content of the header cells. The event handler sets the cell’s display style to “table” and places the cell content into a new div with display set to “table-cell”. This is an easy way to center the cell content vertically. The createColumnGroups function is implemented as follows:

function createColumnGroups(flex, columnGroups, level) {  

// prepare to generate columns  
var colHdrs = flex.columnHeaders;  

// add an extra header row if necessary  
if (level >= colHdrs.rows.length) {  
colHdrs.rows.splice(colHdrs.rows.length, 0, new wijmo.grid.Row());  
}  

// loop through the groups adding columns or groups  
for (var i = 0; i < columnGroups.length; i++) {  
var group = columnGroups[i];  
if (!group.columns) {  

// create a single column  
var col = new wijmo.grid.Column();  

// copy properties from group  
for (var prop in group) {  
col[prop] = group[prop];  
}  

// add the new column to the grid, set the header  
flex.columns.push(col);  
colHdrs.setCellData(level, colHdrs.columns.length - 1, group.header);  

} else {  

// get starting column index for this group  
var colIndex = colHdrs.columns.length;  

// create columns for this group  
createColumnGroups(flex, group.columns, level + 1);  

// set headers for this group  
for (var j = colIndex; j < colHdrs.columns.length; j++) {  
colHdrs.setCellData(level, j, group.header);  
}  
}  
}  
}  

The function loops through the elements in the columnGroups parameter. If a group has no child columns, the function add a column and initializes its properties from the group data. If a group does have child columns, the function calls itself recursively to create the child columns, then applies the group caption to the header cells for the whole group. At this point, the columns and their headers are ready, but they are not merged yet. This is the job of the mergeColumnGroups function:

function mergeColumnGroups(flex) {  

// merge headers  
var colHdrs = flex.columnHeaders;  
flex.allowMerging = wijmo.grid.AllowMerging.ColumnHeaders;  

// merge horizontally  
for (var r = 0; r < colHdrs.rows.length; r++) {  
colHdrs.rows[r].allowMerging = true;  
}  

// merge vertically  
for (var c = 0; c < colHdrs.columns.length; c++) {  
colHdrs.columns[c].allowMerging = true;  
}  
for (var c = 0; c < flex.topLeftCells.columns.length; c++) {  
flex.topLeftCells.columns[c].allowMerging = true;  
}  

// fill empty cells with content from cell above  
for (var c = 0; c < colHdrs.columns.length; c++) {  
for (var r = 1; r < colHdrs.rows.length; r++) {  
var hdr = colHdrs.getCellData(r, c);  
if (!hdr || hdr == colHdrs.columns[c].binding) {  
var hdr = colHdrs.getCellData(r - 1, c);  
colHdrs.setCellData(r, c, hdr);  
}  
}  
}  
}  

The function starts by setting the grid’s allowMerging property to “ColumnHeaders”. This assumes you don’t want the grid to merge the data cells. Next, the function enables merging on all column header rows and columns by setting their allowMerging property to true. We will not define a custom merge manager, so the grid will automatically merge header cells that have the same content. This assumes that you won’t have columns or groups with the same name (or if you do, at least they won’t be next to each other). The final block of code handles the situation where header cells have empty cells below them (this happens when groups have fewer levels than the maximum. In this case, the code simply copies the content from the cell above so they merge vertically.

FlexGrid is also available as an Angular DataGrid, React DataGrid and Vue DataGrid.

Download Now!

Bernardo de Castilho

comments powered by Disqus