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.
- Overview
- Architecture Evaluation
- Plugin Registry System
- Built-in Plugins
- Custom Plugins
- Plugin API Reference
- Integration Points
- Usage Patterns
- Performance Considerations
- Limitations & Future Improvements
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:
- Reusability: Define glyphs once, use them multiple times
- Name-based Access: Reference glyphs by string name instead of function references
- Plugin Lifecycle: Support for initialization and cleanup hooks
- Legend Integration: Optional legend generation support
- Backward Compatibility: Existing
onDrawCellcallbacks continue to work with highest precedence
✅ Implemented and Functional — The plugin system is fully integrated into ScreenGridLayerGL and ready for production use.
- GlyphRegistry (
src/glyphs/GlyphRegistry.js) — Core registry for managing plugins - GlyphUtilities (
src/glyphs/GlyphUtilities.js) — Low-level drawing utilities - ScreenGridLayerGL — Layer integration with plugin support
- Legend Module — Optional legend generation support for plugins
- Lightweight & Simple: Minimal abstraction overhead, uses plain JavaScript objects
- Non-Breaking: Fully backward compatible with existing
onDrawCellpatterns - Flexible: Supports both simple drawing functions and complex plugins with lifecycle hooks
- Performance-Oriented: Synchronous execution path, no async overhead
- Error Resilient: Plugin errors are caught and logged, preventing render loop failures
- Registry Pattern: Centralized registration and retrieval of plugins
- Adapter Pattern: Built-in plugins wrap
GlyphUtilitiesmethods - Factory Pattern: Layer constructs wrapper functions from plugins
- Lifecycle Hooks: Optional initialization and cleanup methods
The system uses a clear precedence order for glyph rendering:
- Highest: User-provided
onDrawCellcallback (full backward compatibility) - Medium: Registered plugin via
glyphname - Lowest: Color-mode rendering (no glyphs)
This 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- In-Memory Storage: Uses JavaScript
Mapfor fast lookups - Global Scope: Registry is shared across all layers
- No Persistence: Plugins are registered at runtime, not persisted
- Thread Safety: Single-threaded JavaScript execution eliminates race conditions
ScreenGrid includes four built-in plugins that are automatically registered on import. These plugins wrap the underlying GlyphUtilities methods.
Description: 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) => color
Usage:
const layer = new ScreenGridLayerGL({
data,
glyph: 'circle',
glyphConfig: {
radius: 15,
color: '#3498db',
alpha: 0.9
},
enableGlyphs: true
});Underlying Utility: GlyphUtilities.drawCircleGlyph()
Description: Draws a horizontal bar chart showing multiple values side-by-side.
Parameters (glyphConfig):
values(array): Array of numeric values (default: extracted fromcellData)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.
Description: Draws a pie chart showing proportional distribution of values.
Parameters (glyphConfig):
values(array): Array of numeric values for slices (default: extracted fromcellData)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()
Description: 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:
Location: 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:
- Multivariate data aggregation (racks and spaces values)
- Category-specific color schemes (blue for racks, green for spaces)
- Global normalization for cross-cell comparison
- Integrated legend support via
getLegend()method - Lifecycle hooks (
init()anddestroy()) - Interactive hover effects with comparison outlines
- Global statistics tracking across all cells
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:
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 value- Additional properties may be present depending on aggregation configuration
config(object): Custom configuration object passed fromglyphConfiglayer option
Return 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();
}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): TheglyphConfigobject from layer options
Return 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().
Description: Called when a layer using this plugin is removed. Allows cleanup of per-layer resources.
Parameters:
layer(ScreenGridLayerGL): The layer instance being removed
Return 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}`);
}Description: Optional method to provide legend metadata for the glyph. Used by the Legend module.
Parameters:
gridData(object): Aggregation result dataconfig(object): TheglyphConfigobject from layer options
Return 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:
/**
* 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;
}
} /**
* 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;
}
} // 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);
});- Synchronous: Plugin
draw()methods execute synchronously during render - Per-Cell: Called once per visible grid cell on each render cycle
- Main Thread: Runs on the main thread (no Web Workers)
- Keep
draw()Fast: Minimize computations indraw(); precompute during aggregation - Cache Calculations: Store computed values in
cellInfoduring aggregation - Avoid Heavy Operations: No network requests, file I/O, or blocking operations
- Use Canvas Efficiently: Minimize context state changes, batch drawing operations
- Leverage Aggregation: Compute aggregations in
onAggregatecallback, not indraw()
// 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`);
}
}
};- No Async Support: Plugins cannot be async; must complete synchronously
- No Web Worker Support: All plugins run on main thread
- No Plugin Sandboxing: No isolation for untrusted third-party plugins
- Limited State Management: Per-layer state only via
init()return value - No Plugin Versioning: Registry doesn't track plugin versions
- No Plugin Metadata: No description, author, or dependency metadata
- No Dynamic Loading: Cannot load plugins from URLs or external sources (must import)
- Limited Error Recovery: Errors are logged but no retry or fallback mechanism
- Enhanced Lifecycle Hooks: Full support for
init()anddestroy()per layer - Legend Integration: Complete integration with Legend module's
getLegend()support - Plugin Sandboxing: Consider WebWorker or sandboxed execution for untrusted code
- Dynamic Loading: Allow registering plugins from URLs (with security considerations)
- Plugin Marketplace: Potential for a community plugin repository
- Plugin Validation: Schema validation for plugin structure
- Plugin Dependencies: Support for plugins that depend on other plugins
- Plugin Middleware: Chain of plugins or transformation pipeline
- Plugin Events: Plugin-to-plugin communication or event system
- Plugin Configuration UI: Auto-generate configuration UI from plugin schema
- Plugin Testing Framework: Utilities for testing plugins in isolation
- Plugin Performance Profiling: Built-in performance monitoring and reporting
- Plugin Hot Reloading: Development-time hot reloading for faster iteration
The 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 chart
import { 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:
- 4 Built-in Plugins:
circle,bar,pie,heatmap - 1 Documented Custom Plugin:
grouped-bar(example) - 8 Utility Methods: Low-level drawing functions available for custom plugins
- Full Integration: Seamlessly integrated into layer and legend systems
- Backward Compatible: No breaking changes to existing code
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
});- Design Doc:
docs/PLUGIN_GLYPHS.md - Usage Guide:
docs/GLYPH_DRAWING_GUIDE.md - Example:
examples/plugin-glyph.html - Registry Code:
src/glyphs/GlyphRegistry.js - Utilities Code:
src/glyphs/GlyphUtilities.js
Last Updated: Based on codebase evaluation as of current state