# schedgrid/time

URL: https://schedgrid.fabio.cv/docs/time

timeAxis maps a time range onto fractional indices, plus ticks.

```ts
import { timeAxis } from 'schedgrid/time';
```

The grid never sees a `Date`. `timeAxis` maps a continuous time range onto
axis indices so that index `i` covers `[start + i·slot, start + (i+1)·slot)`.
Everything is plain millisecond arithmetic, so a day that contains a DST
change simply has fewer or more slots than 24 hours would.

## timeAxis(options) [#timeaxisoptions]

**TimeAxisOptions**

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `start` | `number \| Date` | yes |  |
| `end` | `number \| Date` | yes |  |
| `slot` | `number` | yes | Length of one index in milliseconds: 15 minutes is `15 * 60_000`. |
| `size` | `SizeSpec` | yes | Pixel size of one slot, constant or per index. |

```ts
const day = timeAxis({
  start: new Date(2026, 8, 7),
  end: new Date(2026, 8, 8),
  slot: 15 * 60_000,
  size: 24,
});

day.count; // 96
day.axis; // { count: 96, size: 24 } — pass to createGrid or setColumns
day.toIndex(new Date(2026, 8, 7, 9, 37)); // 38.47
day.toRange(a, b); // [toIndex(a), toIndex(b)], ready for an Item
day.fromIndex(42); // Date at 10:30
```

## The axis [#the-axis]

**TimeAxis**

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `start` | `number` | yes |  |
| `end` | `number` | yes |  |
| `slot` | `number` | yes |  |
| `count` | `number` | yes |  |
| `axis` | `AxisOptions` | yes | Drop-in value for `grid.setColumns(...)` or `grid.setRows(...)`. |
| `toIndex` | `(time: Date \| number) => number` | yes | Fractional index of a moment. Not clamped: before `start` is negative. |
| `fromIndex` | `(index: number) => Date` | yes |  |
| `toRange` | `(start: Date \| number, end: Date \| number) => [number, number]` | yes | Fractional index range for an interval, ready for an `Item`. |
| `ticks` | `(every: number, options?: { align?: Date \| number; }) => Tick[]` | yes | Moments every `every` ms from `align` (default `start`) that fall inside `[start, end]`. Steps are fixed lengths, so use this for hours and minutes; calendar steps (days across DST, months) are not fixed lengths — compute those `Date`s yourself and map them with `toIndex`. |

`toIndex` is not clamped: a moment before `start` maps to a negative index.
That is deliberate, so an item that starts before the visible range still
lands in the right place once you offset it.

`timeAxis` throws a `RangeError` if `slot` is not greater than zero — a
zero-length slot has no meaningful index — and `ticks` throws the same for
`every`. Nothing else in the library throws.

## Ticks [#ticks]

`ticks(every, { align })` returns the moments every `every` milliseconds,
counted from `align` (default `start`), that fall inside the range. Each
tick carries its fractional index, so drawing a gridline is
`columns.offsetAt(tick.index)`.

**Tick**

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `index` | `number` | yes | Fractional index on the axis; feed it to `offsetAt` to draw it. |
| `time` | `Date` | yes |  |

Steps are fixed lengths, so use `ticks` for hours and minutes. Calendar
steps (days across DST, months) are not fixed lengths. Compute those
`Date`s yourself and map them with `toIndex`.

## Offsetting for a label column [#offsetting-for-a-label-column]

The grid's column 0 is often a pinned label column, which means time slot
`i` lives in grid column `i + 1`. Keep that offset in one place:

```ts
const columns = {
  count: day.count + 1,
  size: (i: number) => (i === 0 ? 160 : 24),
  pinned: 1,
};

const [c0, c1] = day.toRange(start, end);
const item = { id, rows: [row, row + 1], columns: [c0 + 1, c1 + 1] };
```