Tutorial: Cell Renderers

Cell Renderers

This document describes the Cell Renderer interface. This information is useful to the application developer to better understand what cell renderers are, how to use them, and how they may be customized.

What is a cell renderer?

A cell renderer is custom rendering logic that "paints" a cell's data in some form into the a region confined to the bounding rectangle of the cell. Special care should be taken when creating a custom cell renderer to ensure good performance. On every grid repaint, this code will be called repeatedly for all cells that reference it.

Cell renderers have access to the 2D graphics context of the Hypergrid canvas element and can be used to draw anything the user can imagine (again, with considerations for speed).

Which cells can have a renderer?

All cells in the grid from header cells to data cells, etc., require a cell renderer.

How do I assign a cell renderer?

Cell renderers are assigned declaratively at setup time; this assignment can be overridden programmatically at cell render time.

Declarative assignment. Cell renderers are assigned declaratively by supplying the name of a registered cell renderer to the renderer render property. This property (like all render properties) may be applied at the cell level, column level, or grid level.

The default value for this property (applied at the grid level) is 'SimpleCell'. This is therefore the default cell renderer for all cells. It is a basic text renderer and is discussed in more detail below.

Programmatic reassignment. The declared cell renderer for any given cell may be reassigned programmatically at render time by logic you put in your override of the getCell method. This is explained in more detail below.

Text vs. non-text cell renderers

Grid data is presented to Hypergrid by the application layer. This will typically have come from a serialized JSON object, but the precise means of transport is up to the developer. After parsing the serialized data, and perhaps instancing some objects, the cell data may take on any form. It is the job of a cell renderer to display this data.

Raw data

The default cell renderer renders data as text using the toString() method that all JavaScript objects support.

Formatted text data

The default renderer will respect the cell's format render property, invoking the named formatter to render the text into a more human-friendly form. For example, if the raw datum is 1234.56789, an integer formatter might render as "1235". The precise format is of course up to the formatter.

Localized text data

As soon as you start formatting string data, you run into the various local differences for doing so. For example, 1000.5 while rendered in many locales as "1,000.5" is rendered as "1 000,50" in France.

Hypergrid defines a lightweight localization API for creating and registering localizers. In fact, the format render property mentioned above actually names a registered localizers. The default cell renderer calls the localizer's format() function to render the raw data into human-friendly form.

As the name implies, localizers have a locale property. The localizers generated by the included factory functions use the Intl API, which respects this locale property for numbers, currency, percentages, and dates. Your custom localizers should also respect locale as needed by your application.

See the separate Localizers (formatters and deformatters) tutorial for more information.

Infographics

While the default cell renderer renders data as text, cell renderers are not constrained to do so. Cells may specify other cell renderers that render the data in a non-text form, as infographics. Examples include sparklines (inline chart), a star ratings, and confidence intervals. Implementations of these are included with Hypergrid. You can also write your own custom cell renderers and assign them to particular cells. This is explained in more detail below.

Default Renderers Available

The CellRenderer Base Class is the object that provides a empty cell.

The following cell renderers are available for you to use declaratively. They have been extended from the CellRenderer base.

Cell Renderer | Description :.------------ | :---------- simpleCell | Is the normal cell renderer operation which accommodates for images/fonts/text.They will be centered vertical and be placed on horizontally aligned left, right or middle. emptyCell | Paints a blank cell. Provided with the base CellRenderer class. treeCell | Paints a tree cell that accommodates nested data. errorCell | Renderer for any cell considered to be in an error state. button | Paints a button dependent on mousedown state. lastSeletion | Renderer for painting a selection rectangle on top of cells. linkCellRenderer | Simple Cell with the link option set. Paint text in a cell that is underline. sparklineCell | Paints an implementation of https://en.wikipedia.org/wiki/Sparkline. Requires an array of values to be useful. sparkbarCell | A tiny bar chart. Requires a list of values to be useful.

Programmatic cell editor association

The following examples refer to a grid object:

var grid = new Hypergrid(...);

