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.
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.
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
startDatefordays); 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. Withpeak: 'absolute', bars are measured againstmax(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.
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.
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
| 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:
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()orupdate().
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.
Related
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.
- Time scales and zoom for the zoom limits the brush edges respect.
- Themes, tokens, Tailwind and dark mode for the tokens the minimap falls back to.
Related examples
- FleetlineFleet 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.
- ForgeManufacturing order schedulingMaintenance was brought forward. Move the order out of the way and keep its operations in sequence.
- HarborworksPort berth planningA ship arrives twelve hours late. Move its berth window, then bring its tug and cranes along.
- Casa NomaHotel 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.