Date Adapters

Plug in custom date libraries for timezone and locale support.

Overview

The timeline uses a date adapter for all date operations: parsing input dates, calculating tick positions, snapping to time boundaries, and formatting labels. By default, a built-in adapter using the browser's native Date object handles everything. If you need timezone-aware rendering or custom locale formatting, you can register a custom adapter that wraps a library like Luxon or Day.js.

Built-in Native Adapter

The default adapter works out of the box with no configuration. It uses the browser's local timezone and supports the following format tokens for axis labels and tooltips:

TokenOutputExample
YYYYFull year2026
MMMMFull month nameJanuary
MMMShort month nameJan
MMZero-padded month01
DDay of month5
dddShort weekdayMon
HH24-hour hours14
hh12-hour hours02
mmMinutes05
ssSeconds03
SSSMilliseconds007
AUppercase AM/PMPM
aLowercase am/pmpm

Tokens can be combined freely: "ddd D MMMM HH:mm" produces "Mon 5 January 14:05".

The built-in adapter uses the browser's local timezone. All dates are displayed relative to the user's system clock. If you need to display events in a specific timezone (e.g. UTC, or a fixed office timezone), you'll need a custom adapter.

TempisTimelineDateAdapter Interface

A custom adapter must implement four methods. All operate on epoch milliseconds internally.

MethodSignatureDescription
parse(input: string | number | Date) => number | nullConvert a date input (ISO string, timestamp, or Date object) into epoch milliseconds. Return null if the input is invalid.
startOf(instant: number, unit: string) => numberSnap a timestamp to the start of the given unit (e.g. "day", "month", "year"). Used to calculate tick positions on the time axis.
add(instant: number, unit: string, amount: number) => numberAdd a number of calendar units to a timestamp. Supports negative amounts for subtraction. Used to step through ticks on the axis.
format(instant: number, pattern: string) => stringFormat a timestamp into a display string using the pattern tokens. Used for axis labels and default tooltips.

Supported Units

The startOf and add methods receive one of these unit strings:

year, month, week, day, hour, minute, second, millisecond

Registering an Adapter

import { AdapterRegistry } from '@tempis/timeline';

AdapterRegistry.register(myAdapter);
A registered adapter applies globally to all timeline instances. Register it once at application startup, before creating any timelines. There is no way to use different adapters for different instances.

When to Use a Custom Adapter

Example — Luxon Adapter

This adapter renders all dates in the America/New_York timezone using Luxon:

import { DateTime } from 'luxon';
import { AdapterRegistry } from '@tempis/timeline';
import type { TempisTimelineDateAdapter } from '@tempis/timeline';

const luxonAdapter: TempisTimelineDateAdapter = {
  parse(input) {
    if (input instanceof Date) return input.getTime();
    if (typeof input === 'number') return isNaN(input) ? null : input;
    const dt = DateTime.fromISO(input, { zone: 'America/New_York' });
    return dt.isValid ? dt.toMillis() : null;
  },

  startOf(instant, unit) {
    return DateTime.fromMillis(instant, { zone: 'America/New_York' })
      .startOf(unit as any)
      .toMillis();
  },

  add(instant, unit, amount) {
    return DateTime.fromMillis(instant, { zone: 'America/New_York' })
      .plus({ [unit + 's']: amount })
      .toMillis();
  },

  format(instant, pattern) {
    // Luxon uses different tokens — map common ones.
    const luxonPattern = pattern
      .replace('YYYY', 'yyyy')
      .replace('D', 'd')
      .replace('ddd', 'EEE');
    return DateTime.fromMillis(instant, { zone: 'America/New_York' })
      .toFormat(luxonPattern);
  }
};

// Register once at app startup.
AdapterRegistry.register(luxonAdapter);
Luxon and the native adapter use different format tokens. Your format method should either map the Tempis tokens to your library's tokens, or configure the timeline's range.minorUnit.formats and range.majorUnit.formats to use your library's native token syntax.

Example — Day.js Adapter

import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { AdapterRegistry } from '@tempis/timeline';
import type { TempisTimelineDateAdapter } from '@tempis/timeline';

dayjs.extend(utc);
dayjs.extend(timezone);

const TZ = 'Europe/London';

const dayjsAdapter: TempisTimelineDateAdapter = {
  parse(input) {
    if (input instanceof Date) return input.getTime();
    if (typeof input === 'number') return isNaN(input) ? null : input;
    const d = dayjs.tz(input, TZ);
    return d.isValid() ? d.valueOf() : null;
  },

  startOf(instant, unit) {
    return dayjs(instant).tz(TZ).startOf(unit as any).valueOf();
  },

  add(instant, unit, amount) {
    return dayjs(instant).tz(TZ).add(amount, unit as any).valueOf();
  },

  format(instant, pattern) {
    return dayjs(instant).tz(TZ).format(pattern);
  }
};

AdapterRegistry.register(dayjsAdapter);