grid.behavior.dataModel.getCell method is called by HyperGrid to resolve the renderer for each data cell. It is called with a config object (see below) and declaredRendererName, which the proposed cell renderer name (from the cell's render properties). As you can see from the default implementation, the return value of this method is one of the renderer singletons (from ./src/dataModels/DataModel.js):

DataModel.prototype.getCell = function(config, declaredRendererName) {
    return this.grid.cellRenderers.get(declaredRendererName);
};

For a programmatic cell renderer association, simply reassign rendererName in your method override (in your main program):

grid.behavior.dataModel.getCell = function(config, declaredRendererName) {
    if (...) { // some condition based on config
        declaredRendererName = 'myRegisteredRenderer'; // case-insensitive
    }
    return this.grid.cellRenderers.get(declaredRendererName);
};

For useful example of getCell overrides, search for "getCell" in the demo folder.

The grid-wide default renderer name as defined in ./src/defaults.js is SimpleCell (which you can of course override).

In your getCell override, you can optionally set additional arbitrary properties on config which will be passed along to the renderer's paint function later.

getCell is called with a config object which is created from (i.e., whose prototype is) the column's "state" object (its render properties, as documented here), with the following additional stateful properties providing information about the cell:

Parameter Description
bounds The clipping rect of the cell to be rendered; the region to which the renderer's paint function should confine itself.
bounds.height Paint region height in canvas pixels.
bounds.width Paint region width in canvas pixels.
bounds.x Paint region horizontal pixel coordinate from canvas origin (top left).
bounds.y Paint region vertical pixel coordinate from canvas origin (top left).
halign Whether to horizontally align 'left', 'right', or 'center'.
isCellHovered If the cell is hovered by mouse.
isCellSelected If the cell was selected specifically.
isColumnHovered If the column the cell is in is hovered.
isColumnSelected If the column the cell is in is selected.
isGridColumn If the cell is in a column that is part of the data region (as opposed to column and row headers). Always true.
isGridColumn If the cell is in a row that is part of the data region (as opposed to column and row headers). Always true.
isInCurrentSelectionRectangle If the cell is in the last selected cell region.
isRowHovered If the row the cell is in is hovered.
isRowSelected If the row the cell is in is selected.
isSelected The cell is currently selected (included in one of the current selection models).
isUserDataArea If the cell holds actual user data.
value an untyped field that represents contextual information for the cell to present. I.e. for a text cell value you may used this represent stringified data.
untranslatedX The horizontal grid coordinate measured from first data column. That is, the column's index into the list of columns currently active in the grid, grid.behavior.columns[]. This list is a subset of of the full list (grid.behavior.allColumns[]) because (a) "hidden" columns are excluded and (b) remaining columns are ordered. The order of the columns in this list can be re-ordered at any time programmatically or via the UI by dragging columns around.
y The vertical grid coordinate measured from top header row.he grid row index, including the header rows. Because the number of header rows can vary, the first data row index also varies.
x The "translated" index into the behavior.allColumns array. That is, The column's index into the full column list, grid.behavior.allColumns[] (and the data source's fields[] array upon which it is based).
normalizedY The vertical grid coordinate normalized to first data row. For the first data row, this value is always 0. Excludes the header rows.

Note about the LastSelection renderer

The config object only has access to bounds and the following

Parameter Description
selectionRegionOutlineColor Borders of selected cells
selectionRegionOverlayColor Color of selected cells

Creating your own renderer

You can create your own renderer by extending from the CellRenderer base class and overriding the paint method that expects gc (2D graphics context) object and a config object (described above).

