# Heures, minutes, jours et zoom

> Configurez échelles, durées de cellule et en-têtes de temps, affichez des cellules de 15 minutes, masquez le temps non ouvré et zoomez des mois aux minutes.

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

Choisissez la taille des cellules avec scale ('Hour', 'Day', 'Week', 'Month', 'Year', ou 'CellDuration' avec cellDuration en minutes), définissez cellWidth en pixels par cellule et days pour la longueur, et décrivez les lignes d’en-tête avec timeHeaders. Masquez les nuits et les week-ends avec businessBeginsHour, businessEndsHour et showNonBusiness={false}. Pour le zoom, listez des zoomLevels et passez de l’un à l’autre avec control.zoom.setActive, animateTo ou step ; les gestes de pincement et Ctrl/Cmd + molette sont activés par défaut.

L’axe du temps de SuperScheduler Pro est défini par une poignée d’options : ce que représente une cellule (`scale`), sa largeur (`cellWidth`), le début et la longueur de la frise (`startDate`, `days`), et la façon dont les lignes d’en-tête l’étiquettent (`timeHeaders`). Le zoom est une liste de telles configurations, `zoomLevels`, que les utilisateurs atteignent par des gestes et votre code via `control.zoom`.

Ce guide va des échelles fixes au zoom continu. Lite a un axe fixe en jours ; tout le reste de cette page nécessite Pro.

## Échelle, durée et largeur de cellule
| `scale` | Une cellule vaut | Usage typique |
|---|---|---|
| `'Minute'` | 1 minute | Conducteurs d’antenne, séries d’analyses en laboratoire |
| `'CellDuration'` | `cellDuration` minutes (60 par défaut) | Créneaux de 5, 15 ou 30 minutes ; postes de 240 minutes |
| `'Hour'` | 1 heure | Ateliers, salles de réunion, équipes |
| `'Day'` | 1 jour calendaire | Hôtels, locations, gestion du personnel |
| `'Week'` | 1 semaine calendaire, commençant le jour `weekStarts` | Projets, campagnes |
| `'Month'` | 1 mois calendaire | Affectations longues, plans de capacité |
| `'Year'` | 1 année calendaire | Vues d’ensemble pluriannuelles |
| `'Manual'` | Les cellules que vous listez dans `timeline` | Périodes irrégulières |

`cellWidth` s’exprime en pixels **par cellule de l’échelle en cours** (40 par défaut) : 44 signifie 44 pixels par jour sur un axe en jours, mais 44 pixels par heure sur un axe en heures. `startDate` (aujourd’hui par défaut, tronqué à minuit) et `days` fixent la longueur de la frise.

> **Behavior:**
> Les valeurs par défaut sont `scale: 'CellDuration'` avec `cellDuration: 60` et `days: 1` : un composant qui ne reçoit que `resources` et `events` affiche une seule journée en cellules d’une heure. Définissez toujours `scale` et `days` explicitement.

Quelques configurations typiques :

```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` fixe aussi le magnétisme par défaut : avec des cellules de 15 minutes, déplacements, redimensionnements et sélections s’alignent sur les quarts d’heure. La famille d’options `snapToGrid` désactive le magnétisme geste par geste. Sur un axe en jours, les événements sont dessinés par défaut en cellules entières (`useEventBoxes: 'Always'`) ; définissez `useEventBoxes="Never"` pour les dessiner à leurs horaires exacts, de sorte qu’un séjour de 14:00 à 11:00 commence et se termine à l’intérieur de ses cellules de jour.

## En-têtes de temps
`timeHeaders` liste les lignes d’en-tête de haut en bas. Chaque ligne groupe le temps selon une unité et peut définir un `format` de libellé et une `height` :

| `groupBy` | Groupe par |
|---|---|
| `'Year'`, `'Quarter'`, `'Month'`, `'Week'`, `'Day'`, `'Hour'`, `'Minute'` | Cette unité calendaire |
| `'Cell'` | Un libellé par cellule |
| `'Default'` | `cellGroupBy` (`'Day'` par défaut) |
| `'None'` | Un seul libellé pour toute la ligne |

