How the Time Slider Integrates with Zarr Layers Containing Internal Time Dimensions

The Time Slider in GeoLibre automatically detects, decodes, and animates temporal dimensions from Zarr data cubes by parsing CF time conventions and mapping slider positions to array slices.

GeoLibre's Time Slider provides seamless animation capabilities for multi-dimensional Zarr datasets without requiring renderer-specific code. This integration works purely on store metadata and coordinate values, enabling any CF-compliant Zarr cube to become time-navigable. The implementation spans three core modules that handle detection, decoding, and UI binding.

Detecting Temporal Dimensions in Zarr Stores

When a Zarr layer loads, the plugin inspects non-spatial dimensions to identify which axis represents time. The pickTimeDimension helper in packages/plugins/src/plugins/zarr-time-axis.ts implements a prioritized search strategy:

  • Named lookup — Checks against TIME_DIMENSION_NAMES (including "time", "valid_time", "t", "date", and variants)
  • Value sampling — Falls back to parsing raw values as timestamps if no name matches
// From zarr-time-axis.ts lines 26-34
const TIME_DIMENSION_NAMES = [
  'time', 't', 'valid_time', 'date', 'datetime',
  'forecast_reference_time', 'forecast_period'
];

// Dimension selection with caching
export async function pickTimeDimension(
  store: ZarrStore,
  dimensions: string[]
): Promise<string | undefined> {
  // First pass: exact name match
  const named = dimensions.find(d => 
    TIME_DIMENSION_NAMES.includes(d.toLowerCase())
  );
  if (named) return named;
  
  // Second pass: sample values for datetime patterns
  // ...
}

This detection runs once per store and benefits from an attributeCache (lines 30-34) to avoid redundant metadata fetches when the same Zarr source is bound to multiple layers.

Fetching and Parsing CF Time Metadata

Zarr stores encode temporal coordinates using Climate and Forecast (CF) conventions. Numeric values require decoding via units and calendar attributes. The fetchZarrTimeAttributes function traverses the Zarr hierarchy to locate these:

  1. Consolidated metadata (.zmetadata) — preferred for single-request efficiency
  2. Root store attributes (zarr.json or .zattrs)
  3. Per-dimension attributes (.zattrs within the dimension path)
// From zarr-time-axis.ts lines 48-69
export async function fetchZarrTimeAttributes(
  store: ZarrStore,
  dimension: string
): Promise<CfTimeAttributes | undefined> {
  // Try consolidated metadata first
  const consolidated = await getConsolidatedMetadata(store);
  if (consolidated?.metadata[`${dimension}/.zattrs`]?.units) {
    return extractAttributes(consolidated.metadata[`${dimension}/.zattrs`]);
  }
  
  // Fall back to individual attribute reads
  const zattrs = await store.getItem(`${dimension}/.zattrs`);
  // ...
}

Decoding CF Time Units to Epoch Milliseconds

The parseCfTimeUnits function transforms CF strings like "days since 1970-01-01" into machine-usable time steps. Only fixed-length units defined in CF_UNIT_MS are supported:

Unit Milliseconds
seconds 1000
minutes 60000
hours 3600000
days 86400000

Supported calendars are limited to Gregorian-compatible systems: standard, gregorian, proleptic_gregorian. This restriction ensures reliable arithmetic for animation stepping.

// From zarr-time-axis.ts lines 36-64, 68-73
const CF_UNIT_MS = {
  second: 1000, seconds: 1000, sec: 1000, s: 1000,
  minute: 60000, minutes: 60000, min: 60000,
  hour: 3600000, hours: 3600000, hr: 3600000, h: 3600000,
  day: 86400000, days: 86400000, d: 86400000
} as const;

export function parseCfTimeUnits(units: string): ParsedCfTime {
  const match = units.match(/^(\w+)\s+since\s+(.+)$/i);
  if (!match) throw new Error(`Invalid CF time units: ${units}`);
  
  const unitMs = CF_UNIT_MS[match[1].toLowerCase() as keyof typeof CF_UNIT_MS];
  if (!unitMs) throw new Error(`Unsupported time unit: ${match[1]}`);
  
  const epochMs = Date.parse(match[2]); // Reference date
  return { unitMs, epochMs, calendar: 'standard' };
}

Resolving the Complete Time Axis

resolveZarrTimeAxis orchestrates the full pipeline: dimension selection, attribute fetching, value decoding, and fallback handling. The result is a ZarrTimeAxis object containing:

  • dimension: The identified time axis name
  • values: Array of epoch-millisecond timestamps for slider positioning
