# Minimap and derived metrics

> Add a timeline overview with super-scheduler/minimap, feed it utilization or any metric your application computes, and style, label and dispose of it correctly.

Source: https://superscheduler.org/en/docs/minimap-metrics/
Reviewed: 2026-10-07

Render SchedulerMinimap from super-scheduler/minimap with the control from useSchedulerControl(); with no options it shows how many events overlap each day. For a business metric such as utilization or occupancy, pass a series function your application computes per bucket, with peak: 'absolute', max: 1 and a tone function for warning and danger colors. Dragging the brush pans the timeline, dragging its edges zooms, and the brush works from the keyboard as a slider.

A year of bookings does not fit on screen. The minimap is a thin strip under (or above) the scheduler that shows the whole timeline at once: one bar per bucket of time, with a brush marking the period in view. Users see where the busy weeks are and jump there. The bars show whatever number your application decides, which makes the strip a compact chart of utilization, occupancy, load or revenue.

The minimap needs SuperScheduler Pro.

## Add a minimap
`SchedulerMinimap` is the React component. It needs the scheduler's control, which exists only after the scheduler mounts; `useSchedulerControl()` gives you `control` as state (`null` first), and the minimap accepts `null` and waits.

```tsx
// src/PlanningWithOverview.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { SchedulerMinimap } from 'super-scheduler/minimap'
import 'super-scheduler/styles.css'

export function PlanningWithOverview(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  // `control` is null until the scheduler has mounted; the minimap waits for it.
  const { controlRef, control } = useSchedulerControl()
  const events = useMemo(() => props.events.slice(), [props.events])

  return (
    <>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-01-01"
        days={365}
        scale="Day"
        cellWidth={40}
        resources={props.resources}
        events={events}
      />
      {/* Without `series`, the strip shows how many events overlap each day. */}
      <SchedulerMinimap control={control} height={32} className="planning-minimap" />
    </>
  )
}
```
You should see a 32 px strip with month initials, a tick for today, earlier days washed out, and a brush covering the visible weeks. Drag the brush and the scheduler scrolls with it.

Without `series`, the strip uses `eventDensity(control)`: the number of events that overlap each bucket. It counts in short background tasks (up to 8 ms or 15,000 events per task), caches the result and repaints when done, so a large store never blocks the page.

## How the strip is built
The minimap divides a time range into buckets and draws one value per bucket:

- **Range.** By default, the control's timeline (from `startDate` for `days`); with infinite scrolling, the window currently generated. `range: { start, end }` fixes another span, for example a whole year while the scheduler shows a month.
- **Buckets.** One day each; one hour with `scale: 'Hour'` or `'Minute'`; one week when the range is longer than 730 days.
- **Values.** Your series returns one number per bucket. When there are more buckets than pixels, neighboring values are averaged into one bar per pixel column, aligned to device pixels.
- **Height.** With `peak: 'relative'` (default) the tallest bar is the largest value. With `peak: 'absolute'`, bars are measured against `max` (default 1), so a full day always looks full.

## Feed it your own metric
`series` is either a `Float32Array` that covers the whole range, or a function that receives the range (`start`, `end`, `buckets`, `bucketMs`) and returns one value per bucket. The function form adapts when the user zooms and the bucket size changes.

The library does not know what "busy" means for your business, so the metric is your code. This one computes utilization: the booked share of the available time, for any number of resources.

```ts
// src/utilizationSeries.ts
import { SuperScheduler } from 'super-scheduler'
import type { MinimapRange, MinimapSeries } from 'super-scheduler/minimap'

export interface Booking {
  /** ISO wall-clock values with seconds; `end` is exclusive. */
  readonly start: string
  readonly end: string
}

/**
 * Booked share of the available time in each bucket: 0 is idle, 1 is every resource busy
 * for the whole bucket. The application decides what "capacity" means; here it is the
 * number of bookable resources.
 */
export function utilizationSeries(bookings: readonly Booking[], capacity: number): MinimapSeries {
  // Parse once; the series function runs again on every redraw.
  const spans = bookings.map((booking) => ({
    start: new SuperScheduler.Date(booking.start).getTime(),
    end: new SuperScheduler.Date(booking.end).getTime(),
  }))

  return (range: MinimapRange) => {
    const values = new Float32Array(range.buckets)
    const origin = range.start.getTime()
    const available = range.bucketMs * Math.max(1, capacity)
    for (const span of spans) {
      // Half-open [start, end): a booking ending at midnight does not touch the next day.
      const first = Math.max(0, Math.floor((span.start - origin) / range.bucketMs))
      const last = Math.min(range.buckets, Math.ceil((span.end - origin) / range.bucketMs))
      for (let i = first; i < last; i++) {
        const bucketStart = origin + i * range.bucketMs
        const overlap =
          Math.min(span.end, bucketStart + range.bucketMs) - Math.max(span.start, bucketStart)
        if (overlap > 0) values[i] = (values[i] ?? 0) + overlap / available
      }
    }
    return values
  }
}
```
With two vans, one booked all day and the other from noon, the day reads 0.75. Intervals are half-open, as in the scheduler: a booking that ends at midnight does not touch the next day.