La valeur par défaut est `[{ groupBy: 'Default' }, { groupBy: 'Cell' }]` : les jours au-dessus des cellules. Chaque ligne d’en-tête mesure `headerHeight` pixels de haut (30 par défaut), sauf si elle définit sa propre `height`.

### Jetons de format
Les formats utilisent ces jetons ; tout autre caractère est affiché tel quel. Les exemples formatent `2026-10-05T14:30:00` avec la locale `en-us`.

| Jeton | Résultat | Jeton | Résultat |
|---|---|---|---|
| `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 |

Les noms suivent la `locale` du planificateur (`'en-us'` par défaut) : `'dddd d MMMM'` donne « lunes 5 octubre » avec `locale="es-es"`. Dans plusieurs locales, `ddd` est une abréviation d’une ou deux lettres (« Mo », « L ») ; utilisez `dddd`, ou écrivez votre propre libellé dans `onBeforeTimeHeaderRender`, si vous voulez trois lettres. Libellés, styles, infobulles et zones des en-têtes peuvent tous être personnalisés dans ce hook.

### Libellés sur 12 ou 24 heures
`timeFormat` contrôle les libellés d’heure par défaut : `'Auto'` (par défaut) suit la locale (12 heures pour `en-us`, 24 heures pour la plupart des locales européennes), `'Clock12Hours'` et `'Clock24Hours'` imposent l’un ou l’autre. Un `format` explicite sur une ligne d’en-tête l’emporte toujours : `'h:mm tt'` pour des libellés sur 12 heures, `'HH:mm'` pour des libellés sur 24 heures. Changer le format d’horloge ne change que les libellés ; les horaires des événements ne bougent jamais.

## Heures ouvrées et temps masqué
Le temps ouvré est défini par `businessBeginsHour` (9 par défaut), `businessEndsHour` (18 par défaut ; `0` signifie minuit à la fin de la journée) et `businessWeekends` (`false` par défaut). Avec `showNonBusiness` à sa valeur par défaut `true`, les cellules de temps non ouvré sont grisées. Avec `showNonBusiness={false}`, elles sont retirées de l’axe :

- sur un axe en jours, les jours de week-end disparaissent (14 jours deviennent 10 colonnes) ;
- sur un axe intrajournalier, les heures hors de la plage ouvrée disparaissent : une semaine de travail en heures n’affiche alors que 08:00 à 18:00 chaque jour.

Pour des besoins plus spécifiques, `onIncludeTimeCell` est appelé pour chaque cellule candidate pendant la construction de la frise : définissez `args.cell.visible = false` pour supprimer une cellule, ou `args.cell.width` pour la redimensionner. `scale: 'Manual'` avec un tableau `timeline` de cellules `{ start, end, width }` donne un contrôle total.

> **Limitation:**
> Masquer du temps ne modifie que l’axe. Cela ne déplace, ne raccourcit ni ne valide les événements qui tombent dans les périodes masquées ; si les utilisateurs ne doivent pas planifier à ces moments-là, refusez ces positions dans vos règles ou [désactivez les cellules](https://superscheduler.org/fr/docs/drag-resize-rules/#disabled-cells) au lieu de les masquer.

## Niveaux de zoom
Un niveau de zoom est un ensemble nommé d’options appliquées en bloc : en général `scale`, `cellDuration`, `cellWidth` et `timeHeaders`. Définissez l’échelle des niveaux une seule fois, au niveau du module :

```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']
```
Passez-la dans `zoomLevels` et choisissez le niveau initial avec `zoom` (un index ou un `id`). `zoomPosition` (`'left'` par défaut, ou `'middle'`, `'right'`) détermine quelle partie de la zone visible reste en place quand le niveau change.

Une propriété peut aussi être une fonction de la date d’ancrage, `({ date, level }) => value`, par exemple pour n’afficher l’année dans l’en-tête des mois qu’autour du Nouvel An.

Pendant un geste continu, le planificateur choisit le niveau le plus proche du temps par pixel en cours et applique son axe et ses en-têtes dès que l’utilisateur entre dans ce niveau. Les propriétés qui réinitialiseraient la fenêtre, comme `days` et `startDate`, ne s’appliquent que lorsque votre code sélectionne explicitement un niveau. L’ordre du tableau n’a pas d’importance pour les gestes, qui mesurent tous les niveaux.

## Changer le zoom depuis le code
`control.zoom` comporte trois méthodes et une propriété :

| Membre | Rôle |
|---|---|
| `setActive(level, position?, anchorDate?)` | Applique un niveau (index ou id) immédiatement, `days` et `startDate` compris |
| `animateTo(target, options?)` | Anime vers `{ level }` ou vers une largeur libre `{ cellWidth }` ; renvoie une promesse résolue une fois l’animation stabilisée |
| `step(delta, options?)` | Avance de `delta` positions dans `zoomLevels`, dans l’ordre du tableau et avec butée ; sans `zoomLevels`, multiplie la largeur de cellule par 1,6 à chaque pas |
| `active` | Index du niveau actif, `-1` tant qu’aucun niveau n’a été appliqué |

`animateTo` et `step` acceptent `{ duration, position, anchorDate }` : `duration` en millisecondes (300 par défaut, `0` pour aucune animation), et `anchorDate` sous forme de date, de `'center'` ou de `'today'` pour garder ce moment en place. Les animations sont instantanées quand l’utilisateur préfère réduire les animations, et ne font rien pendant qu’un geste de zoom est en cours. Un id de niveau inconnu lève une erreur.

```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}
      />
    </>
  )
}
```
Vous devriez voir quatre boutons de niveau ainsi que des boutons plus et moins au-dessus du planning. Appuyer sur « hours » anime l’axe des jours vers les heures autour du centre de la vue, et l’état enfoncé suit aussi les gestes de pincement, car `onZoom` signale le niveau à la fin de chaque zoom.

`onZoom` reçoit `phase` (`'start'`, `'change'`, `'end'`), `origin` (`'gesture'` ou `'api'`), `level`, `cellWidth`, `scale`, `cellDuration`, la date d’ancrage, le début de la zone visible et le niveau de détail. Les gestes et `animateTo` signalent chaque frame ; agissez sur `phase === 'end'`, sauf si vous mettez à jour le DOM directement, et ne mettez jamais à jour le state React à chaque frame.

## Gestes
Les gestes de zoom sont activés par défaut : Ctrl ou Cmd avec la molette de la souris (ce qui couvre aussi le pincement au trackpad dans Chrome, Edge et Firefox), le pincement au trackpad dans Safari et le pincement à deux doigts sur écran tactile. Le zoom est continu et ancré sous le pointeur. Réglez-le avec `zoomGesture` :

| Option | Défaut | Signification |
|---|---|---|
| `min` | `cellWidthMin` (au moins 1) | Plus petite largeur de cellule en pixels |
| `max` | `400` | Plus grande largeur de cellule en pixels |
| `wheel` | `'ctrl'` | `'always'` zoome à chaque mouvement vertical de la molette (Maj + molette fait défiler) ; `false` ne zoome jamais avec la molette |
| `pinch` | `true` | Pincement au trackpad dans Safari et sur écran tactile |
| `sensitivity` | `1` | Multiplicateur de vitesse |
| `scales` | `'zoomLevels'` | Passe d’un de vos niveaux à l’autre ; `'auto'` utilise une échelle heure, jour, semaine, mois ; `false` conserve l’échelle en cours |
| `link` | aucun | Les planificateurs partageant le même identifiant de lien zooment ensemble |

`zoomGesture={false}` supprime tous les écouteurs de gestes. Comme `cellWidth` s’entend par cellule, passer d’un niveau en jours à un niveau en heures demande de la place : un jour à 400 pixels ne fait qu’environ 17 pixels par heure. Augmentez donc `max` (l’exemple utilise 1024) quand votre échelle de niveaux va des jours aux heures.

`keyboardOptions={{ zoomKeys: true }}`, avec `keyboardEnabled`, ajoute Ctrl/Cmd avec `=` ou `+` (`step(1)`), `-` (`step(-1)`) et `0` (retour au niveau initial) lorsque le focus est dans le planificateur.

> **Behavior:**
> `step()` et les touches de zoom suivent l’ordre de votre tableau `zoomLevels`. Avec une échelle de niveaux ordonnée du plus détaillé au plus large, comme ci-dessus, `step(1)` et Ctrl/Cmd `+` passent à une vue plus large. Si vous activez `zoomKeys` avec `zoomLevels`, ordonnez les niveaux de la vue la plus large à la plus détaillée pour que `+` zoome vers l’avant.

## Widgets de zoom
`super-scheduler/zoom-ui` fournit trois widgets DOM facultatifs qui se mettent à jour sans rendu React :

- **`createZoomHud(control, options)`** : un indicateur dans la grille, affiché pendant les gestes et 700 ms après un zoom par l’API ; `format` définit son texte.
- **`createZoomSlider(control, container, options)`** : un input range natif avec prise en charge du clavier, une échelle logarithmique ou linéaire, et des crans à vos niveaux de zoom ou à des largeurs explicites. Sa valeur s’exprime en pixels par cellule : il convient donc surtout à un axe à échelle unique.
- **`createLodBadge(control, container, labels)`** : indique si la vue est en mode détail, compact ou vue d’ensemble.

```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}
      />
    </>
  )
}
```
Chaque widget possède `element` et `dispose()`. Créez-les dans un effet qui dépend du contrôle et libérez-les dans sa fonction de nettoyage ; libérer le contrôle les supprime aussi.

## Niveau de détail
Quand les utilisateurs dézooment fortement, dessiner chaque libellé en taille réelle serait illisible et lent. Le niveau de détail (`lod`, activé par défaut) adapte le rendu à l’espace disponible à l’écran et ne change rien à partir de 40 pixels par jour :

- **Les événements**, en dessous de 40 pixels par jour, s’adaptent à leur propre largeur : contenu complet à partir de 80 pixels, une ligne de texte à partir de 66 pixels, un simple bloc en dessous. Sous 8 pixels par jour, les blocs deviennent des aplats et les événements étroits de fines barres.
- **Les cellules** affichent leur contenu (HTML, texte, zones) à partir de 24 pixels par cellule. Sous 2 pixels par cellule, il n’y a plus du tout d’éléments de cellule, et `onBeforeCellRender` n’est pas appelé.
- **Les lignes de grille** restent espacées d’au moins 8 pixels, le grisé des week-ends et du temps non ouvré nécessite 6 pixels par jour, et les libellés d’en-tête se raccourcissent ou passent à une unité plus grossière quand ils ne tiennent plus.

Chaque seuil peut être modifié via `lod={{ ... }}` (`zoomedOut`, `eventFull`, `eventText`, `eventSolid`, `cellContent`, `cellBackground`, `gridLines`, `shading`, `dayLabel`, `dayNumber`, `weekLabel`, `hysteresis`), et `lod={false}` affiche tout littéralement à tous les niveaux de zoom. L’état courant est `control.levelOfDetail` (`level` vaut `'full'`, `'compact'` ou `'overview'`) et il est écrit sous forme d’attributs `data-lod` sur la racine, pour votre CSS.

→ https://superscheduler.org/fr/examples/clinic-appointments/
→ https://superscheduler.org/fr/examples/festival-stages/
→ https://superscheduler.org/fr/examples/fleet-rentals/
## Étapes suivantes
- Montrez où se trouve la zone visible sur une longue frise : [Minimap et métriques](https://superscheduler.org/fr/docs/minimap-metrics/).
- Enregistrez le zoom et la position de défilement par utilisateur : [Volets et vues enregistrées](https://superscheduler.org/fr/docs/panes-saved-views/).
- Gardez le défilement et le zoom rapides avec de gros volumes de données : [Performances et virtualisation](https://superscheduler.org/fr/docs/performance-virtualization/).
