# Minimapa y métricas derivadas

> Añade una vista general de la línea de tiempo con super-scheduler/minimap, aliméntala con la ocupación o tu propia métrica y dale estilo, etiquetas y limpieza.

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

Renderiza SchedulerMinimap de super-scheduler/minimap con el control de useSchedulerControl(); sin opciones, muestra cuántos eventos se solapan cada día. Para una métrica de negocio como la utilización o la ocupación, pasa una función series que tu aplicación calcula por tramo, con peak: 'absolute', max: 1 y una función tone para los colores de aviso y de peligro. Arrastrar la ventana desplaza la línea de tiempo, arrastrar sus bordes hace zoom, y la ventana funciona con el teclado como un control deslizante.

Un año de reservas no cabe en pantalla. El minimapa es una franja estrecha debajo (o encima) del scheduler que muestra toda la línea de tiempo de una vez: una barra por tramo de tiempo y una ventana que marca el periodo visible. Los usuarios ven dónde están las semanas con más carga y saltan allí. Las barras muestran el número que decida tu aplicación, así que la franja se convierte en un gráfico compacto de utilización, ocupación, carga o ingresos.

El minimapa requiere SuperScheduler Pro.

## Añadir un minimapa
`SchedulerMinimap` es el componente React. Necesita el control del scheduler, que solo existe después de que el scheduler se monte; `useSchedulerControl()` te da `control` como estado (`null` al principio), y el minimapa acepta `null` y espera.

```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" />
    </>
  )
}
```
Deberías ver una franja de 32 px con las iniciales de los meses, una marca en el día de hoy, los días anteriores atenuados y una ventana que cubre las semanas visibles. Arrastra la ventana y el scheduler se desplaza con ella.

Sin `series`, la franja usa `eventDensity(control)`: el número de eventos que se solapan con cada tramo. Cuenta en tareas cortas en segundo plano (hasta 8 ms o 15.000 eventos por tarea), guarda el resultado en caché y vuelve a pintar al terminar, de modo que un almacén grande nunca bloquea la página.

## Cómo se construye la franja
El minimapa divide un rango de tiempo en tramos y dibuja un valor por tramo:

- **Rango.** Por defecto, la línea de tiempo del control (desde `startDate` durante `days`); con scroll infinito, el periodo generado en ese momento. `range: { start, end }` fija otro periodo, por ejemplo un año entero mientras el scheduler muestra un mes.
- **Tramos.** Un día cada uno; una hora con `scale: 'Hour'` o `'Minute'`; una semana cuando el rango supera los 730 días.
- **Valores.** Tu serie devuelve un número por tramo. Cuando hay más tramos que píxeles, los valores contiguos se promedian en una barra por columna de píxeles, alineada con los píxeles del dispositivo.
- **Altura.** Con `peak: 'relative'` (por defecto), la barra más alta corresponde al valor más grande. Con `peak: 'absolute'`, las barras se miden respecto a `max` (por defecto 1), así que un día completo siempre se ve lleno.

## Alimentarlo con tu propia métrica
`series` es un `Float32Array` que cubre todo el rango, o una función que recibe el rango (`start`, `end`, `buckets`, `bucketMs`) y devuelve un valor por tramo. La forma de función se adapta cuando el usuario hace zoom y cambia el tamaño del tramo.

La librería no sabe qué significa «ocupado» en tu negocio, así que la métrica es código tuyo. Esta calcula la utilización: la parte del tiempo disponible que está reservada, para cualquier número de recursos.

```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
  }
}
```
Con dos furgonetas, una reservada todo el día y la otra desde el mediodía, el día marca 0,75. Los intervalos son semiabiertos, como en el scheduler: una reserva que termina a medianoche no toca el día siguiente.

```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"
      />
    </>
  )
}
```
Ahora la franja se lee como un gráfico de utilización: barras suaves que se oscurecen con el valor, ámbar a partir del 80 %, rojo a partir del 95 %, y un lector de pantalla anuncia «Fleet utilization overview, Showing 1 Jan 2026 to 26 Jan 2026».

> **Tip:**
> En estancias de hotel (entrada a las 14:00, salida a las 11:00), la utilización ponderada por tiempo nunca llega a 1 en una noche completa. Cuenta noches en su lugar: una habitación está ocupada un día cuando una estancia cubre esa noche, y el valor es el número de habitaciones ocupadas dividido entre el de habitaciones.

Para el caso habitual de ponderar los eventos en lugar de contarlos, `eventDensity(control, { weight })` acepta una función de los datos del evento (horas, unidades, huéspedes). El fragmento imperativo de más abajo la usa.

## Tonos, pico, marcas y etiquetas
| Opción | Por defecto | Efecto |
|---|---|---|
| `height` | `28` | Altura de la franja en píxeles; las iniciales de los meses necesitan 24 o más |
| `peak` | `'relative'` | `'absolute'` mide las barras respecto a `max` |
| `max` | `1` | El valor que llena una barra, con `peak: 'absolute'` |
| `tone(value, index)` | todo `'base'` | `'base'`, `'warn'` o `'danger'` por tramo |
| `marks` | todo `true` | `today` (una marca en la fecha actual del navegador), `months` (separadores e iniciales, con el año en enero), `past` (tramos anteriores atenuados) |
| `range` | la línea de tiempo | El periodo que cubre la franja |
| `labels` | inglés o español | `label` (el nombre accesible de la ventana), `zoom` (instrucciones para redimensionar) y `valueText(start, end)` |

