Like most data grids, the FlexGrid shows items in rows and their properties in columns.
For example:
Name |
Age | Hired | Rating | Street | City | Country |
Paul | 34 | 1/23/1961 | 0.43 | 123 Main St. | London | England |
Ringo | 43 | 11/12/1959 | 0.93 | 465 Grand Ave. | Oxford | England |
George | 23 | 9/2/1961 | 0.66 | 789 Broadway | Edinburgh | Scotland |
John | 22 | 3/3/1957 | 0.13 | 321 Oak St. | Dublin | Ireland |
But occasionally, we got requests for transposed views, where columns represent items and rows their properties:
Name |
Paul | Ringo | George | John |
Age | 34 | 43 | 23 | 22 |
Hired | 1/23/1961 | 11/12/1959 | 9/2/1961 | 3/3/1957 |
Rating | 0.43 | 0.93 | 0.66 | 0.13 |
Street | 123 Main St. | 465 Grand Ave. | 789 Broadway | 321 Oak St. |
City | London | Oxford | Edinburgh | Dublin |
Country | England | England | Scotland | Ireland |
In these cases, we typically suggested a simple solution: transpose the data source and bind the grid to the transposed version of the data.
This approach works, but it has shortcomings:
We recently got another request for this feature and decided to address these shortcomings.
One of the initial ideas was to add this to the FlexGrid as a built-in feature. We decided not to go that route because this is not a common enough request, and we try to keep the core FlexGrid as compact and simple as possible.
Another idea was to create a new class that would extend the FlexGrid and add the transposed view feature. This is the approach we took when we created the FlexSheet, MultiRow, and PivotGrid controls. We may someday create a TransposedGrid control if there are enough requests for it, but for now we decided to implement this using a regular FlexGrid and a couple of re-usable methods.
You can see the result in the FlexGrid with Transposed Views sample app.
The sample app shows three data sets, each on a regular and a transposed grid. The most realistic data set is the last one. It contains information for different TV models. The transposed view looks like a typical comparison sheet found on web sites and magazines, where each column represents a product and the rows show the product information:
The methods used to turn a regular FlexGrid into a transposed grid are:
The next sections describe how these methods work.
The transpose function creates a transposed view of the original data. It takes two arguments:
Here is a slightly simplified version of the transpose method (to see the actual implementation please refer to the sample mentioned earlier).
// transpose data
transpose(arr, rowInfo) {
// generate rowInfo array if not provided
if (!rowInfo) {
rowInfo = this.getRowInfo(arr);
}
// create keys (property names) used by all proxy objects
let proxyKeys = arr.map((item, index) => { return 'item' + index });
// generate array with one proxy object for each rowInfo (property)
let transposed = rowInfo.map(rowInfo => {
return this.createProxy(arr, rowInfo, proxyKeys)
});
// return the transposed array
return transposed;
}
If the user did not provide a rowInfo array with the properties that should be included in the transposed view, the code generates the array based on the data. The getRowInfo method uses the same logic that the grid uses when the autoGenerateColumns property is set to true:
The code generates a proxyKeys array with the names of all properties in the proxy objects. Remember, each proxy object represents a single property in the original data items and has one property for each original data item. The properties of the proxy objects are called "item0 and "item1" etc.
Once it has the rowInfo and proxyKeys arrays, the code scans the items in the rowInfo array (original object properties) and generates a proxy object for each. The returned array has the same length as the rowInfo array, and each object has one property for each original data item.
The most interesting part of the code is the createProxy method. It creates a JavaScript Proxy object to get and set the data from the original array, at the appropriate index and using the appropriate binding:
// create proxy that reads/writes data from the original array
createProxy(arr, rowInfo, proxyKeys) {
// target object contains the data
let target = {
_arr: arr,
_rowInfo: rowInfo,
_keys: proxyKeys
};
// handler object contains methods that act on the data
let handler = {
// methods required for property enumeration
ownKeys: target => {
return target._keys;
},
getOwnPropertyDescriptor: (target, key) => {
return { enumerable: true, configurable: true, writable: true }
},
// property getter/setter
get: (obj, prop) => {
let index = obj._keys.indexOf(prop);
return index > -1 ?
obj._arr[index][obj._rowInfo.binding] :
obj[prop];
},
set: (obj, prop, value) => {
let index = obj._keys.indexOf(prop);
if (index > -1) {
obj._arr[index][obj._rowInfo.binding] = value;
}
return index > -1;
}
};
// create and return the proxy
return new Proxy(target, handler);
}
The code creates a JavaScript proxy based on a "target" object that contains data and a "handler" object that acts on that data.
In this case, the target object holds a reference to the original data array, a rowInfo object that describes the property in the original data that is represented by the proxy, and the array of keys that contains the names of all properties defined by the proxy object (one for each data item in the original array).
The handler object has "ownKeys" and "getOwnPropertyDescriptor" methods that are required for property enumeration. This allows the transposed grid to auto-generate columns as it would with regular plain data objects.
The handler object defines getter and setter methods that work with the data in the original array. There are no extra copies of the data. The getter starts by looking up the index of the item in the keys array. This corresponds to the index of the data object in the original array. If the index is found, the getter uses the binding to retrieve the value from the original array. If the index is not found, the getter returns the value of the property in the target objects itself. This allows callers to get the target's "_rowInfo" member for example.
The setter function uses similar logic to change the data in the original array.
JavaScript Proxy objects are supported in all modern browsers, but not in IE. If you want to support IE, or for some reason don't want to use Proxy objects, you can create regular objects that do the same thing. This approach is less efficient, but it is simple, and it works:
// create proxy-like object when real proxies are not available
createTransposedObject(arr, rowInfo, keyPrefix) {
let obj = {
_rowInfo: rowInfo
}
arr.forEach((item, index) => {
Object.defineProperty(obj, keyPrefix + index, {
enumerable: true,
get: () => {
return item[rowInfo.binding];
},
set: value => {
item[rowInfo.binding] = value;
return true;
}
});
});
return obj;
}
The implementation is simpler than the one using proxies, but it is also less efficient. For each proxy object, this option defines a property for each data item. If the data source is large, the objects may end up defining hundreds of properties. This could be improved by creating a class in the main function and instantiating the class for each object.
Now that we have a transposed version of the data (using real proxies or plain JavaScript objects), let's look at transposing the grid.
The transposeGrid method is responsible for showing the property names in the row headers and for applying the rowInfo properties to the grid's rows. Both tasks are performed automatically by the grid when it is not transposed (property names are shown in column headers and column properties are auto-generated or assigned by the caller).
This is the transposeGrid implementation:
// configure grid for showing transposed data
transposeGrid(grid) {
// make row header column wider
let rhCols = grid.rowHeaders.columns;
if (rhCols.length) {
let hdrCol = rhCols[rhCols.length - 1];
hdrCol.width = grid.columns.defaultSize;
}
// update grid when rows are loaded
grid.loadedRows.addHandler((s, e) => {
grid.rows.forEach(row => {
let info = row.dataItem._rowInfo;
for (let prop in info) {
row[prop] = info[prop];
}
if (rhCols.length) {
let hdr = info.header || wijmo.toHeaderCase(info.binding);
grid.rowHeaders.setCellData(row.index, rhCols.length - 1, hdr);
}
});
});
// apply row properties now
grid.onLoadedRows(grid);
}
The code starts by making the last row header column wider so it can accommodate the property names.
Then it attaches a handler to the grid's loadedRows event to configure the rows by:
The application is now ready. If you run the sample again, notice how the FlexGrid is used to show original and transposed versions of three data sets. Both grids support editing, and changes made to either grid are automatically reflected on the other.
To support this scenario, we did have to make some small changes to the FlexGrid. The following properties used to be defined by the Column class, and were moved to its base class RowCol, which is also the base class for the Row class: align, dataMap, dataType, dropDownCssClass, format, inputType, mask, and maxLength.
When rendering a cell, the grid now looks up these properties on both the column and the row objects.
The changes were very small and they do increase the flexibility of the grid to support this and other custom view scenarios without breaking existing code.
The most interesting aspect of the sample is the use of JavaScript Proxy objects to create the transposed views.
Proxy objects are typically used to add observability and validation to plain JavaScript objects. Using them to generate alternate views of the data is also a valuable feature.
The application shows a nice way to use the FlexGrid to show transposed data. It is substantially better than the original approach of simply creating static transposed versions of the source data, but it does have a few limitations when compared to a regular (non-transposed) FlexGrid:
Both features should be relatively easy to add, but they would require extra code that is not included in our sample.
If we get enough requests for either feature, we will extend the sample to support them.