Skip to content
Jonathan Eiten edited this page Nov 17, 2018 · 22 revisions

Hypergrid cells can contain images

Hypergrid's rendering engine calls functions known as cell renderers to do the actual rendering of each cell's content. Cell renderers render data by calling any combination of 2D canvas graphics context methods. They can therefore render image data into a cell by calling the 2D graphics context's drawImage().

The default cell renderer

The default cell renderer, the included SimpleCell cell renderer, optionally renders icons to the left or right of the data; or in place of the data. The remainder of this document discusses SimpleCell's icon rendering capabilities. If these capabilities do not suit your needs, you also have the additional option of writing your own cell renderer.

Specifying an image

Images can be specified to SimpleCell in one of two ways:

  1. Programmatically with Image objects
  2. Declaratively with registered image names

Specifying an image programmatically

Normally, the data for a cell contains a single value to be formatted and displayed. This value is passed to the cell renderer as config.value. If SimpleCell is passed an array instead of a scalar value, it interprets it as an image triplet, a 3-element array as follows:

  1. One of:
    • Image object — Renders the image into the cell, left-aligned
    • falsy value — to render nothing on the left side of the cell
  2. One of:
    • Image object - Renders the image into the cell, center-aligned
    • A function returning a stringifiable value - Value is formatted per cell's localizer (format property) and rendered into the cell, aligned per cell's halign property
    • A stringifiable value - Value is formatted per cell's localizer (format property) and rendered into the cell, aligned per cell's halign property
    • Otherwise renders nothing into the center of the cell
  3. One of:
    • Image object — Renders the image into the cell, right-aligned
    • falsy value — Renders nothing on the left side of the cell

There are three ways the cell value can become such an image triplet.

Data source

The triplet can come directly from the data source:

// access the value from the 4th data column, 8th row
grid.getValue(3, 7); // may return a scalar or a 3-element array as discussed above

Data is typically sent in JSON format so this approach would require that the received data be processed to insert the Image objects.

Set somewhere in the application layer

The application can explicitly set the value to such a triplet:

// set a value + right-side icon of a specific cell in a specific column
var value = [null, 23, images.triangle];
grid.setValue(3, 7, value);

In the example above, the value 23 is displayed in the cell, along with a right-side icon (presumably a triangle).

Set in getCell

Applications can override the data model's getCell method. As described in the getCell wiki, the method is passed a config object and a rendererName string. The standard implementation simply returns a reference to the named cell renderer.

The override can include logic to replace the value based on the cell coordinates. For example, the following implementation of a getCell override adds an age-dependent image to the right of the age value for each cell in the "age" column:

dataModel.getCell = function(config, rendererName) {
    if (config.column.name === 'age') {
        var icon = images['ageDecade' + Math.floor(config.value / 10)];
        config.value = [null, config.value, icon];
    }
    return grid.cellRenderers.get(rendererName);
}

The reference to config.value above assumes it is not already a triplet, a fair assumption to make when you are in control of the data.

Note that getCell is called for every cell, not just data cells, which provides the opportunity to set images on metadata cells (e.g., headers).

For example, when the optional row checkboxes and/or numbers are turned on, Hypergrid automatically generates the row header column, an additional column displayed to the left of all the other columns. The cells in this column for each data row contain a checkbox image + a data row number. To make the checkbox follow the row number rather than the other way around:

grid.behavior.dataModel.getCell = function(config, rendererName) {
    if (config.isHandleColumn && config.isDataRow) {
        var icon = images[config.isRowSelected ? 'checked' : 'unchecked'];
        config.value = [null, config.value, icon];
    }
    return grid.cellRenderers.get(rendererName);
}

(Normally, without the above getCell logic, the row header checkboxes use the declarative method discussed below. The above code will override that and use the programmatic method because the SimpleCell cell renderer gives preference to the programmatic method of determining icons over the declarative method.)

Specifying an image declaratively

Images are specified declaratively as property values.

Icon properties

Hypergrid has a cascading structure of properties objects, beginning with the grid properties object; then specific columns extended from that object; and (optionally) specific cells extended from the column properties objects. Thus for example, if a column does not declare a specific property of its own, the value defaults to that of the grid property of the same name. (See the Properties wiki for more information.)

Specify an image in one or more of the following properties:

  • leftIcon
  • centerIcon
  • rightIcon

Icon property values

The values are registered image names. (See Image registry below.) For example, to set the right-side icon to an image registered as "triangle":

// get a reference to the column properties object
var columnProps = grid.behavior.getColumn(3).properties;

// declare an image for all cells in a specific column
columnProps.rightIcon = 'triangle';

// declare an image for a specific cell in a specific column
columnProps.setCellProperty(5, 'rightIcon', 'triangle');
// or:
grid.behavior.setCellProperty(3, 5, 'rightIcon', 'triangle');

Calculated property values

Instead of declaring a primitive string value for a property, it is also possible to define a getter for the property. So the icon properties, which normally have registered image names as their values, can be redefined as getters that return such names.

The following example chooses the same age-related right icon image as the programmatic getCell example above:

var iconDescriptor = {
    enumerable: true,
    get: function() {
        return 'ageDecade' + Math.floor(this.value / 10);
    }
};

Object.defineProperty(columnProps, 'rightIcon', iconDescriptor);

The execution context (the this value) for property getters is the relevant properties object itself, which will include at render time additional properties added by the renderer, such as value in the above.