```tsx
// src/FleetPlanning.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { SchedulerMinimap } from 'super-scheduler/minimap'
import type { MinimapLabels, MinimapTone } from 'super-scheduler/minimap'
import { utilizationSeries } from './utilization-series'

// Module-level: the minimap receives the same functions on every render.
const tone = (value: number): MinimapTone =>
  value >= 0.95 ? 'danger' : value >= 0.8 ? 'warn' : 'base'

const LABELS: Partial<MinimapLabels> = {
  label: 'Fleet utilization overview',
  valueText: (start, end) =>
    `Showing ${start.toString('d MMM yyyy')} to ${end.toString('d MMM yyyy')}`,
}

export function FleetPlanning(props: {
  vehicles: SuperScheduler.ResourceData[]
  bookings: SuperScheduler.EventData[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const events = useMemo(() => props.bookings.slice(), [props.bookings])

  // Recomputed only when the data changes; a new series function makes the strip redraw.
  const series = useMemo(
    () =>
      utilizationSeries(
        props.bookings.map((booking) => ({
          start: String(booking.start),
          end: String(booking.end),
        })),
        props.vehicles.length,
      ),
    [props.bookings, props.vehicles.length],
  )

  return (
    <>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-01-01"
        days={365}
        scale="Day"
        cellWidth={40}
        resources={props.vehicles}
        events={events}
      />
      <SchedulerMinimap
        control={control}
        series={series}
        // 1 means full, whatever the busiest bucket is.
        peak="absolute"
        max={1}
        tone={tone}
        labels={LABELS}
        marks={{ today: true, months: true, past: true }}
        height={32}
        className="fleet-minimap"
      />
    </>
  )
}
```
Now the strip reads as a utilization chart: calm bars that deepen with the value, amber from 80 %, red from 95 %, and a screen reader hears "Fleet utilization overview, Showing 1 Jan 2026 to 26 Jan 2026".

> **Tip:**
> For hotel stays (check-in 14:00, check-out 11:00), time-weighted utilization never reaches 1 on a full night. Count nights instead: a room is occupied on a day when a stay covers that night, and the value is occupied rooms divided by rooms.

For the common case of weighting events instead of counting them, `eventDensity(control, { weight })` takes a function of the event data (hours, units, guests). The imperative snippet below uses it.