Las etiquetas por defecto están en inglés, o en español cuando el `locale` del scheduler empieza por `es`: «Visible period», «Drag either edge to zoom, or use + and −» y las fechas visibles como `yyyy-MM-dd – yyyy-MM-dd`. Para cualquier otro idioma, proporciona `labels`.

Los colores salen de tokens que, si no los defines, toman los del tema del scheduler. Defínelos en el contenedor del minimapa o en cualquier ancestro:

| Token | Valor de respaldo |
|---|---|
| `--super-scheduler-minimap-base` | el acento |
| `--super-scheduler-minimap-warn` | `#f59e0b` |
| `--super-scheduler-minimap-danger` | `#ef4444` |
| `--super-scheduler-minimap-past` | el texto atenuado |
| `--super-scheduler-minimap-today` | el acento |
| `--super-scheduler-minimap-months` | el color de borde |
| `--super-scheduler-minimap-label` | el texto atenuado |
| `--super-scheduler-minimap-brush` | el acento |

La franja se vuelve a pintar cuando cambia el tema: un cambio de `class`, `data-theme` o `data-color-scheme` en `<html>`, en la raíz del scheduler o en el contenedor, o un cambio del esquema de color del sistema.

## Interacción con la ventana
| Entrada | Efecto |
|---|---|
| Arrastrar la ventana | Desplaza la línea de tiempo |
| Arrastrar cualquiera de los bordes de la ventana | Hace zoom: hacia fuera muestra más tiempo, hacia dentro menos; el borde opuesto queda fijo, dentro del mínimo y el máximo de `zoomGesture` |
| Hacer clic en la franja fuera de la ventana | Desplaza la vista para que esa fecha quede en el centro (con animación, salvo que esté activado el movimiento reducido) |
| Flecha izquierda / derecha, abajo / arriba | Un día antes o después; con Mayús, siete días |
| RePág / AvPág | Un mes antes o después |
| Inicio / Fin | Principio o final del rango |
| `+` o `=`, `-` o `−` | Acerca o aleja el zoom alrededor del centro |

La ventana es un `role="slider"` enfocable con `aria-valuetext`; el canvas queda oculto para las tecnologías de apoyo. Con `cellWidthSpec: 'Auto'`, los tiradores de los bordes y las teclas de zoom se desactivan, porque el scheduler ya ajusta toda la línea de tiempo a la vista. Los movimientos del puntero se aplican una vez por fotograma de animación.

## API imperativa y liberación
`createMinimap(control, container, options)` crea la franja dentro de cualquier elemento y devuelve `{ element, update, refresh, dispose }`. Lanza una excepción si el control no se ha inicializado, así que en React créala en un efecto que dependa de `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)` cambia opciones y vuelve a pintar;
- `refresh()` vuelve a pedir la serie;
- `dispose()` elimina la franja y libera sus observers, listeners y trabajo pendiente. Liberar el control también lo hace.

`SchedulerMinimap` hace todo esto por ti: crea la franja cuando `control` está disponible, la vuelve a crear si el control cambia y la libera al desmontarse.

## Mantener la franja al día
El minimapa se vuelve a pintar, y vuelve a llamar a la función de serie, cuando:

- cambian los eventos del control (un arrastre, una llamada a la API, una carga);
- termina un zoom, se redimensiona el contenedor o cambia el tema;
- llamas a `refresh()` o a `update()`.

Mientras dura un gesto, los repintados esperan y se hacen cuando termina. Por eso, una función de serie que lee los eventos del propio control siempre está al día. Una serie calculada a partir de los datos de tu aplicación está al día cuando pasas una función nueva después de que cambien esos datos, como hace `useMemo` en el fragmento de utilización.

`SchedulerMinimap` llama a `update` con sus props en cada render de su componente padre. Memoiza `series`, `tone` y `labels` (o defínelos a nivel de módulo) para que un nuevo render no vuelva a calcular la serie sin motivo.

## Qué le corresponde a tu aplicación
- **La métrica.** Qué cuenta como capacidad, qué eventos se tienen en cuenta (provisionales, cancelados, bloqueos) y cómo ponderarlos.
- **Los datos que no has cargado.** La serie solo ve lo que tu código le da. Con la [carga por rangos](https://superscheduler.org/es/docs/range-loading/), puede que solo una parte del año esté en memoria: para una vista del año completo, pide a tu backend agregados por día y pásalos como serie junto con un `range` fijo.
- **Umbrales y textos.** Los límites de los tonos, las etiquetas y sus traducciones.

## Relacionado
→ https://superscheduler.org/es/examples/fleet-rentals/
→ https://superscheduler.org/es/examples/manufacturing-orders/
→ https://superscheduler.org/es/examples/port-berths/
→ https://superscheduler.org/es/examples/hotel-rooms/
- [Escalas de tiempo y zoom](https://superscheduler.org/es/docs/time-scales-zoom/) para los límites de zoom que respetan los bordes de la ventana.
- [Temas, tokens, Tailwind y modo oscuro](https://superscheduler.org/es/docs/theming/) para los tokens de los que el minimapa toma sus valores de respaldo.