// From zarr-time-axis.ts lines 94-109, 120-143
export async function resolveZarrTimeAxis(
  storeUrl: string,
  coordinates: Record<string, number[] | string[]>,
  options?: ZarrRequestOptions
): Promise<ZarrTimeAxis | undefined> {
  const dimension = await pickTimeDimension(store, Object.keys(coordinates));
  if (!dimension) return undefined;
  
  const rawValues = coordinates[dimension];
  const attributes = await fetchZarrTimeAttributes(store, dimension);
  
  // Decode numeric CF times or parse string dates
  const values = typeof rawValues[0] === 'number' && attributes
    ? decodeCfTimeValues(rawValues as number[], attributes)
    : rawValues.map(v => parseTimeValue(v as string));
  
  return { dimension, values };
}

If decoding fails (unsupported calendar, missing units), the system falls back to parseTimeValue (lines 124-134), treating raw entries as ISO date strings.

Binding to the Time Slider UI

The MapLibre-Time-Slider plugin bridges resolved axes to user interaction. In packages/plugins/src/plugins/maplibre-components.ts, layer loading triggers automatic axis resolution:

// Example: Adding a Zarr layer with automatic Time Slider binding
import { addZarrLayer } from "@geolibre/plugins";
import { useTimeSlider } from "@geolibre/ui";

await addZarrLayer({
  url: "https://example.com/my-cube.zarr",
  name: "Global Temperature",
});

// Slider automatically populated from CF time metadata
useTimeSlider().setCurrentTime(new Date("2022-01-01"));

The apps/geolibre-desktop/src/lib/time-slider-dock.ts component:

  1. Receives the ZarrTimeAxis from layer metadata
  2. Renders tick marks at calculated positions
  3. Maps slider thumb position to array index
  4. Updates the layer's data request with the selected time slice

Direct API Access for Advanced Use

Application developers rarely need low-level APIs, but custom plugins can leverage them:

import {
  resolveZarrTimeAxis,
  fetchZarrTimeAttributes,
} from "@geolibre/plugins";

const axis = await resolveZarrTimeAxis(
  "https://example.com/my-cube.zarr",
  { time: [0, 1, 2, 3], latitude: [90, 0, -90], longitude: [-180, 0, 180] },
  { headers: { Authorization: "Bearer token" } }
);

if (axis) {
  console.log("Dimension:", axis.dimension);  // "time"
  console.log("Timestamps:", new Date(axis.values[0]));  // Epoch ms → Date
}

Performance Optimizations

The integration includes targeted caching strategies:

  • attributeCache — Stores fetched .zattrs and .zmetadata per store URL
  • Lazy resolution — Time axes compute only when layers activate the Time Slider
  • Index-based slicing — Avoids re-fetching metadata during animation; only the array index changes

Summary

  • Detection: pickTimeDimension in zarr-time-axis.ts identifies temporal axes by name convention or value sampling
  • Decoding: fetchZarrTimeAttributes and parseCfTimeUnits implement CF convention parsing for Gregorian calendars and fixed-length units
  • Resolution: resolveZarrTimeAxis produces epoch-millisecond timelines for any compliant Zarr store
  • Binding: The Time Slider UI (time-slider-dock.ts) maps user interaction to Zarr array slices via maplibre-components.ts
  • Portability: No renderer dependency—works with MapLibre raster, vector, and custom plugin layers

Frequently Asked Questions

What CF time units does GeoLibre support?

GeoLibre supports seconds, minutes, hours, and days with Gregorian-compatible calendars (standard, gregorian, proleptic_gregorian). The parseCfTimeUnits function rejects variable-length units like months or years to ensure consistent animation stepping. As noted in zarr-time-axis.ts lines 68-73, non-Gregorian calendars (e.g., 360_day, 365_day) fall back to string parsing rather than numeric decoding.

Can the Time Slider animate Zarr layers without CF metadata?

Yes, through fallback string parsing. If fetchZarrTimeAttributes returns no valid units or the calendar is unsupported, resolveZarrTimeAxis calls parseTimeValue on each raw coordinate entry (lines 124-134). This handles ISO 8601 strings and common date formats, though animation smoothness depends on consistent formatting across the axis.

Where is the time axis cached, and for how long?

Metadata lookups cache in the module-level attributeCache Map defined at zarr-time-axis.ts lines 30-34. The cache persists for the application session and keys entries by store URL. Array values themselves are not cached—only the attribute metadata needed to decode them—keeping memory usage bounded while eliminating redundant network requests for repeated layer bindings.

How does the slider position map to Zarr array indices?

The Time Slider dock maintains a bidirectional mapping between epoch milliseconds (display coordinates) and original array indices. When the user drags the thumb, the UI finds the nearest timestamp in ZarrTimeAxis.values, retrieves its index, and passes that index to the layer's tile or data request. This decouples display time from storage order, supporting non-uniform or out-of-order Zarr time coordinates.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →