Skip to main content Skip to footer

How to use JavaScript Proxies to Create Transposed Data Views

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:

  1. The grid is bound to a copy of the data. Any edits made on the grid will not be reflected on the original data.
  2. The data types, format, alignment, validation, and other properties typically associated with columns cannot be applied to rows, so if you wanted to format or edit cells you would need to write some code.
  3. By default, the grid's row headers will not show the property names and the grid will be hard to understand.

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:

FlexGrid with Transposed Views

The methods used to turn a regular FlexGrid into a transposed grid are:

  • transpose: This method takes an array with data objects and an optional array containing the properties to transpose. It returns a live transposed array with proxy objects that refer to the same data as the original data array.
  • transposeGrid: This method adds a row header column to show the property names and handles the loadedRows event to apply to rows the properties that are typically assigned to columns, including format, datatype, and dataMap.

The next sections describe how these methods work.

Creating Transposed Views

The transpose function creates a transposed view of the original data. It takes two arguments:

  • arr: The array containing the data to transpose
  • rowInfo: An optional array containing information about the columns to transpose. If not provided, all properties are included.

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.

Creating the JavaScript Proxy Objects

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.

What if Proxy is not Supported?

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.

Creating Transposed Grids

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:

  1. Setting row properties such as format, header, dataType, cssClass, and dataMap. These properties come from the "_rowInfo" member of the proxy object.
  2. Setting the row header data to show the property name for the item represented by the row.

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.

FlexGrid Improvements

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.

JavaScript Proxies

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.

Limitations and Potential Improvements

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:

  1. Filtering: To implement filtering, we would have to filter the original data and re-generate the transposed view when the filter is applied.
  2. Grouping: To implement grouping, we would have to add column header rows (one per group) with merged cells to show which columns belong to each group. We would also add buttons to the group headers to allow users to collapse and expand the groups.

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.

Bernardo de Castilho

comments powered by Disqus