Register your new cell renderer on the grid with grid.cellRenderers.add. This allows it to be referenced by name (in the cell's renderer render property).

Here's an example use the Star Rating as the inspiration

"Star ratings" look something like this.

/*
   Define your rendering logic
*/

var REGEXP_CSS_HEX6 = /^#(..)(..)(..)$/,
  REGEXP_CSS_RGB = /^rgba\((\d+),(\d+),(\d+),\d+\)$/;

function paintSparkRating(gc, config) {
  var x = config.bounds.x,
    y = config.bounds.y,
    width = config.bounds.width,
    height = config.bounds.height,
    options = config.value,
    domain = options.domain || config.domain || 100,
    sizeFactor = options.sizeFactor || config.sizeFactor || 0.65,
    darkenFactor = options.darkenFactor || config.darkenFactor || 0.75,
    color = options.color || config.color || 'gold',
    stroke = this.stroke = color === this.color ? this.stroke : getDarkenedColor(this.color = color, darkenFactor),
    bgColor = config.isSelected ? (options.bgSelColor || config.bgSelColor) : (options.bgColor || config.bgColor),
    fgColor = config.isSelected ? (options.fgSelColor || config.fgSelColor) : (options.fgColor || config.fgColor),
    shadowColor = options.shadowColor || config.shadowColor || 'transparent',
    font = options.font || config.font || '11px verdana',
    middle = height / 2,
    diameter = sizeFactor * height,
    outerRadius = sizeFactor * middle,
    val = Number(options.val),
    points = this.points;

  if (!points) {
    var innerRadius = 3 / 7 * outerRadius;
    points = this.points = [];
    for (var i = 5, θ = Math.PI / 2, incr = Math.PI / 5; i; --i, θ += incr) {
      points.push({
        x: outerRadius * Math.cos(θ),
        y: middle - outerRadius * Math.sin(θ)
      });
      θ += incr;
      points.push({
        x: innerRadius * Math.cos(θ),
        y: middle - innerRadius * Math.sin(θ)
      });
    }
    points.push(points[0]); // close the path
  }

  gc.cache.shadowColor = 'transparent';

  gc.cache.lineJoin = 'round';
  gc.beginPath();
  for (var i = 5, sx = x + 5 + outerRadius; i; --i, sx += diameter) {
    points.forEach(function(point, index) {
      gc[index ? 'lineTo' : 'moveTo'](sx + point.x, y + point.y);
    });
  }
  gc.closePath();

  val = val / domain * 5;

  gc.cache.fillStyle = color;
  gc.save();
  gc.clip();
  gc.fillRect(x + 5, y,
    (Math.floor(val) + 0.25 + val % 1 * 0.5) * diameter, // adjust width to skip over star outlines and just meter their interiors
    height);
  gc.restore(); // remove clipping region

  gc.cache.strokeStyle = stroke;
  gc.cache.lineWidth = 1;
  gc.stroke();

  if (fgColor && fgColor !== 'transparent') {
    gc.cache.fillStyle = fgColor;
    gc.cache.font = '11px verdana';
    gc.cache.textAlign = 'right';
    gc.cache.textBaseline = 'middle';
    gc.cache.shadowColor = shadowColor;
    gc.cache.shadowOffsetX = gc.cache.shadowOffsetY = 1;
    gc.fillText(val.toFixed(1), x + width + 10, y + height / 2);
  }
}

function getDarkenedColor(color, factor) {
  var rgba = getRGBA(color);
  return 'rgba(' + Math.round(factor * rgba[0]) + ',' + Math.round(factor * rgba[1]) + ',' + Math.round(factor * rgba[2]) + ',' + (rgba[3] || 1) + ')';
}

function getRGBA(colorSpec) {
  // Normalize variety of CSS color spec syntaxes to one of two
  gc.cache.fillStyle = colorSpec, colorSpec = gc.cache.fillStyle;

  var rgba = colorSpec.match(REGEXP_CSS_HEX6);
  if (rgba) {
    rgba.shift(); // remove whole match
    rgba.forEach(function(val, idx) {
      rgba[idx] = parseInt(val, 16);
    });
  } else {
    rgba = colorSpec.match(REGEXP_CSS_RGB);
    if (!rgba) {
      throw 'Unexpected format getting CanvasRenderingContext2D.fillStyle';
    }
    rgba.shift(); // remove whole match
  }

  return rgba;
}


//Extend HyperGrid's base Renderer
var sparkStarRatingRenderer = grid.cellRendererBase.extend({
    paint: paintSparkRating
});

//Register your renderer
grid.registerCellRenderer(sparkStarRatingRenderer, "Starry");


// Using your new render
grid.behavior.dataModel.getCell = function(config, rendererName) {
    //Retrieve the Singleton
    var starryRenderer = this.grid.cellRenderers.get('Starry');
        idxOfStarColumn = 5;

    if (config.x === idxOfStarColumn){
        config.domain= 100; // default is 100
        config.sizeFactor =  0.65; // default is 0.65; size of stars as fraction of height of cell
        config.darkenFactor = 0.75; // default is 0.75; star stroke color as fraction of star fill color
        config.color = 'gold'; // default is 'gold'; star fill color
        return starry;
    } 

    return starryRenderer;
};

Rendering in HyperGrid

Note that HyperGrid...

  • is lazy in regards to rendering. It relies on explicit calls to grid.repaint() (sometimes made on your behalf), to request a redraw of the canvas.
  • throttles multiple calls to repaint to 60 FPS.
  • every re-render is a complete re-render; there is no partial re-rendering.
  • for efficiency reasons, the grid lines that divide cells and establish their boundaries and painted separately and not part of the individual cell renders.

Animating Renderers

When wanting to do an animation within a cell renderer, you will need to set your own animation interval for calling repaint You can additionally check for grid repaint events by listening on the fin-grid-rendered event like so

    grid.addEventListener('fin-grid-rendered', function(e) {
       //Do something 
    });

Cells as Links

Hypergrid supports clickable link cells, to achieve this you need to...

  • register a listener to the table for 'fin-cell-click'
    jsonGrid.addFinEventListener('fin-cell-click', function(e){
      var cell = e.detail.cell;
      if (cell.x !== 0) {
          return;
      }
      alert('fin-cell-click at (' + cell.x + ', ' + cell.y + ')');
    });
  • override the getCursorAt method on behavior to be a function that returns the string of the name of the cursor for the column with the links
    grid.behavior.getCursorAt = function(x,y) {
      if (x === 0) {
          return 'pointer'
      } else {
          return null;
      }
    };
  • override the cell-provider to return the linkRenderer for the desired link columns and set config.link = true
    grid.behavior.dataModel.getCell = function(config, renderName) {
      config.link = true;
      var defaultRenderer = this.grid.cellRenderers.get(rendererName);
      config.halign = 'left';
      var x = config.x;
      if (x === 0) {
          config.link = true;
      } else if (x === 2) {
      ...
      ...
      ...
    }