The refactored ScreenGrid library follows clean architecture principles with separated concerns and modular design. The monolithic screengrid.js has been decomposed into focused, reusable modules.
src/
├── index.js # Main export file
├── ScreenGridLayerGL.js # Orchestrator class (~220 lines)
│
├── config/
│ └── ConfigManager.js # Configuration management
│
├── core/
│ ├── Aggregator.js # Grid aggregation (pure logic)
│ ├── Projector.js # Coordinate projection
│ ├── CellQueryEngine.js # Cell queries & spatial analysis
│ └── geometry/ # Geometry placement (v2.1.0+)
│ ├── PlacementEngine.js # Geometry to anchor conversion
│ ├── PlacementValidator.js # Config validation
│ ├── GeometryUtils.js # Geometry utilities
│ └── strategies/ # Placement strategies
│ ├── CentroidStrategy.js
│ ├── PolylabelStrategy.js
│ ├── LineSampleStrategy.js
│ ├── GridGeoStrategy.js
│ ├── GridScreenStrategy.js
│ └── PointStrategy.js
│
├── canvas/
│ ├── CanvasManager.js # Canvas lifecycle & sizing
│ └── Renderer.js # Drawing logic
│
├── events/
│ ├── EventBinder.js # Event attachment/detachment
│ └── EventHandlers.js # Event handler implementations
│
├── glyphs/
│ ├── GlyphUtilities.js # Reusable glyph drawing functions
│ └── GlyphRegistry.js # Plugin registry (v2.0.0+)
│
├── aggregation/ # Aggregation system (v2.1.0+)
│ ├── AggregationModeRegistry.js # Mode registry
│ ├── modes/ # Aggregation modes
│ │ ├── ScreenGridMode.js # Rectangular grid mode
│ │ ├── ScreenHexMode.js # Hexagonal mode (v2.2.0+)
│ │ └── index.js # Mode registration
│ └── functions/ # Aggregation functions
│ ├── AggregationFunctionRegistry.js
│ ├── SumAggregation.js
│ ├── MeanAggregation.js
│ ├── CountAggregation.js
│ ├── MaxAggregation.js
│ ├── MinAggregation.js
│ └── index.js
│
├── normalization/ # Normalization system (v2.1.0+)
│ └── functions/
│ ├── NormalizationFunctionRegistry.js
│ ├── MaxLocalNormalization.js
│ ├── MaxGlobalNormalization.js
│ ├── ZScoreNormalization.js
│ ├── PercentileNormalization.js
│ └── index.js
│
├── utils/ # Utility modules (v2.1.0+)
│ ├── Logger.js # Debug logging
│ └── DataUtilities.js # Data processing utilities
│
└── legend/ # Legend system (v2.0.0+)
├── Legend.js # Main legend class
├── LegendDataExtractor.js # Data extraction
└── LegendRenderers.js # Rendering utilities
config/ConfigManager.jsPurpose: Centralized configuration management
Key Methods:
ConfigManager.create(options) - Create config from options with defaultsConfigManager.update(config, updates) - Merge updates into existing configConfigManager.isValid(config) - Validate configuration structureUsage:
import { ConfigManager } from 'screengrid';
const config = ConfigManager.create({
data: myData,
cellSizePixels: 50,
colorScale: (v) => [255 * v, 100, 200, 200]
});
// Update config later
const updated = ConfigManager.update(config, { cellSizePixels: 75 });
Benefits:
core/Projector.jsPurpose: Transform geographic coordinates to screen space
Key Methods:
Projector.projectPoints(data, getPosition, getWeight, map) - Static methodUsage:
import { Projector } from 'screengrid';
const projector = new Projector(map);
const projected = projector.project(
data,
(d) => d.coordinates,
(d) => d.weight
);
// Returns: [{x, y, w}, {x, y, w}, ...]
Benefits:
core/Aggregator.jsPurpose: Pure grid aggregation logic
Key Methods:
Aggregator.aggregate(projectedPoints, originalData, width, height, cellSize) - StaticgetStats(aggregationResult) - Get grid statistics📖 For detailed explanation of the aggregation and normalization process, see API_REFERENCE.md
Usage:
import { Aggregator } from 'screengrid';
const aggregator = new Aggregator();
const result = aggregator.aggregate(
projectedPoints, // [{x, y, w}, ...]
data, // Original data array
800, // Canvas width
600, // Canvas height
50 // Cell size
);
// Result contains:
// {
// grid: [0, 5, 10, ...], // Aggregated values
// cellData: [[], [item1, ...], ...], // Raw data per cell
// cols: 16,
// rows: 12,
// ...
// }
const stats = aggregator.getStats(result);
// Returns: {totalCells, cellsWithData, maxValue, minValue, avgValue, totalValue}
Benefits:
core/CellQueryEngine.jsPurpose: Spatial queries on the aggregated grid
Key Methods:
CellQueryEngine.getCellAt(aggregationResult, point) - Get cell at positiongetCellsInBounds(aggregationResult, bounds) - Get cells in rectanglegetCellsAboveThreshold(aggregationResult, threshold) - Filter by valueUsage:
import { CellQueryEngine } from 'screengrid';
const engine = new CellQueryEngine(aggregationResult);
// Get cell at mouse position
const cell = engine.getCellAt({x: 100, y: 200});
// Returns: {col, row, value, cellData, x, y, cellSize, index}
// Get all cells in a region
const cells = engine.getCellsInBounds({
minX: 0, minY: 0, maxX: 400, maxY: 300
});
// Get high-value cells
const hotspots = engine.getCellsAboveThreshold(50);
Benefits:
canvas/CanvasManager.jsPurpose: Canvas lifecycle management (create, resize, cleanup, DPI handling)
Key Methods:
init(map) - Initialize overlay canvas on mapgetContext() - Get 2D rendering contextresize() - Handle canvas resizing with DPI scalingclear() - Clear canvas contentgetDisplaySize() - Get canvas dimensions in CSS pixelscleanup() - Remove canvas and cleanup observersUsage:
import { CanvasManager } from 'screengrid';
const canvasManager = new CanvasManager();
canvasManager.init(map);
const ctx = canvasManager.getContext();
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 50, 50);
// Automatically handles resize
// On cleanup:
canvasManager.cleanup();
Benefits:
canvas/Renderer.jsPurpose: Draw grid cells (color-based or glyph-based)
Key Methods:
Renderer.render(aggregationResult, ctx, config) - Render gridrenderGlyphs(aggregationResult, ctx, onDrawCell, glyphSize) - Glyph moderenderColors(aggregationResult, ctx, colorScale) - Color modeUsage:
import { Renderer } from 'screengrid';
const renderer = new Renderer();
// Color-based rendering
renderer.renderColors(aggregationResult, ctx, (value) => {
return [255 * value, 100, 200, 200]; // [r, g, b, a]
});
// Custom glyph rendering
renderer.renderGlyphs(
aggregationResult,
ctx,
(ctx, x, y, normVal, cellInfo) => {
// Draw custom visualization
ctx.fillStyle = 'blue';
ctx.fillRect(x - 10, y - 10, 20, 20);
},
0.8 // glyphSize
);
Benefits:
events/EventHandlers.jsPurpose: Event handler implementations
Key Methods:
EventHandlers.handleHover(event, cellQueryEngine, onHover)handleClick(event, cellQueryEngine, onClick)handleZoom(map, config, onZoom)handleMove(onMove)Usage:
import { EventHandlers } from 'screengrid';
// Hover handling
EventHandlers.handleHover(
mapLibreEvent,
cellQueryEngine,
({cell, event}) => {
console.log('Hovered cell:', cell);
}
);
// Zoom handling with cell size adjustment
EventHandlers.handleZoom(map, config, () => {
console.log('Map zoomed');
});
Benefits:
events/EventBinder.jsPurpose: Manage event attachment and detachment
Key Methods:
bind(map, eventHandlers) - Attach events to mapunbind() - Detach all eventsbindEvent(eventName, handler) - Bind specific eventunbindEvent(eventName) - Unbind specific eventUsage:
import { EventBinder } from 'screengrid';
const binder = new EventBinder();
binder.bind(map, {
handleHover: (e) => console.log('hover'),
handleClick: (e) => console.log('click'),
handleZoom: () => console.log('zoom'),
handleMove: () => console.log('move')
});
// Later, unbind
binder.unbind();
Benefits:
glyphs/GlyphUtilities.jsPurpose: Reusable glyph drawing functions
Key Methods:
GlyphUtilities.drawCircleGlyph(ctx, x, y, radius, color, alpha)drawBarGlyph(ctx, x, y, values, maxValue, cellSize, colors)drawPieGlyph(ctx, x, y, values, radius, colors)drawScatterGlyph(ctx, x, y, points, cellSize, color)drawDonutGlyph(ctx, x, y, values, outerRadius, innerRadius, colors) ✨ NEWdrawHeatmapGlyph(ctx, x, y, radius, normalizedValue, colorScale) ✨ NEWdrawRadialBarGlyph(ctx, x, y, values, maxValue, maxRadius, color) ✨ NEWdrawTimeSeriesGlyph(ctx, x, y, timeSeriesData, cellSize, options) ✨ NEWUsage:
import { GlyphUtilities } from 'screengrid';
// Use in custom glyph drawing
const onDrawCell = (ctx, x, y, normVal, cellInfo) => {
GlyphUtilities.drawCircleGlyph(ctx, x, y, 15, '#ff0000', 0.8);
};
// Or use directly
GlyphUtilities.drawPieGlyph(ctx, 100, 100, [30, 20, 10], 20, ['red', 'green', 'blue']);
Benefits:
glyphs/GlyphRegistry.js (v2.0.0+)Purpose: Registry for glyph plugins
Key Methods:
GlyphRegistry.register(name, plugin, { overwrite })GlyphRegistry.get(name)GlyphRegistry.has(name)GlyphRegistry.list()GlyphRegistry.unregister(name)Usage:
import { GlyphRegistry } from 'screengrid';
// Register custom plugin
GlyphRegistry.register('myGlyph', {
draw(ctx, x, y, normVal, cellInfo, config) {
// Drawing logic
}
});
// Use by name
const layer = new ScreenGridLayerGL({
glyph: 'myGlyph',
enableGlyphs: true
});
Benefits:
aggregation/ (v2.1.0+)Purpose: Aggregation modes and functions system
Components:
Usage:
import { AggregationModeRegistry, AggregationFunctions } from 'screengrid';
// Use hex mode
const layer = new ScreenGridLayerGL({
aggregationMode: 'screen-hex',
aggregationModeConfig: { hexSize: 50 }
});
// Use custom aggregation function
const layer = new ScreenGridLayerGL({
aggregationFunction: AggregationFunctions.mean
});
Benefits:
normalization/ (v2.1.0+)Purpose: Normalization functions system
Components:
Usage:
import { NormalizationFunctions } from 'screengrid';
// Use z-score normalization
const layer = new ScreenGridLayerGL({
normalizationFunction: NormalizationFunctions.zScore
});
// Use global normalization with context
const layer = new ScreenGridLayerGL({
normalizationFunction: NormalizationFunctions.maxGlobal,
normalizationContext: { globalMax: 1000 }
});
Benefits:
utils/ (v2.1.0+)Purpose: Utility modules
Components:
Controlled debug logging:
import { Logger, setDebug } from 'screengrid';
setDebug(true);
Logger.log('Debug message');
Data processing utilities:
import { groupBy, extractAttributes, computeStats, groupByTime } from 'screengrid';
// Group by category
const categories = groupBy(cellData, 'category');
// Extract multiple attributes
const attrs = extractAttributes(cellData, { total: w => w.weight });
// Compute statistics
const stats = computeStats(cellData);
// Group by time
const timeSeries = groupByTime(cellData, 'year', 'value');
Benefits:
legend/ (v2.0.0+)Purpose: Legend generation system
Components:
Usage:
import { Legend } from 'screengrid';
const legend = new Legend({
layer: gridLayer,
type: 'auto',
position: 'bottom-right',
title: 'Data Legend'
});
Legend Types:
color-scale: Continuous color scalecategorical: Discrete categoriestemporal: Time-basedsize-scale: Size-basedauto: Auto-detectmulti: Multi-attributeBenefits:
core/geometry/ (v2.1.0+)Purpose: Geometry placement for non-point inputs
Components:
Usage:
import { PlacementEngine, PlacementValidator } from 'screengrid';
// Validate config
const isValid = PlacementValidator.validate(placementConfig);
// Process features
const engine = new PlacementEngine(map);
const anchors = engine.processFeatures(features, placementConfig);
Benefits:
Purpose: Main class that composes all modules
Key Methods:
constructor(options) - Create layeronAdd(map, gl) - MapLibre lifecyclerender() - Render the layeronRemove() - CleanupsetData(newData) - Update datasetConfig(updates) - Update configgetCellAt(point) - Query cellgetCellsInBounds(bounds) - Query regiongetGridStats() - Get statisticsUsage (Unchanged from user perspective):
import { ScreenGridLayerGL } from 'screengrid';
const gridLayer = new ScreenGridLayerGL({
data: myData,
getPosition: (d) => d.coordinates,
getWeight: (d) => d.weight,
cellSizePixels: 50
});
map.addLayer(gridLayer);
Benefits:
import { Projector, Aggregator, CellQueryEngine } from 'screengrid';
// Use aggregation without rendering
const projector = new Projector(map);
const projected = projector.project(data, getPos, getWeight);
const aggregator = new Aggregator();
const result = aggregator.aggregate(projected, data, 800, 600, 50);
// Query results
const engine = new CellQueryEngine(result);
const stats = aggregator.getStats(result);
console.log(`Grid has ${stats.cellsWithData} cells with data`);
import { Renderer, GlyphUtilities } from 'screengrid';
const renderer = new Renderer();
renderer.renderGlyphs(result, ctx, (ctx, x, y, normVal, cellInfo) => {
// Use multiple glyphs
GlyphUtilities.drawCircleGlyph(ctx, x, y, 10, '#ff0000', normVal);
GlyphUtilities.drawRadialBarGlyph(
ctx, x, y,
cellInfo.cellData.map(d => d.weight),
Math.max(...cellInfo.cellData.map(d => d.weight)),
15,
'#ffffff'
);
});
import { EventBinder, EventHandlers, CellQueryEngine } from 'screengrid';
const binder = new EventBinder();
const engine = new CellQueryEngine(aggregationResult);
binder.bind(map, {
handleHover: (e) => {
const cell = engine.getCellAt({x: e.point.x, y: e.point.y});
if (cell) updateTooltip(cell);
},
handleClick: (e) => { /* ... */ },
handleZoom: () => { /* ... */ },
handleMove: () => { /* ... */ }
});
| Aspect | Before | After |
|---|---|---|
| File Size | 470 lines | 50-120 lines per module |
| Single Responsibility | Multiple per class | One per module |
| Testability | Hard to isolate logic | Pure functions, easy to test |
| Reusability | Tied to ScreenGridLayerGL | Modules reusable independently |
| Maintainability | Large monolith | Clear, focused modules |
| Extensibility | Hard to extend | Easy to add features |
| Coupling | High inter-dependencies | Loose coupling via composition |
| Code Cohesion | Mixed concerns | High cohesion per module |
No changes needed! The public API is identical:
// This still works exactly the same
import { ScreenGridLayerGL } from 'screengrid';
const layer = new ScreenGridLayerGL(options);
map.addLayer(layer);
New options for advanced usage:
// Import individual modules
import { Aggregator, Projector, CellQueryEngine } from 'screengrid';
// Use modules independently
const agg = new Aggregator();
const result = agg.aggregate(...);
// test/core/Aggregator.test.js
import { Aggregator } from 'screengrid';
describe('Aggregator', () => {
it('should aggregate points correctly', () => {
const result = Aggregator.aggregate(
[{x: 10, y: 10, w: 1}],
data,
100, 100, 50
);
expect(result.grid[0]).toBe(1);
});
});
// test/ScreenGridLayerGL.integration.test.js
import { ScreenGridLayerGL } from 'screengrid';
describe('ScreenGridLayerGL', () => {
it('should render grid on map', () => {
const layer = new ScreenGridLayerGL(options);
map.addLayer(layer);
expect(layer.gridData).toBeDefined();
});
});
The modular structure enables:
Aggregator on backendThe refactored ScreenGrid library provides:
✅ Modularity - Independent, composable modules
✅ Testability - Pure functions, easy unit tests
✅ Reusability - Use modules anywhere
✅ Maintainability - Clear separation of concerns
✅ Extensibility - Easy to add features
✅ Backward Compatibility - Public API unchanged
This architecture follows SOLID principles and makes the codebase more professional and sustainable for long-term development.