## Tones, peak, marks and labels
| Option | Default | Effect |
|---|---|---|
| `height` | `28` | Strip height in pixels; month initials need 24 or more |
| `peak` | `'relative'` | `'absolute'` measures bars against `max` |
| `max` | `1` | The value that fills a bar, with `peak: 'absolute'` |
| `tone(value, index)` | all `'base'` | `'base'`, `'warn'` or `'danger'` per bucket |
| `marks` | all `true` | `today` (a tick at the browser's current date), `months` (separators and initials, the year in January), `past` (earlier buckets washed out) |
| `range` | the timeline | The span the strip covers |
| `labels` | English or Spanish | `label` (the brush's accessible name), `zoom` (resize instructions) and `valueText(start, end)` |

Default labels are English, or Spanish when the scheduler's `locale` starts with `es`: "Visible period", "Drag either edge to zoom, or use + and −", and the visible dates as `yyyy-MM-dd – yyyy-MM-dd`. Provide `labels` for any other language.

Colors come from tokens, which fall back to the scheduler theme. Set them on the minimap's container or any ancestor:

| Token | Falls back to |
|---|---|
| `--super-scheduler-minimap-base` | the accent |
| `--super-scheduler-minimap-warn` | `#f59e0b` |
| `--super-scheduler-minimap-danger` | `#ef4444` |
| `--super-scheduler-minimap-past` | muted text |
| `--super-scheduler-minimap-today` | the accent |
| `--super-scheduler-minimap-months` | the border color |
| `--super-scheduler-minimap-label` | muted text |
| `--super-scheduler-minimap-brush` | the accent |

The strip repaints when the theme changes: a `class`, `data-theme` or `data-color-scheme` change on `<html>`, the scheduler root or the container, or a change of the system color scheme.

## Brush interactions
| Input | Effect |
|---|---|
| Drag the brush | Pans the timeline |
| Drag either edge of the brush | Zooms: outward shows more time, inward less; the opposite edge stays fixed, within `zoomGesture` min and max |
| Click the strip outside the brush | Scrolls so that date is in the middle (animated unless reduced motion is on) |
| Left / Right, Down / Up | One day earlier or later; with Shift, seven days |
| PageUp / PageDown | One month earlier or later |
| Home / End | Start or end of the range |
| `+` or `=`, `-` or `−` | Zoom in or out around the center |

The brush is a focusable `role="slider"` with `aria-valuetext`; the canvas is hidden from assistive technology. With `cellWidthSpec: 'Auto'`, the edge grips and the zoom keys are off, because the scheduler fits the whole timeline anyway. Pointer moves are applied once per animation frame.

## Imperative API and disposal
`createMinimap(control, container, options)` creates the strip inside any element and returns `{ element, update, refresh, dispose }`. It throws if the control has not been initialized, so in React create it in an effect keyed on `control`:

```tsx
// src/ProductionOverview.tsx
import { useEffect, useMemo, useRef } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { createMinimap, eventDensity } from 'super-scheduler/minimap'

type Order = { quantity: number }

export function ProductionOverview(props: {
  lines: SuperScheduler.ResourceData[]
  orders: SuperScheduler.EventData<Order>[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const host = useRef<HTMLDivElement>(null)
  const events = useMemo(() => props.orders.slice(), [props.orders])

  useEffect(() => {
    // createMinimap needs an initialized control: run it after mount, keyed on the control.
    if (control === null || host.current === null) return
    const minimap = createMinimap(control, host.current, {
      height: 28,
      // Each order weighs its quantity instead of counting 1.
      series: eventDensity(control, {
        weight: (e) => (e as SuperScheduler.EventData<Order>).quantity,
      }),
      labels: { label: 'Production load overview' },
    })
    // Releases observers and pending work; control.dispose() does it too.
    return () => minimap.dispose()
  }, [control])

  return (
    <>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-01"
        days={90}
        scale="Day"
        resources={props.lines}
        events={events}
      />
      <div ref={host} className="production-minimap" />
    </>
  )
}
```
- `update(partialOptions)` changes options and repaints;
- `refresh()` asks for the series again;
- `dispose()` removes the strip and releases its observers, listeners and pending work. Disposing the control does it too.

`SchedulerMinimap` does all of this for you: it creates the strip when `control` becomes available, recreates it if the control changes, and disposes of it on unmount.

## Keeping the strip current
The minimap repaints, and calls a series function again, when:

- the control's events change (a drag, an API call, a load);
- a zoom ends, the container is resized, or the theme changes;
- you call `refresh()` or `update()`.

Repaints wait while a gesture runs and happen once it ends. A series function that reads the control's own events is therefore always current. A series computed from your application's data is current when you pass a new function after that data changes, as `useMemo` does in the utilization snippet.

`SchedulerMinimap` calls `update` with its props on every render of its parent. Memoize `series`, `tone` and `labels` (or define them at module level) so a re-render does not compute the series again for nothing.

## What your application owns
- **The metric.** What counts as capacity, which events count (tentative, cancelled, blocks) and how to weight them.
- **Data you have not loaded.** The series sees only what your code gives it. With [range loading](https://superscheduler.org/en/docs/range-loading/), only part of the year may be in memory: for a whole-year view, fetch per-day aggregates from your backend and pass them as the series together with a fixed `range`.
- **Thresholds and wording.** Tone limits, labels and their translations.

## Related
→ https://superscheduler.org/en/examples/fleet-rentals/
→ https://superscheduler.org/en/examples/manufacturing-orders/
→ https://superscheduler.org/en/examples/port-berths/
→ https://superscheduler.org/en/examples/hotel-rooms/
- [Time scales and zoom](https://superscheduler.org/en/docs/time-scales-zoom/) for the zoom limits the brush edges respect.
- [Themes, tokens, Tailwind and dark mode](https://superscheduler.org/en/docs/theming/) for the tokens the minimap falls back to.
