This document provides a comprehensive evaluation and documentation of the ScreenGrid plugin glyph ecosystem, including built-in plugins, custom plugins, architecture, API details, and usage patterns.
The plugin glyph system in ScreenGrid enables developers to create reusable, named glyph visualizations that can be registered and used across multiple layers. This system provides:
onDrawCell callbacks continue to work with highest precedence✅ Implemented and Functional — The plugin system is fully integrated into ScreenGridLayerGL and ready for production use.
src/glyphs/GlyphRegistry.js) — Core registry for managing pluginssrc/glyphs/GlyphUtilities.js) — Low-level drawing utilitiesonDrawCell patternsGlyphUtilities methodsThe system uses a clear precedence order for glyph rendering:
onDrawCell callback (full backward compatibility)glyph nameThis ensures backward compatibility while allowing gradual migration to the plugin system.
The GlyphRegistry is the central component for managing glyph plugins.
// Register a plugin
GlyphRegistry.register(name, plugin, { overwrite = false })
// Retrieve a plugin
GlyphRegistry.get(name)
// Check if plugin exists
GlyphRegistry.has(name)
// List all registered plugins
GlyphRegistry.list()
// Unregister a plugin
GlyphRegistry.unregister(name)
// Clear all plugins (use with caution)
GlyphRegistry.clear()
import { GlyphRegistry } from 'screengrid';
GlyphRegistry.register('myCustomGlyph', {
draw(ctx, x, y, normalizedValue, cellInfo, config) {
// Drawing logic here
},
init({ layer, config }) {
// Optional initialization
return { destroy: () => {} };
},
getLegend(gridData, config) {
// Optional legend data
}
}, { overwrite: true }); // Overwrite if exists
Map for fast lookupsScreenGrid includes four built-in plugins that are automatically registered on import. These plugins wrap the underlying GlyphUtilities methods.
circle — Simple Circle GlyphDescription: Draws a filled circle glyph with customizable size, color, and opacity.
Parameters (glyphConfig):
radius (number): Circle radius (default: auto-calculated from cell size)color (string): Fill color (default: '#ff6b6b')alpha (number): Opacity 0-1 (default: 0.8)colorScale (function): Optional color scale function (v) => colorUsage:
const layer = new ScreenGridLayerGL({
data,
glyph: 'circle',
glyphConfig: {
radius: 15,
color: '#3498db',
alpha: 0.9
},
enableGlyphs: true
});
Underlying Utility: GlyphUtilities.drawCircleGlyph()
bar — Bar Chart GlyphDescription: Draws a horizontal bar chart showing multiple values side-by-side.
Parameters (glyphConfig):
values (array): Array of numeric values (default: extracted from cellData)maxValue (number): Maximum value for scaling (default: max of values)colors (array): Array of colors for bars (default: ['#ff6b6b', '#4ecdc4', '#45b7d1'])Usage:
const layer = new ScreenGridLayerGL({
data,
glyph: 'bar',
glyphConfig: {
values: [10, 20, 15], // Or let it auto-extract from cellData
maxValue: 100,
colors: ['#e74c3c', '#3498db', '#2ecc71']
},
enableGlyphs: true
});
Underlying Utility: GlyphUtilities.drawBarGlyph()
Data Extraction: If values not provided, automatically extracts weights from cellInfo.cellData.
pie — Pie Chart GlyphDescription: Draws a pie chart showing proportional distribution of values.
Parameters (glyphConfig):
values (array): Array of numeric values for slices (default: extracted from cellData)radius (number): Pie radius (default: auto-calculated from cell size)colors (array): Array of colors for slices (default: ['#ff6b6b', '#4ecdc4', '#45b7d1'])Usage:
const layer = new ScreenGridLayerGL({
data,
glyph: 'pie',
glyphConfig: {
values: [30, 40, 30], // Percentages or raw values
radius: 20,
colors: ['#e74c3c', '#3498db', '#2ecc71']
},
enableGlyphs: true
});
Underlying Utility: GlyphUtilities.drawPieGlyph()
heatmap — Heatmap Intensity GlyphDescription: Draws a circle whose color intensity represents a normalized value (0-1).
Parameters (glyphConfig):
radius (number): Circle radius (default: auto-calculated from cell size)colorScale (function): Color scale function (v) => colorString (default: red intensity scale)Usage:
const layer = new ScreenGridLayerGL({
data,
glyph: 'heatmap',
glyphConfig: {
radius: 15,
colorScale: (v) => `rgba(255, ${255 * (1 - v)}, 0, ${Math.min(0.9, v)})`
},
enableGlyphs: true
});
Underlying Utility: GlyphUtilities.drawHeatmapGlyph()
Custom plugins can be created by implementing the plugin interface. The ecosystem currently includes one documented custom plugin:
grouped-bar — Grouped Bar Chart PluginLocation: examples/plugin-glyph.html (lines 203-391)
Description: A sophisticated custom plugin that visualizes parking capacity data comparing bike racks vs. parking spaces using grouped bars. This plugin demonstrates advanced plugin features including global state management, cross-cell normalization, and interactive hover effects.
Features:
getLegend() methodinit() and destroy())Key Implementation Details:
const GroupedBarGlyph = {
// Global stats for normalization across all cells
globalStats: {
maxRacks: 0,
maxSpaces: 0,
maxTotal: 0,
initialized: false
},
init({ layer, config } = {}) {
// Store layer reference and reset global stats
this.layerRef = layer;
this.globalStats = { maxRacks: 0, maxSpaces: 0, maxTotal: 0, initialized: false };
return {
destroy() {
console.log('GroupedBarGlyph instance destroyed');
}
};
},
draw(ctx, x, y, normalizedValue, cellInfo, config = {}) {
// 1. Aggregate data from cellData (racks and spaces)
// 2. Calculate chart dimensions
// 3. Draw grouped bars for each category (racks vs spaces)
// 4. Apply color schemes (blue for racks, green for spaces)
// 5. Show comparison outline when hovering over different cell
},
getLegend(gridData, config = {}) {
return {
type: 'custom',
title: 'Parking Capacity',
description: 'Grouped bar chart comparing bike racks and parking spaces:',
items: [
{ label: 'Racks', description: 'Number of bike racks (blue)', color: 'rgba(52, 152, 219, 0.85)' },
{ label: 'Spaces', description: 'Number of parking spaces (green)', color: 'rgba(46, 204, 113, 0.85)' }
]
};
}
};
GlyphRegistry.register('grouped-bar', GroupedBarGlyph, { overwrite: true });
Usage:
const layer = new ScreenGridLayerGL({
data,
glyph: 'grouped-bar',
glyphConfig: { glyphSize: null },
enableGlyphs: true
});
A plugin is a JavaScript object with the following optional methods:
draw(ctx, x, y, normalizedValue, cellInfo, config)Description: Main drawing method called for each cell during rendering.
Parameters:
ctx (CanvasRenderingContext2D): Canvas 2D rendering contextx (number): Center X coordinate of the cell in screen spacey (number): Center Y coordinate of the cell in screen spacenormalizedValue (number): Normalized aggregate value (0-1) for the cellcellInfo (object): Cell metadata object containing:
cellData (array): Array of original data points in this cellcellSize (number): Cell size in pixelsglyphRadius (number): Recommended glyph radius based on cell sizeaggregatedValue (number): Raw aggregated valueconfig (object): Custom configuration object passed from glyphConfig layer optionReturn Value: undefined (void)
Example:
draw(ctx, x, y, normalizedValue, cellInfo, config) {
const radius = config.radius || cellInfo.glyphRadius;
ctx.fillStyle = config.color || '#3498db';
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
}
init({ layer, config })Description: Called when a layer using this plugin is created. Allows plugin to initialize per-layer state.
Parameters:
layer (ScreenGridLayerGL): The layer instance using this pluginconfig (object): The glyphConfig object from layer optionsReturn Value: Optional object with destroy() method, or null/undefined
Example:
init({ layer, config }) {
console.log(`Initializing plugin for layer ${layer.id}`);
const state = {
counter: 0,
destroy() {
console.log('Plugin instance destroyed');
}
};
return state;
}
Storage: The returned object is stored in layer._glyphInstance and can be accessed in destroy().
destroy({ layer })Description: Called when a layer using this plugin is removed. Allows cleanup of per-layer resources.
Parameters:
layer (ScreenGridLayerGL): The layer instance being removedReturn Value: undefined (void)
Note: If init() returned an object with a destroy() method, that method is called instead of the plugin’s destroy() method.
Example:
destroy({ layer }) {
// Cleanup resources, close connections, etc.
console.log(`Cleaning up plugin for layer ${layer.id}`);
}
getLegend(gridData, config)Description: Optional method to provide legend metadata for the glyph. Used by the Legend module.
Parameters:
gridData (object): Aggregation result dataconfig (object): The glyphConfig object from layer optionsReturn Value: Object with legend metadata, or null/undefined
Example:
getLegend(gridData, config) {
return {
type: 'custom',
title: 'My Glyph Legend',
description: 'Description of what the glyph shows',
items: [
{ label: 'Item 1', description: 'Description 1', color: '#ff0000' },
{ label: 'Item 2', description: 'Description 2', color: '#00ff00' }
]
};
}
Legend Integration: The Legend module checks for getLegend() when a plugin is used and automatically generates legend content.
The plugin system is integrated into ScreenGridLayerGL at several points:
_initGlyphPlugin()) /**
* Initialize glyph plugin for this layer if configured
* @private
*/
_initGlyphPlugin() {
if (!this.config || !this.config.glyph) return;
try {
const plugin = GlyphRegistry.get(this.config.glyph);
if (plugin && typeof plugin.init === 'function') {
// Allow plugin.init to return a per-layer instance/state
this._glyphInstance = plugin.init({ layer: this, config: this.config.glyphConfig || {} }) || null;
}
} catch (e) {
console.error(`Glyph plugin init failed for "${this.config.glyph}":`, e);
this._glyphInstance = null;
}
}
_destroyGlyphPlugin()) /**
* Destroy glyph plugin instance for this layer
* @private
*/
_destroyGlyphPlugin() {
if (!this.config || !this.config.glyph) return;
try {
const plugin = GlyphRegistry.get(this.config.glyph);
// If plugin returned an instance with destroy, prefer that
if (this._glyphInstance && typeof this._glyphInstance.destroy === 'function') {
this._glyphInstance.destroy();
} else if (plugin && typeof plugin.destroy === 'function') {
plugin.destroy({ layer: this });
}
} catch (e) {
console.error(`Glyph plugin destroy failed for "${this.config.glyph}":`, e);
} finally {
this._glyphInstance = null;
}
}
_draw()) // Determine the onDrawCell behavior. Priority:
// 1. user-provided onDrawCell callback
// 2. registered glyph via `config.glyph` (uses GlyphRegistry)
// 3. no onDrawCell -> color-mode rendering
let onDrawCell = this.config.onDrawCell || null;
if (!onDrawCell && this.config.glyph) {
const plugin = GlyphRegistry.get(this.config.glyph);
if (plugin && typeof plugin.draw === 'function') {
// Wrap plugin.draw to match the onDrawCell signature and pass glyphConfig
const glyphCfg = this.config.glyphConfig || {};
onDrawCell = (ctxArg, x, y, normVal, cellInfo) => {
try {
plugin.draw(ctxArg, x, y, normVal, cellInfo, glyphCfg);
} catch (e) {
console.error(`Glyph plugin "${this.config.glyph}" threw an error:`, e);
}
};
} else {
console.warn(`Glyph "${this.config.glyph}" not found in GlyphRegistry`);
}
}
The Legend module automatically detects and uses plugin legends:
// From src/legend/Legend.js (simplified)
if (this.layer.config.glyph) {
const plugin = GlyphRegistry.get(this.layer.config.glyph);
if (plugin && typeof plugin.getLegend === 'function') {
const pluginLegend = plugin.getLegend(gridData, this.layer.config.glyphConfig);
// Use pluginLegend to generate legend content
}
}
Use a built-in plugin with default settings:
const layer = new ScreenGridLayerGL({
data,
getPosition: (d) => d.coordinates,
glyph: 'circle',
enableGlyphs: true
});
Configure a built-in plugin via glyphConfig:
const layer = new ScreenGridLayerGL({
data,
getPosition: (d) => d.coordinates,
glyph: 'pie',
glyphConfig: {
colors: ['#e74c3c', '#3498db', '#2ecc71'],
radius: 25
},
enableGlyphs: true
});
Define and register a custom plugin, then use it:
import { GlyphRegistry, ScreenGridLayerGL } from 'screengrid';
// Define plugin
const MyGlyph = {
draw(ctx, x, y, normalizedValue, cellInfo, config) {
const radius = config.radius || cellInfo.glyphRadius;
ctx.fillStyle = config.color || '#3498db';
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
}
};
// Register
GlyphRegistry.register('myGlyph', MyGlyph);
// Use
const layer = new ScreenGridLayerGL({
data,
glyph: 'myGlyph',
glyphConfig: { radius: 15, color: '#ff6600' },
enableGlyphs: true
});
Use initialization and cleanup for stateful plugins:
const StatefulGlyph = {
init({ layer, config }) {
const state = {
cache: new Map(),
destroy() {
state.cache.clear();
}
};
return state;
},
draw(ctx, x, y, normalizedValue, cellInfo, config) {
// Use state if needed
// Drawing logic
}
};
GlyphRegistry.register('stateful', StatefulGlyph);
Include legend generation for better user experience:
const LegendGlyph = {
draw(ctx, x, y, normalizedValue, cellInfo, config) {
// Drawing logic
},
getLegend(gridData, config) {
return {
type: 'custom',
title: 'My Visualization',
items: [
{ label: 'Category A', color: '#ff0000' },
{ label: 'Category B', color: '#00ff00' }
]
};
}
};
GlyphRegistry.register('legendGlyph', LegendGlyph);
Register plugins conditionally or from external sources:
async function loadCustomGlyph(url) {
const module = await import(url);
GlyphRegistry.register(module.name, module.glyph, { overwrite: true });
}
// Or register multiple plugins
const plugins = {
gauge: GaugePlugin,
sparkline: SparklinePlugin,
tree: TreeMapPlugin
};
Object.entries(plugins).forEach(([name, plugin]) => {
GlyphRegistry.register(name, plugin);
});
draw() methods execute synchronously during renderdraw() Fast: Minimize computations in draw(); precompute during aggregationcellInfo during aggregationonAggregate callback, not in draw()// Good: Precompute during aggregation
layer.setConfig({
onAggregate: (gridData) => {
gridData.cells.forEach(cell => {
// Precompute statistics
cell.computedStats = computeStats(cell.cellData);
});
},
glyph: 'myGlyph'
});
// Plugin uses precomputed data
const MyGlyph = {
draw(ctx, x, y, normalizedValue, cellInfo, config) {
// Use cellInfo.computedStats instead of recalculating
const stats = cellInfo.computedStats;
// Fast drawing using precomputed data
}
};
Monitor plugin performance:
const ProfiledGlyph = {
draw(ctx, x, y, normalizedValue, cellInfo, config) {
const start = performance.now();
// Drawing logic
drawGlyph(ctx, x, y, cellInfo);
const duration = performance.now() - start;
if (duration > 1) { // Warn if > 1ms
console.warn(`Glyph draw took ${duration.toFixed(2)}ms`);
}
}
};
init() return valueinit() and destroy() per layergetLegend() supportThe GlyphUtilities class provides low-level drawing utilities that can be used within plugins:
drawCircleGlyph(ctx, x, y, radius, color, alpha) — Draw a filled circledrawBarGlyph(ctx, x, y, values, maxValue, cellSize, colors) — Draw horizontal barsdrawPieGlyph(ctx, x, y, values, radius, colors) — Draw pie chart slicesdrawScatterGlyph(ctx, x, y, points, cellSize, color) — Draw scatter plot pointsdrawDonutGlyph(ctx, x, y, values, outerRadius, innerRadius, colors) — Draw donut chartdrawHeatmapGlyph(ctx, x, y, radius, normalizedValue, colorScale) — Draw heatmap circledrawRadialBarGlyph(ctx, x, y, values, maxValue, maxRadius, color) — Draw radial barsdrawTimeSeriesGlyph(ctx, x, y, timeSeriesData, cellSize, options) — Draw time series line chartimport { GlyphUtilities } from 'screengrid';
const MyGlyph = {
draw(ctx, x, y, normalizedValue, cellInfo, config) {
// Use built-in utilities
GlyphUtilities.drawCircleGlyph(ctx, x, y, 10, '#3498db', 0.8);
// Or combine multiple utilities
GlyphUtilities.drawPieGlyph(ctx, x - 15, y, [1, 2, 3], 8, ['#e74c3c', '#3498db', '#2ecc71']);
GlyphUtilities.drawBarGlyph(ctx, x + 15, y, [10, 20], 100, cellInfo.cellSize, ['#ff0000', '#00ff00']);
}
};
The plugin glyph ecosystem is fully functional and production-ready. It provides:
circle, bar, pie, heatmapgrouped-bar (example)import { ScreenGridLayerGL, GlyphRegistry } from 'screengrid';
// Option 1: Use built-in
const layer = new ScreenGridLayerGL({
data,
glyph: 'circle',
enableGlyphs: true
});
// Option 2: Create custom
GlyphRegistry.register('myGlyph', {
draw(ctx, x, y, normVal, cellInfo, config) {
// Your drawing logic
}
});
const layer2 = new ScreenGridLayerGL({
data,
glyph: 'myGlyph',
enableGlyphs: true
});
docs/PLUGIN_GLYPHS.mddocs/GLYPH_DRAWING_GUIDE.mdexamples/plugin-glyph.htmlsrc/glyphs/GlyphRegistry.jssrc/glyphs/GlyphUtilities.jsLast Updated: Based on codebase evaluation as of current state