Remember that the icon properties are strings containing registered image names — unlike the image triplet elements described above which are references to actual Image objects.

Self-defeating setter

Note that a property defined as a getter as above is read-only and cannot be set to an explicit value. This is generally appropriate for a calculated value. That said, if you do need to be able to subsequently reset such a property, first make sure it is "configurable" and then add the following "self-defeating" setter which redefines the property (both the getter and this setter itself) as a simple instance var:

iconDescriptor.configurable = true;
iconDescriptor.set = function(value) {
    Object.defineProperty(this, 'rightIcon', {
        configurable: true,
        enumerable: true,
        writable: true,
        value: value
    });
};

NOTE: It is assumed you will be calling the above setter on the object owning the property, i.e., the column properties object. (Specifically, do not invoke the setter through getCell's config parameter. The object owning the definition is in config's prototype chain because config is created from the column properties object on every cell render. The setter, when invoked through config, creates a new definition which is "owned" by config and will therefore only be effective for the current render. Even if you were to set it on the correct object in the prototype chain, this is still the wrong place to do this because it only needs to be done once; the action would be redundant on subsequent getCell calls.)

Image registry

The image registry is a simple hash of image names paired with Image objects.

To register an image:

Method A: Images loaded on the page

This approach gets the data directly from an image file loaded with the page response.

Step 1: Request the image data be loaded with the page:

<body>
    ...
    <img id="triangle-icon" style="display:none" src="my-triangle-icon.png">
    ...
</body>

Step 2: Locate the image registry:

// if using the fin-hypergrid.js build
var imageRegistry = fin.Hypergrid.require('fin-hypergrid/images');

// if using the fin-hypergrid npm module
var imageRegistry = require('fin-hypergrid/images');

The image registry is merely a JavaScript hash of Image objects.

Step 3: Register the image for SimpleCell use

Make sure the image is loaded. (For example, by waiting for the image's or the window's load event.) Then register the image:

// register the image under the name "tricon"
imageRegistry.add('tricon', document.getElementById('triangle-icon'));

Method B: Raw image data

To avoid depending on external files, you can encode the binary image data in Base64 text format. There are many utilities available to help you do this, such as this web client one; and my gulp-imagine-64 npm gulp module.

Once you have the data, to register the image under the name "tricon" for example:

var img = new Image();
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAECAYAAABcDxXOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjExR/NCNwAAABpJREFUGFdjgIL/eDAKIKgABggqgAE0BQwMAPTlD/Fpi0JfAAAAAElFTkSuQmCC';
imageRegistry.add('tricon', img);

SVG images

Images may derive from SVG data, which has the advantage that it is fully themeable. See the Themeable Images wiki for details.

Built-in images

The registry comes pre-loaded with a few icon images (using the raw image data method described above). You can see these in the registry hash:

console.log(Object.keys(imageRegistry));

For example, the checkbox found in the row number column uses two of these images, one for when the checkbox is empty (unchecked); and one for when it is checked (checked).

Replacing the built-in images

If you wish to replace any of these images with your own, simply override them with a different Image object or reset their src property. Suppose for example that you had replacement images for checked and unchecked:

imageRegistry.checked = myCheckedImage;
imageRegistry.unchecked = myUncheckedImage;

(See Image Registry above for how to get images into variables.)

Overriding the row handle images

The row handle column has its own properties object. This object has a predefined getter for leftIcon as described above in Calculated property values. This getter chooses between the 'checked' vs. 'unchecked' registered image names based on whether or not the row in question is currently unselected vs. selected, respectively.

Putting the row handle images on the right

If you prefer to put the checkboxes on the right, you could "move" the getter to the rightIcon property:

var rowHeaderProps = grid.behavior.columns[-1].properties.rowHeader;
var iconDescriptor = Object.getOwnPropertyDescriptor(rowHeaderProps, 'leftIcon');
delete rowHeaderProps.leftIcon;
Object.defineProperty(rowHeaderProps, 'rightIcon', iconDescriptor);

Although you could similarly move it to the centerIcon property, the cell value (the row number) would override it so it would be pointless.

Blanking the row handle images

One way to eliminate the row handle column's checkboxes entirely might be to delete this getter and replace it with an unregistered image name:

rowHeaderProps.leftIcon = '';

Another way would be to delete the images from the registry (although that would render them unusable thereafter):

delete imageRegistry.checked;
delete imageRegistry.unchecked;

Update: While the above examples will work, be aware that (as of v2.1.0) the boolean rowHeaderCheckboxes property can be used to hide just the checkboxes from the row headers; and the boolean rowHeaderNumbers property can be used to hide just the row numbers. The boolean property showRowNumbers is now a setter that sets both of the first two properties; and a getter that returns truthy if either is truthy.

Image size

It is up to the cell renderer programmer to make sure the image is rendered neatly within the cell. The easiest way to do this is to not worry about it all, which works fine so long as you keep the images small (which is why we're referring to them as "icons"). In particular, keep the height ≤ the row height.

As a general rule, you want to avoid defining a clipping region in your renderer because of the cost: Clipping every cell will significantly slow down grid rendering. That said, you could clip just those cells with images in danger of overflowing the cell boundaries. But of course you don't really want your image to be clipped anyway, right? So that is probably the wrong approach. On the other hand, having the image overflow the cell boundaries is even worse. If the resulting speed is still adequate for your needs, go for it. But the best advice is, again, to keep the images small and you shouldn't have any issues.

Clone this wiki locally