# Horas, minutos, días y zoom

> Configura escalas, duraciones de celda y cabeceras de tiempo, muestra horas o celdas de 15 minutos, oculta el tiempo no laborable y haz zoom de meses a minutos.

Source: https://superscheduler.org/es/docs/time-scales-zoom/
Reviewed: 2026-10-07

Elige el tamaño de celda con scale ('Hour', 'Day', 'Week', 'Month', 'Year', o 'CellDuration' con cellDuration en minutos), fija cellWidth en píxeles por celda y days para la longitud, y describe las filas de cabecera con timeHeaders. Oculta noches y fines de semana con businessBeginsHour, businessEndsHour y showNonBusiness={false}. Para el zoom, enumera zoomLevels y cambia entre ellos con control.zoom.setActive, animateTo o step; los gestos de pellizco y Ctrl/Cmd + rueda están activados por defecto.

El eje de tiempo de SuperScheduler Pro se define con unas pocas opciones: qué representa una celda (`scale`), cuánto mide de ancho (`cellWidth`), dónde empieza la línea de tiempo y cuánto dura (`startDate`, `days`), y cómo la etiquetan las filas de cabecera (`timeHeaders`). El zoom es una lista de configuraciones de ese tipo, `zoomLevels`, a la que los usuarios llegan con gestos y tu código mediante `control.zoom`.

Esta guía va de las escalas fijas al zoom continuo. Lite tiene un eje de días fijo; todo lo demás que aparece aquí requiere Pro.

## Escala, duración y ancho de celda
| `scale` | Una celda es | Uso típico |
|---|---|---|
| `'Minute'` | 1 minuto | Escaletas de emisión, ejecuciones de laboratorio |
| `'CellDuration'` | `cellDuration` minutos (60 por defecto) | Franjas de 5, 15 o 30 minutos; turnos de 240 minutos |
| `'Hour'` | 1 hora | Talleres, salas de reuniones, cuadrillas |
| `'Day'` | 1 día natural | Hoteles, alquileres, asignación de personal |
| `'Week'` | 1 semana natural, que empieza en `weekStarts` | Proyectos, campañas |
| `'Month'` | 1 mes natural | Asignaciones largas, planes de capacidad |
| `'Year'` | 1 año natural | Vistas generales de varios años |
| `'Manual'` | Las celdas que enumeras en `timeline` | Periodos irregulares |

`cellWidth` está en píxeles **por celda de la escala actual** (40 por defecto): 44 significa 44 píxeles por día en un eje de días, pero 44 píxeles por hora en un eje de horas. `startDate` (hoy por defecto, truncado a medianoche) y `days` fijan la longitud de la línea de tiempo.

> **Behavior:**
> Los valores por defecto son `scale: 'CellDuration'` con `cellDuration: 60` y `days: 1`: un componente con solo `resources` y `events` muestra un único día en celdas de una hora. Fija siempre `scale` y `days` de forma explícita.

Algunas configuraciones típicas:

```ts
// src/scales.ts
import type { SchedulerProps } from 'super-scheduler'

// A month of day cells: the classic booking chart.
export const monthOfDays = {
  scale: 'Day',
  startDate: '2026-10-01',
  days: 31,
  cellWidth: 44,
  timeHeaders: [{ groupBy: 'Month' }, { groupBy: 'Day', format: 'ddd d' }],
} satisfies SchedulerProps

// One working day in 15-minute cells; nights are removed from the axis.
export const quarterHours = {
  scale: 'CellDuration',
  cellDuration: 15,
  startDate: '2026-10-12',
  days: 1,
  cellWidth: 36,
  businessBeginsHour: 7,
  businessEndsHour: 19,
  showNonBusiness: false,
  timeHeaders: [
    { groupBy: 'Hour', format: 'HH:mm' },
    { groupBy: 'Cell', format: 'mm' },
  ],
} satisfies SchedulerProps

// A work week of hours, Monday to Friday, with 12-hour labels.
export const workWeekOfHours = {
  scale: 'Hour',
  startDate: '2026-10-12',
  days: 5,
  cellWidth: 48,
  timeFormat: 'Clock12Hours',
  businessBeginsHour: 8,
  businessEndsHour: 18,
  showNonBusiness: false,
  timeHeaders: [{ groupBy: 'Day', format: 'dddd d MMMM' }, { groupBy: 'Hour' }],
} satisfies SchedulerProps

// A year in month cells, for long-running assignments.
export const yearOfMonths = {
  scale: 'Month',
  startDate: '2026-01-01',
  days: 365,
  cellWidth: 90,
  timeHeaders: [{ groupBy: 'Year' }, { groupBy: 'Month', format: 'MMM' }],
} satisfies SchedulerProps
```
`cellDuration` también fija el ajuste a la cuadrícula por defecto: con celdas de 15 minutos, los movimientos, redimensionados y selecciones se ajustan a cuartos de hora. La familia de opciones `snapToGrid` desactiva el ajuste gesto a gesto. En un eje de días, los eventos se dibujan por defecto como celdas completas (`useEventBoxes: 'Always'`); pon `useEventBoxes="Never"` para dibujarlos en su hora exacta, de modo que una estancia de 14:00 a 11:00 empiece y termine dentro de sus celdas de día.

