Skip to content
SuperScheduler

Pro modulesApplies toSuperScheduler Pro

Minimap and derived metrics

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.

Verified against v0.1.0 · reviewed October 7, 2026.md

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.

src/PlanningWithOverview.tsxtsx
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.

src/utilizationSeries.tsts
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.

src/FleetPlanning.tsxtsx
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".

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

OptionDefaultEffect
height28Strip height in pixels; month initials need 24 or more
peak'relative''absolute' measures bars against max
max1The value that fills a bar, with peak: 'absolute'
tone(value, index)all 'base''base', 'warn' or 'danger' per bucket
marksall truetoday (a tick at the browser's current date), months (separators and initials, the year in January), past (earlier buckets washed out)
rangethe timelineThe span the strip covers
labelsEnglish or Spanishlabel (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:

TokenFalls back to
--super-scheduler-minimap-basethe accent
--super-scheduler-minimap-warn#f59e0b
--super-scheduler-minimap-danger#ef4444
--super-scheduler-minimap-pastmuted text
--super-scheduler-minimap-todaythe accent
--super-scheduler-minimap-monthsthe border color
--super-scheduler-minimap-labelmuted text
--super-scheduler-minimap-brushthe 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

InputEffect
Drag the brushPans the timeline
Drag either edge of the brushZooms: outward shows more time, inward less; the opposite edge stays fixed, within zoomGesture min and max
Click the strip outside the brushScrolls so that date is in the middle (animated unless reduced motion is on)
Left / Right, Down / UpOne day earlier or later; with Shift, seven days
PageUp / PageDownOne month earlier or later
Home / EndStart 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:

src/ProductionOverview.tsxtsx
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, 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.

Fleet rental planningA compact is grounded on pickup day. Hand its rental to another car, keep the cleaning slot and see where the fleet runs out. Manufacturing order schedulingMaintenance was brought forward. Move the order out of the way and keep its operations in sequence. Port berth planningA ship arrives twelve hours late. Move its berth window, then bring its tug and cranes along. Hotel room planningA shower leaks in Room 104. Rehouse the next guest, block the room for the plumber and find the nights that are already full.