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

> Learn how GeoLibre's Time Slider automatically animates temporal dimensions from Zarr data cubes by parsing CF time conventions and mapping slider positions to array slices.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-08-15

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/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

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/zarr.json) or `.zattrs`)
3. **Per-dimension attributes** (`.zattrs` within the dimension path)

```typescript
// 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.

```typescript
// 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

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/maplibre-components.ts), layer loading triggers automatic axis resolution:

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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:

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/time-slider-dock.ts)) maps user interaction to Zarr array slices via [`maplibre-components.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.