## Cabeceras de tiempo
`timeHeaders` enumera las filas de cabecera de arriba abajo. Cada fila agrupa el tiempo por una unidad y puede fijar un `format` de etiqueta y una `height`:

| `groupBy` | Agrupa por |
|---|---|
| `'Year'`, `'Quarter'`, `'Month'`, `'Week'`, `'Day'`, `'Hour'`, `'Minute'` | Esa unidad del calendario |
| `'Cell'` | Una etiqueta por celda |
| `'Default'` | `cellGroupBy` (`'Day'` por defecto) |
| `'None'` | Una etiqueta para toda la fila |

El valor por defecto es `[{ groupBy: 'Default' }, { groupBy: 'Cell' }]`: días encima de las celdas. Cada fila de cabecera mide `headerHeight` píxeles de alto (30 por defecto) salvo que fije su propia `height`.

### Tokens de formato
Los formatos usan estos tokens; cualquier otro carácter se imprime tal cual. Los ejemplos formatean `2026-10-05T14:30:00` con el locale `en-us`.

| Token | Resultado | Token | Resultado |
|---|---|---|---|
| `yyyy` | 2026 | `HH` | 14 |
| `yy` | 26 | `H` | 14 |
| `MMMM` | October | `hh` | 02 |
| `MMM` | Oct | `h` | 2 |
| `MM` | 10 | `mm` | 30 |
| `M` | 10 | `m` | 30 |
| `dddd` | Monday | `ss`, `s` | 00, 0 |
| `ddd` | Mo | `tt` | PM |
| `dd`, `d` | 05, 5 | `%d` | 5 |

Los nombres siguen el `locale` del scheduler (`'en-us'` por defecto): `'dddd d MMMM'` da «lunes 5 octubre» con `locale="es-es"`. En varios locales, `ddd` es una abreviatura de una o dos letras («Mo», «L»); usa `dddd`, o escribe tu propia etiqueta en `onBeforeTimeHeaderRender`, cuando quieras tres letras. En ese hook se pueden personalizar las etiquetas, los estilos, los tooltips y las áreas de la cabecera.

### Etiquetas de 12 o de 24 horas
`timeFormat` controla las etiquetas de hora por defecto: `'Auto'` (por defecto) sigue el locale (12 horas para `en-us`, 24 horas para la mayoría de los locales europeos), y `'Clock12Hours'` y `'Clock24Hours'` fuerzan uno de los dos. Un `format` explícito en una fila de cabecera siempre tiene prioridad: `'h:mm tt'` para etiquetas de 12 horas, `'HH:mm'` para etiquetas de 24 horas. Cambiar el formato de reloj solo cambia las etiquetas; las horas de los eventos nunca se mueven.

## Horario laboral y tiempo oculto
El tiempo laborable se define con `businessBeginsHour` (9 por defecto), `businessEndsHour` (18 por defecto; `0` significa la medianoche al final del día) y `businessWeekends` (`false` por defecto). Con `showNonBusiness` en su valor por defecto, `true`, las celdas de tiempo no laborable se sombrean. Con `showNonBusiness={false}` se eliminan del eje:

- en un eje de días, desaparecen los fines de semana (14 días se convierten en 10 columnas);
- en un eje intradía, desaparecen las horas fuera del horario laboral, así que una semana laboral en horas muestra solo de 08:00 a 18:00 cada día.

Para algo más específico, `onIncludeTimeCell` se llama para cada celda candidata mientras se construye la línea de tiempo: pon `args.cell.visible = false` para quitar una celda, o `args.cell.width` para cambiar su ancho. `scale: 'Manual'` con un array `timeline` de celdas `{ start, end, width }` da control total.

