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) { ... ... ... }