How to Implement Time-Slider Functionality for Raster Time Series in GeoLibre
GeoLibre's Time Slider plugin (maplibre-gl-time-slider) enables interactive exploration of raster time series through a three-part architecture: a UI dock for timeline control, binding logic for timestamp detection and filtering, and plugin glue that synchronizes raster sources with the application store.
The GeoLibre project—an open-source geospatial data platform—provides a production-ready implementation for time-slider functionality that handles Cloud-Optimized GeoTIFFs (COG), raster mosaics, Zarr data cubes, and vector layers with temporal properties. The system automatically detects time-aware data, manages playback controls, and persists state across sessions.
Time Slider Architecture Overview
GeoLibre's time-slider implementation consists of three tightly coupled components:
| Component | Responsibility | Source File |
|---|---|---|
| Control (UI Dock) | Timeline visualization, source management, playback, theme synchronization | apps/geolibre-desktop/src/lib/time-slider-dock.ts |
| Binding Logic | Timestamp column discovery, TimeBinding construction, MapLibre filter generation |
packages/plugins/src/plugins/time-slider-binding.ts |
| Plugin Glue | Store synchronization, raster source mirroring, bound layer orchestration | packages/plugins/src/plugins/maplibre-time-slider.ts |
This architecture separates concerns while maintaining tight integration: the control manages what users see, the binding logic interprets data semantics, and the plugin coordinates state between MapLibre GL JS and GeoLibre's application store.
Core Implementation Steps
1. Activate the Time Slider Plugin
Plugin activation creates a TimeSliderControl instance and registers map callbacks. In maplibre-time-slider.ts, the activate() method instantiates the control and calls attachStoreSync() to listen for source changes and theme updates.
// From packages/plugins/src/plugins/maplibre-time-slider.ts
activate() {
const control = new TimeSliderControl(savedConfig);
this.map.addControl(control, 'bottom-left');
this.attachStoreSync(control);
}
The control persists across the session and can be restored from serialized configuration via controlFromConfig().
2. Mirror Raster Sources to the Store
When raster time-series sources are added, syncStoreLayers() iterates through control.getSources() and creates corresponding store layers. The createStoreLayer() function in maplibre-time-slider.ts builds layers with specific metadata:
const storeLayer = {
id: source.id,
layerType: 'raster',
sourceKind: 'time-slider', // Marks as external-native raster
identifiable: true, // Supports pixel value queries
clientRenderedRaster: true, // Uses client-side rendering pipeline
// ... additional metadata
};
This mirroring enables GeoLibre's layer management UI to control time-slider sources while the plugin maintains synchronization with MapLibre's source system.
3. Detect and Bind Time Properties
For vector layers, time-slider-binding.ts provides detectTimeProperties() to scan feature collections for candidate timestamp columns. The detection handles multiple formats:
- Epoch seconds (Unix timestamps)
- Epoch milliseconds (JavaScript-style timestamps)
- ISO 8601 strings (
2023-01-15T14:30:00Z) - Bare years (
2019,2020)
import { detectTimeProperties, buildTimeBinding } from '@geolibre/plugins';
// Scan layer source for time-aware properties
const candidates = detectTimeProperties(vectorSource.geojson);
const bestCandidate = candidates[0]; // Highest coverage wins
const binding = buildTimeBinding(vectorSource.geojson, bestCandidate.property);
// binding contains: property name, valueKind, min/max extent, granularity, cumulative flag
The buildTimeBinding() function in packages/plugins/src/plugins/time-slider-binding.ts normalizes raw values to epoch milliseconds via parseTimeValue(), determines appropriate granularity via pickGranularity(), and constructs filter expressions compatible with the detected valueKind.
4. Apply Dynamic Time Filters
On each timeline tick, applyBoundFilters() updates vector layer visibility. The buildTimeFilter() function generates MapLibre filter expressions that respect the property's original format:
// For epoch-millisecond properties: direct numeric comparison
["all", [">=", ["get", "timestamp"], currentStart], ["<=", ["get", "timestamp"], currentEnd]]
// For year properties: integer comparison
["all", [">=", ["get", "year"], currentYearStart], ["<=", ["get", "year"], currentYearEnd]]
Filters are stored in layer.timeFilter and applied through MapLibre's setFilter() API.
5. Handle Raster Mosaics and COGs
For mosaic sources, ensureSourceBounds() fetches geographic extents once via loadMosaic() from maplibre-gl-raster, caching results in sourceBounds to avoid repeated requests. The dock's pixel-identify chart queries raster values at the current timeline position through adapters in time-slider-pixel-identify.ts.
Configuration and State Persistence
Saving Project State
The plugin serializes complete time-slider configuration via getProjectState():
getProjectState() {
return {
timeSlider: {
sources: this.control.getSources(),
dateRange: this.control.getDateRange(),
granularity: this.control.getGranularity(),
// undefined values stripped for clean JSON
}.filter(([_, v]) => v !== undefined)
};
}
Restoring from Configuration
On project load, applyProjectState() validates and normalizes configuration before reconstruction:
applyProjectState(state) {
const config = normalizeConfig(state.timeSlider);
const control = controlFromConfig(config);
this.activate(control);
}
Auto-Close Behavior and Idle Detection
The dock implements automatic cleanup when no temporal data remains active. In time-slider-dock.ts, shouldCloseTimeSliderDock() evaluates three conditions:
function shouldCloseTimeSliderDock(): boolean {
return openedByBinding && // Dock was auto-opened by binding
pluginActive && // Plugin is currently active
isTimeSliderIdle(); // No bound layers, no sources, no KML TimeSpans
}
The isTimeSliderIdle() helper checks:
- Zero bound vector layers
- Zero dock sources (
control.getSources().length === 0) - Zero active KML
<TimeSpan>overlays
This prevents orphaned UI elements when users remove time-aware layers (addresses GitHub issue #1512).
Practical Implementation Examples
Adding a COG Time Series via API
import { TimeSliderControl, type SourceSpec } from 'maplibre-gl-time-slider';
const landsatSpec: SourceSpec = {
id: 'landsat-8-ndvi',
type: 'cog',
url: 'https://landsat-data.s3.amazonaws.com/ndvi-timeseries.tif',
name: 'Landsat 8 NDVI (2013-2023)',
visible: true,
opacity: 0.9,
// Optional: explicit date range override
dateRange: ['2013-04-11', '2023-04-11']
};
const control = map.getControl('time-slider') as TimeSliderControl;
control.addSource(landsatSpec);
The source automatically appears in the dock's source list and the GeoLibre layer tree.
Programmatic Vector Layer Binding
import { useAppStore } from '@geolibre/core';
import {
detectTimeProperties,
buildTimeBinding,
buildTimeFilter
} from '@geolibre/plugins';
async function bindLayerToSlider(layerId: string) {
const store = useAppStore.getState();
const layer = store.layers.find(l => l.id === layerId);
if (layer?.type !== 'vector') return;
// Detect candidates from feature sample
const sampleFeatures = await fetchSampleFeatures(layer.source);
const candidates = detectTimeProperties(sampleFeatures);
if (candidates.length === 0) {
console.warn('No time properties detected');
return;
}
// Create binding with automatic granularity detection
const binding = buildTimeBinding(sampleFeatures, candidates[0].property);
// Update layer metadata to enable slider driving
store.updateLayer(layerId, {
metadata: { timeBinding: binding },
// Initial filter for full time range
timeFilter: buildTimeFilter(binding, binding.min, binding.max)
});
}
Conditional Dock Closure
import {
isTimeSliderIdle,
setTimeSliderOpenedByBinding,
shouldCloseTimeSliderDock
} from './time-slider-dock';
// When user removes a time-bound layer
function onLayerRemove(layerId: string) {
const wasBound = store.layers.find(l => l.id === layerId)?.metadata?.timeBinding;
if (wasBound && setTimeSliderOpenedByBinding(true)) {
// Check if any temporal content remains
if (shouldCloseTimeSliderDock()) {
geoLibreApp.deactivatePlugin('maplibre-gl-time-slider');
}
}
}
Key Implementation Files
| File | Lines | Purpose |
|---|---|---|
packages/plugins/src/plugins/maplibre-time-slider.ts |
1,160 | Main plugin: activation, store sync, source mirroring, range reconciliation |
packages/plugins/src/plugins/time-slider-binding.ts |
490 | Timestamp detection, TimeBinding construction, filter generation |
apps/geolibre-desktop/src/lib/time-slider-dock.ts |
52 | Dock state management, theme synchronization, auto-close logic |
packages/plugins/src/plugins/time-slider-source-url.ts |
— | URL parsing utilities for mosaic manifest detection |
packages/plugins/src/plugins/time-slider-pixel-identify.ts |
— | Raster pixel value querying for chart visualization |
packages/plugins/src/plugins/time-slider-pixel-series.ts |
— | Time-series chart generation for selected pixels |
Summary
-
Three-component architecture separates UI control (
time-slider-dock.ts), data semantics (time-slider-binding.ts), and system integration (maplibre-time-slider.ts) -
Automatic format detection handles epoch seconds, milliseconds, ISO dates, and bare years through
detectTimeProperties()andparseTimeValue() -
Source mirroring creates GeoLibre store layers for every time-slider source, enabling unified layer management with
sourceKind: "time-slider"metadata -
Dynamic filtering generates MapLibre-compatible expressions via
buildTimeFilter(), applied on every timeline tick throughapplyBoundFilters() -
State persistence serializes complete configuration via
getProjectState()and restores vianormalizeConfig()andcontrolFromConfig() -
Auto-cleanup removes idle docks when
isTimeSliderIdle()detects no bound layers, sources, or KML TimeSpans
Frequently Asked Questions
What raster formats does the GeoLibre time slider support?
The time slider supports Cloud-Optimized GeoTIFFs (COG) and raster mosaics through the maplibre-gl-raster library. COGs are accessed via single-file URLs with internal tiling, while mosaics reference multi-date stacks with external metadata manifests. Zarr data cubes are supported through additional adapters. The source type is specified in the SourceSpec as type: 'cog' or type: 'mosaic'.
How does the plugin detect which vector property contains timestamps?
The detectTimeProperties() function in time-slider-binding.ts samples features and scores properties by temporal pattern matching. It evaluates coverage (percentage of non-null values), value distribution, and format consistency. Properties with epoch values, ISO date strings, or year integers receive higher scores. The highest-scoring candidate becomes the default binding target, though users can override this selection through the UI.
Can the time slider animate through irregular time intervals?
Yes. The TimeBinding object includes min, max, and granularity properties derived from actual data extent. For irregular intervals—common in satellite imagery collections—the slider advances to the next available timestamp rather than fixed calendar increments. The cumulative flag enables accumulation modes where all data up to the current date remains visible, useful for change-detection visualizations.
What triggers automatic dock closure, and how do I prevent it?
The dock closes automatically when three conditions align: it was originally opened by a binding (not manually), the plugin is active, and isTimeSliderIdle() returns true (no bound layers, no sources, no KML TimeSpan overlays). To prevent closure, ensure at least one time-aware layer remains bound, add a persistent source via control.addSource(), or manually open the dock to clear the openedByBinding flag.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →