A MapLibre GL JS plugin for visualizing time series vector and raster data with an interactive time slider control.
- Interactive time slider with play/pause controls
- Support for both vector and raster time series data
- Built-in TiTiler integration for Cloud Optimized GeoTIFFs (COGs)
- Add Layer button for persisting time periods as permanent layers for comparison
- Customizable playback speed and loop settings
- Control layer ordering with
beforeIdparameter - React component and hooks support
- TypeScript support with full type definitions
- Lightweight and easy to integrate
npm install maplibre-gl-time-sliderimport maplibregl from 'maplibre-gl';
import { TimeSliderControl } from 'maplibre-gl-time-slider';
import 'maplibre-gl-time-slider/style.css';
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
map.on('load', () => {
const timeSlider = new TimeSliderControl({
title: 'Time Slider',
labels: ['2024-01', '2024-02', '2024-03', '2024-04'],
speed: 1000,
loop: true,
onChange: (index, label) => {
console.log(`Current: ${label} (index: ${index})`);
// Update your map layers here
},
});
map.addControl(timeSlider, 'top-right');
});import { useState, useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import { TimeSliderControlReact } from 'maplibre-gl-time-slider/react';
import 'maplibre-gl-time-slider/style.css';
function MyMap() {
const mapContainer = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<maplibregl.Map | null>(null);
useEffect(() => {
if (!mapContainer.current) return;
const mapInstance = new maplibregl.Map({
container: mapContainer.current,
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
mapInstance.on('load', () => setMap(mapInstance));
return () => mapInstance.remove();
}, []);
return (
<>
<div ref={mapContainer} style={{ width: '100%', height: '100vh' }} />
{map && (
<TimeSliderControlReact
map={map}
title="Time Slider"
labels={['2024-01', '2024-02', '2024-03']}
onChange={(index, label) => {
// Update map layers
}}
/>
)}
</>
);
}Filter vector features by a time property:
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const timeSlider = new TimeSliderControl({
labels: months,
onChange: (index, label) => {
// Filter vector layer by month
map.setFilter('my-layer', ['==', ['get', 'month'], index]);
},
});Display time series raster data using TiTiler:
import { TimeSliderControl, buildTiTilerTileUrl } from 'maplibre-gl-time-slider';
const rasterData = {
'2024-01': 'https://example.com/cog_2024_01.tif',
'2024-02': 'https://example.com/cog_2024_02.tif',
'2024-03': 'https://example.com/cog_2024_03.tif',
};
const labels = Object.keys(rasterData);
const urls = Object.values(rasterData);
// Add initial raster source
map.addSource('raster-data', {
type: 'raster',
tiles: [
buildTiTilerTileUrl({
url: urls[0],
colormap: 'viridis',
}),
],
tileSize: 256,
});
map.addLayer({
id: 'raster-layer',
type: 'raster',
source: 'raster-data',
});
// Create time slider
const timeSlider = new TimeSliderControl({
labels: labels,
onChange: (index, label) => {
const source = map.getSource('raster-data');
source.setTiles([
buildTiTilerTileUrl({
url: urls[index],
colormap: 'viridis',
}),
]);
},
});Enable the "Add Layer" button to allow users to persist specific time periods as permanent layers:
let layerCounter = 0;
const timeSlider = new TimeSliderControl({
labels: ['2020', '2021', '2022', '2023'],
onChange: (index, label) => {
// Update the main time slider layer
const source = map.getSource('main-raster');
source.setTiles([getTileUrlForYear(label)]);
},
onAddLayer: (index, label, beforeId) => {
// Create a persistent layer for the selected time period
layerCounter++;
const sourceId = `persistent-source-${label}`;
const layerId = `Persistent Layer ${label}`;
// Add source
map.addSource(sourceId, {
type: 'raster',
tiles: [getTileUrlForYear(label)],
tileSize: 256,
});
// Add layer before the main time slider layer
map.addLayer({
id: layerId,
type: 'raster',
source: sourceId,
paint: {
'raster-opacity': 0.7,
},
}, beforeId);
console.log(`Added persistent layer for ${label}`);
},
beforeId: 'main-layer-id', // Insert persistent layers before this layer
});This feature is useful for:
- Comparing data across multiple time periods
- Creating side-by-side visualizations
- Highlighting specific time periods for analysis
- Building temporal comparisons in the layer control
Main control class that implements MapLibre's IControl interface.
| Option | Type | Default | Description |
|---|---|---|---|
labels |
string[] |
[] |
Array of labels to display (required) |
title |
string |
'Time Slider' |
Title displayed in the panel header |
collapsed |
boolean |
true |
Whether to start with panel collapsed |
position |
string |
'top-right' |
Control position on the map |
panelWidth |
number |
300 |
Width of the panel in pixels |
initialIndex |
number |
0 |
Initial index to start at |
speed |
number |
1000 |
Playback speed in milliseconds |
loop |
boolean |
true |
Whether to loop playback |
className |
string |
'' |
Custom CSS class name |
onChange |
function |
- | Callback when index changes: (index: number, label: string) => void |
onAddLayer |
function |
- | Callback when "Add Layer" button is clicked: (index: number, label: string, beforeId?: string) => void |
beforeId |
string |
- | Layer ID to insert new persistent layers before (ensures proper layer ordering) |
| Method | Description |
|---|---|
play() |
Start playback |
pause() |
Pause playback |
togglePlayback() |
Toggle play/pause state |
next() |
Move to next label |
prev() |
Move to previous label |
goTo(index) |
Navigate to specific index |
setSpeed(ms) |
Set playback speed |
setLoop(enabled) |
Set loop state |
setLabels(labels, resetIndex?) |
Update labels |
getCurrentIndex() |
Get current index |
getCurrentLabel() |
Get current label |
getLabels() |
Get all labels |
getState() |
Get full state object |
toggle() |
Toggle panel collapsed state |
expand() |
Expand panel |
collapse() |
Collapse panel |
on(event, handler) |
Register event listener |
off(event, handler) |
Remove event listener |
| Event | Description |
|---|---|
change |
Fired when the current index changes |
play |
Fired when playback starts |
pause |
Fired when playback pauses |
collapse |
Fired when panel collapses |
expand |
Fired when panel expands |
statechange |
Fired on any state change |
Builds a TiTiler XYZ tile URL for MapLibre raster sources.
const tileUrl = buildTiTilerTileUrl({
url: 'https://example.com/my-cog.tif',
endpoint: 'https://giswqs-titiler-endpoint.hf.space', // optional, default
colormap: 'viridis', // optional, default
rescale: [-10, 10], // optional
bidx: [1], // optional, band indexes
});Fetches the bounds of a COG file.
const bounds = await getTiTilerBounds('https://example.com/my-cog.tif');
map.fitBounds(bounds);Fetches metadata about a COG file.
Fetches statistics (min, max, mean, std) for each band.
# Install dependencies
npm install
# Start development server
npm run dev
# Build library
npm run build
# Build examples
npm run build:examples
# Run tests
npm test
# Lint code
npm run lint
# Format code
npm run formatThe examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
# Pull the latest image
docker pull ghcr.io/opengeos/maplibre-gl-time-slider:latest
# Run the container
docker run -p 8080:80 ghcr.io/opengeos/maplibre-gl-time-slider:latestThen open http://localhost:8080/maplibre-gl-time-slider/ in your browser to view the examples.
# Build the image
docker build -t maplibre-gl-time-slider .
# Run the container
docker run -p 8080:80 maplibre-gl-time-slider| Tag | Description |
|---|---|
latest |
Latest release |
x.y.z |
Specific version (e.g., 1.0.0) |
x.y |
Minor version (e.g., 1.0) |
MIT License - see LICENSE for details.