> **Limitation:**
> Ocultar tiempo solo cambia el eje. No mueve, acorta ni valida los eventos que caen en periodos ocultos; si los usuarios no deben planificar ahí, rechaza esas posiciones en tus reglas o [deshabilita las celdas](https://superscheduler.org/es/docs/drag-resize-rules/#disabled-cells) en lugar de ocultarlas.

## Niveles de zoom
Un nivel de zoom es un conjunto con nombre de opciones que se aplican juntas: normalmente `scale`, `cellDuration`, `cellWidth` y `timeHeaders`. Define la escalera una sola vez, a nivel de módulo:

```ts
// src/zoom-levels.ts
import type { SuperScheduler } from 'super-scheduler'

/**
 * From the most detailed view to the widest. Each level is a set of options applied together;
 * cellWidth is in px per cell of that level (per 15 minutes, per hour, per day, per week).
 */
export const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
  {
    id: 'quarter-hours',
    properties: {
      scale: 'CellDuration',
      cellDuration: 15,
      cellWidth: 40,
      timeHeaders: [
        { groupBy: 'Day', format: 'dddd d MMMM' },
        { groupBy: 'Cell', format: 'HH:mm' },
      ],
    },
  },
  {
    id: 'hours',
    properties: {
      scale: 'Hour',
      cellWidth: 56,
      timeHeaders: [
        { groupBy: 'Day', format: 'dddd d MMMM' },
        { groupBy: 'Hour', format: 'HH:mm' },
      ],
    },
  },
  {
    id: 'days',
    properties: {
      scale: 'Day',
      cellWidth: 80,
      timeHeaders: [{ groupBy: 'Month' }, { groupBy: 'Day', format: 'ddd d' }],
    },
  },
  {
    id: 'weeks',
    properties: {
      scale: 'Week',
      cellWidth: 120,
      timeHeaders: [{ groupBy: 'Month' }, { groupBy: 'Week' }],
    },
  },
]

export type ZoomLevelId = 'quarter-hours' | 'hours' | 'days' | 'weeks'
export const ZOOM_LEVEL_IDS: readonly ZoomLevelId[] = ['quarter-hours', 'hours', 'days', 'weeks']
```
Pásala como `zoomLevels` y elige el nivel inicial con `zoom` (un índice o un `id`). `zoomPosition` (`'left'` por defecto, o `'middle'`, `'right'`) decide qué parte del área visible se queda en su sitio cuando cambia el nivel.

Una propiedad también puede ser una función de la fecha de anclaje, `({ date, level }) => value`, por ejemplo para mostrar el año en la cabecera de meses solo en torno al Año Nuevo.

Durante un gesto continuo, el scheduler elige el nivel más cercano al tiempo por píxel actual y aplica su eje y sus cabeceras cuando el usuario entra en él. Las propiedades que reiniciarían la ventana, como `days` y `startDate`, solo se aplican cuando tu código selecciona un nivel de forma explícita. El orden del array no importa a los gestos, que miden todos los niveles.

## Cambiar el zoom desde código
`control.zoom` tiene tres métodos y una propiedad:

| Miembro | Qué hace |
|---|---|
| `setActive(level, position?, anchorDate?)` | Aplica un nivel (índice o id) de inmediato, incluidos `days` y `startDate` |
| `animateTo(target, options?)` | Anima hasta `{ level }` o hasta un `{ cellWidth }` libre; devuelve una promesa que se resuelve cuando termina |
| `step(delta, options?)` | Avanza `delta` posiciones por `zoomLevels`, en el orden del array y sin salirse de los extremos; sin `zoomLevels`, multiplica el ancho de celda por 1,6 en cada paso |
| `active` | Índice del nivel activo; `-1` antes de aplicar ningún nivel |

`animateTo` y `step` aceptan `{ duration, position, anchorDate }`: `duration` en milisegundos (300 por defecto, `0` para no animar) y `anchorDate` como una fecha, `'center'` o `'today'` para mantener ese momento en su sitio. Las animaciones son instantáneas cuando el usuario prefiere movimiento reducido, y no hacen nada mientras hay un gesto de zoom en curso. Un id de nivel desconocido lanza un error.

```tsx
// src/ZoomablePlanning.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { ZOOM_LEVELS, ZOOM_LEVEL_IDS, type ZoomLevelId } from './zoom-levels'

// Stable objects: a new one per render would be re-applied on every render.
const ZOOM_GESTURE: SuperScheduler.ZoomGestureOptions = {
  // The default maximum (400 px per cell) is too narrow to cross from days into hours.
  max: 1024,
}

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly bookings: SuperScheduler.EventData[]
}

export function ZoomablePlanning({ rooms, bookings }: Props) {
  const { controlRef, control } = useSchedulerControl()
  const [level, setLevel] = useState<ZoomLevelId>('days')
  const owned = useMemo(() => bookings.slice(), [bookings])

  // One React update when a gesture or an animation settles, never one per frame.
  const onZoom = useCallback((args: SuperScheduler.ZoomArgs) => {
    if (args.phase !== 'end') return
    const id = ZOOM_LEVEL_IDS[args.level]
    if (id !== undefined) setLevel(id)
  }, [])

  const show = (id: ZoomLevelId) =>
    void control?.zoom.animateTo({ level: id }, { anchorDate: 'center' })
  // step() walks the zoomLevels array in its order: here -1 is more detail, +1 a wider view.
  const zoomIn = () => void control?.zoom.step(-1)
  const zoomOut = () => void control?.zoom.step(1)

  return (
    <>
      <div role="toolbar" aria-label="Zoom">
        {ZOOM_LEVEL_IDS.map((id) => (
          <button key={id} type="button" aria-pressed={level === id} onClick={() => show(id)}>
            {id}
          </button>
        ))}
        <button type="button" aria-label="Zoom in" onClick={zoomIn}>
          +
        </button>
        <button type="button" aria-label="Zoom out" onClick={zoomOut}>
          −
        </button>
      </div>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-12"
        days={14}
        zoomLevels={ZOOM_LEVELS}
        zoom="days"
        zoomPosition="middle"
        zoomGesture={ZOOM_GESTURE}
        onZoom={onZoom}
        resources={rooms}
        events={owned}
      />
    </>
  )
}
```
Deberías ver cuatro botones de nivel y botones de más y menos encima de la planificación. Pulsar «hours» anima el eje de días a horas en torno al centro de la vista, y el estado pulsado también sigue a los gestos de pellizco, porque `onZoom` comunica el nivel al terminar cada zoom.

`onZoom` recibe `phase` (`'start'`, `'change'`, `'end'`), `origin` (`'gesture'` o `'api'`), `level`, `cellWidth`, `scale`, `cellDuration`, la fecha de anclaje, el inicio del área visible y el nivel de detalle. Los gestos y `animateTo` informan en cada fotograma; actúa en `phase === 'end'` salvo que actualices el DOM directamente, y nunca actualices el estado de React en cada fotograma.

## Gestos
Los gestos de zoom están activados por defecto: Ctrl o Cmd con la rueda del ratón (que también cubre el pellizco en el trackpad en Chrome, Edge y Firefox), el pellizco en el trackpad en Safari y el pellizco con dos dedos en pantallas táctiles. El zoom es continuo y se ancla bajo el puntero. Ajústalo con `zoomGesture`:

| Opción | Por defecto | Significado |
|---|---|---|
| `min` | `cellWidthMin` (como mínimo 1) | Ancho de celda mínimo en píxeles |
| `max` | `400` | Ancho de celda máximo en píxeles |
| `wheel` | `'ctrl'` | `'always'` hace zoom con cualquier giro vertical de la rueda (Mayús + rueda hace scroll); `false` nunca hace zoom con la rueda |
| `pinch` | `true` | Pellizco en el trackpad de Safari y en pantallas táctiles |
| `sensitivity` | `1` | Multiplicador de velocidad |
| `scales` | `'zoomLevels'` | Pasa de uno a otro de tus niveles; `'auto'` usa una escalera de hora, día, semana y mes; `false` mantiene la escala actual |
| `link` | ninguno | Los schedulers con el mismo id de enlace hacen zoom juntos |

`zoomGesture={false}` elimina todos los listeners de gestos. Como `cellWidth` es por celda, pasar de un nivel de días a uno de horas necesita margen: un día a 400 píxeles son solo unos 17 píxeles por hora, así que sube `max` (el ejemplo usa 1024) cuando tu escalera va de días a horas.

`keyboardOptions={{ zoomKeys: true }}`, junto con `keyboardEnabled`, añade Ctrl/Cmd con `=` o `+` (`step(1)`), `-` (`step(-1)`) y `0` (vuelta al nivel inicial) mientras el foco está dentro del scheduler.

> **Behavior:**
> `step()` y las teclas de zoom siguen el orden de tu array `zoomLevels`. Con una escalera ordenada de la vista más detallada a la más amplia, como la de arriba, `step(1)` y Ctrl/Cmd `+` pasan a una vista más amplia. Si activas `zoomKeys` con `zoomLevels`, ordena los niveles de la vista más amplia a la más detallada para que `+` acerque.

## Widgets de zoom
`super-scheduler/zoom-ui` ofrece tres widgets DOM opcionales que se actualizan sin renders de React:

- **`createZoomHud(control, options)`**: un indicador dentro de la cuadrícula que se muestra durante los gestos y durante 700 ms después de un zoom por API; `format` fija su texto.
- **`createZoomSlider(control, container, options)`**: un input de rango nativo con soporte de teclado, escala logarítmica o lineal y topes en tus niveles de zoom o en anchos explícitos. Su valor son píxeles por celda, así que encaja mejor con un eje de una sola escala.
- **`createLodBadge(control, container, labels)`**: muestra si la vista está en modo detalle, compacto o vista general.

```tsx
// src/PlanningWithZoomWidgets.tsx
import { useEffect, useRef } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { createLodBadge, createZoomHud, createZoomSlider } from 'super-scheduler/zoom-ui'

export function PlanningWithZoomWidgets({ rooms }: { rooms: SuperScheduler.ResourceData[] }) {
  const { controlRef, control } = useSchedulerControl()
  const toolbar = useRef<HTMLDivElement>(null)

  // Widgets need a mounted control. The cleanup matters: Strict Mode mounts twice in development.
  useEffect(() => {
    const host = toolbar.current
    if (control === null || host === null) return
    // A pill inside the grid while zooming (no container needed).
    const hud = createZoomHud(control, {
      format: ({ cellWidth }) => `${Math.round(cellWidth)} px per day`,
    })
    // A native range input; its value is px per cell, so it suits a single-scale axis like this one.
    const slider = createZoomSlider(control, host, {
      min: 4,
      max: 160,
      scale: 'log',
      label: 'Day width',
    })
    // Detail, Compact or Overview, following the level of detail.
    const badge = createLodBadge(control, host)
    return () => {
      hud.dispose()
      slider.dispose()
      badge.dispose()
    }
  }, [control])

  return (
    <>
      <div ref={toolbar} role="toolbar" aria-label="Zoom" />
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-01"
        days={120}
        scale="Day"
        cellWidth={44}
        resources={rooms}
      />
    </>
  )
}
```
Cada widget tiene `element` y `dispose()`. Créalos en un efecto que dependa del control y libéralos en su función de limpieza; liberar el control también los elimina.

## Nivel de detalle
Cuando los usuarios alejan mucho el zoom, dibujar cada etiqueta a tamaño completo sería ilegible y lento. El nivel de detalle (`lod`, activado por defecto) adapta el render al espacio en pantalla y no cambia nada a partir de 40 píxeles por día:

- **Los eventos**, por debajo de 40 píxeles por día, se adaptan a su propio ancho: contenido completo a partir de 80 píxeles, una línea de texto a partir de 66 píxeles y un bloque simple por debajo. Por debajo de 8 píxeles por día, los bloques pasan a ser rellenos sólidos y los eventos estrechos, barras finas.
- **Las celdas** muestran su contenido (HTML, texto, áreas) a partir de 24 píxeles por celda. Por debajo de 2 píxeles por celda no hay ningún elemento de celda y no se llama a `onBeforeCellRender`.
- **Las líneas de la cuadrícula** mantienen al menos 8 píxeles de separación, el sombreado de fines de semana y de tiempo no laborable necesita 6 píxeles por día, y las etiquetas de cabecera se acortan o pasan a una unidad mayor cuando ya no caben.

Todos los umbrales se pueden cambiar mediante `lod={{ ... }}` (`zoomedOut`, `eventFull`, `eventText`, `eventSolid`, `cellContent`, `cellBackground`, `gridLines`, `shading`, `dayLabel`, `dayNumber`, `weekLabel`, `hysteresis`), y `lod={false}` renderiza literalmente con cualquier zoom. El estado actual está en `control.levelOfDetail` (`level` es `'full'`, `'compact'` u `'overview'`) y se escribe como atributos `data-lod` en la raíz, para tu CSS.

→ https://superscheduler.org/es/examples/clinic-appointments/
→ https://superscheduler.org/es/examples/festival-stages/
→ https://superscheduler.org/es/examples/fleet-rentals/
## Siguientes pasos
- Muestra dónde está el área visible en una línea de tiempo larga: [Minimapa y métricas](https://superscheduler.org/es/docs/minimap-metrics/).
- Guarda el zoom y la posición de scroll por usuario: [Paneles y vistas guardadas](https://superscheduler.org/es/docs/panes-saved-views/).
- Mantén rápidos el scroll y el zoom con muchos datos: [Rendimiento y virtualización](https://superscheduler.org/es/docs/performance-virtualization/).
