Librerías / Timeline

Timeline

Una biblioteca basada en React para crear visualizaciones de línea de tiempo interactivas con renderizado en lienzo.

@gravity-ui/timeline npm package Release storybook

English version

Una biblioteca basada en React para crear visualizaciones interactivas de líneas de tiempo con renderizado en canvas.

Documentación

Para más detalles, consulta Documentación.

Vista previa

Línea de tiempo básica con eventos y ejes:

Línea de tiempo básica con eventos

Renderizado personalizado con eventos anidados expandibles (ejemplo de NestedEvents):

Línea de tiempo con eventos anidados

Características

  • Renderizado basado en Canvas para alto rendimiento
  • Línea de tiempo interactiva con capacidades de zoom y desplazamiento (pan)
  • Interacciones flexibles con rueda y trackpad, incluyendo el paso del scroll vertical
  • Soporte para eventos, marcadores, secciones, ejes y cuadrícula
  • Secciones de fondo para organización visual y resaltado de períodos de tiempo
  • Agrupación inteligente de marcadores con zoom automático al grupo - Haz clic en marcadores agrupados para hacer zoom en sus componentes individuales
  • Renderizado virtualizado para mejorar el rendimiento con grandes conjuntos de datos (solo activo cuando el contenido de la línea de tiempo excede la ventana gráfica)
  • Apariencia y comportamiento personalizables
  • Soporte de TypeScript con definiciones de tipos completas
  • Integración con React mediante hooks personalizados

Instalación

npm install @gravity-ui/timeline

Uso

El componente de línea de tiempo se puede utilizar en aplicaciones React con la siguiente configuración básica:

import { TimelineCanvas, useTimeline } from '@gravity-ui/timeline/react';

const MyTimelineComponent = () => {
  const { timeline, api, start, stop } = useTimeline({
    settings: {
      start: Date.now(),
      end: Date.now() + 3600000, // 1 hora a partir de ahora
      axes: [],
      events: [],
      markers: [],
      sections: []
    },
    viewConfiguration: {
      // Configuración de vista opcional
    }
  });

  // timeline - Instancia de Timeline
  // api - Instancia de CanvasApi (igual que timeline.api)
  // start - función para inicializar la línea de tiempo con el canvas
  // stop - función para destruir la línea de tiempo

  return (
    <div style={{ width: '100%', height: '100%' }}>
      <TimelineCanvas timeline={timeline} />
    </div>
  );
};

Estructura de Ejes

Cada eje tiene la siguiente estructura:

type TimelineAxis = {
  id: string;          // Identificador único del eje
  tracksCount: number; // Número de pistas en el eje
  top: number;         // Posición vertical (px)
  height: number;      // Altura por pista (px)
};

Líneas Horizontales de Ejes

Configura la colocación de las líneas horizontales a través de viewConfiguration.axes.linePosition:

  • "center" (predeterminado) dibuja una línea a través del centro de cada pista.
  • "between" dibuja una línea después de cada pista, en su límite inferior. Esto es útil para filas de estilo tabla con barras de eventos centradas.
viewConfiguration: {
  axes: {
    linePosition: 'between'
  }
}

Interacciones Flexibles de Cámara

ZoomMode proporciona preajustes de interacción familiares, mientras que camera.interactions te permite anular un gesto individual. Esto es útil cuando una línea de tiempo se encuentra dentro de una página con desplazamiento vertical: mantén el desplazamiento horizontal y el zoom del trackpad, pero deja que el scroll normal de la rueda llegue al contenedor padre.

import {ZoomMode} from '@gravity-ui/timeline';

const {timeline} = useTimeline({
  settings: { /* ... */ },
  viewConfiguration: {
    camera: {
      zoom: ZoomMode.DEFAULT,
      interactions: {
        verticalWheel: 'pass-through',
        horizontalWheel: 'pan',
        pinch: 'zoom',
      },
      zoomSensitivity: {
        in: 0.5,
        out: 0.5,
      },
      minRange: 5_000,
      maxRange: 1000 * 60 * 60 * 24 * 365,
    },
  },
});

Cada interacción acepta 'zoom', 'pan' o 'pass-through'. pinch representa el gesto de Ctrl+rueda del trackpad del navegador. zoomSensitivity.in y zoomSensitivity.out multiplican independientemente la velocidad de zoom-in y zoom-out: 1 es el valor predeterminado, valores más bajos son más suaves y 0 deshabilita el zoom en esa dirección. Los pequeños deltas del trackpad se suavizan automáticamente. minRange y maxRange son duraciones en milisegundos; el mínimo por defecto es 5 segundos y el máximo no está restringido a menos que se configure, así que establece maxRange para limitar hasta dónde pueden hacer zoom los usuarios. Consulta el ejemplo interactivo de Interacciones de Cámara en Storybook.

Estructura de Secciones

Cada sección requiere la siguiente estructura:

type TimelineSection = {
  id: string;               // Identificador único de la sección
  from: number;             // Marca de tiempo de inicio
  to?: number;              // Marca de tiempo de fin opcional (por defecto, el final de la línea de tiempo)
  color: string;            // Color de fondo de la sección
  hoverColor?: string;      // Color opcional cuando la sección está en modo hover
  renderer?: AbstractSectionRenderer; // Renderizador personalizado opcional (exportado del paquete)
};

Las secciones proporcionan color de fondo para períodos de tiempo y ayudan a organizar visualmente el contenido de la línea de tiempo:

const MyTimelineComponent = () => {
  const { timeline } = useTimeline({
    settings: {
      start: Date.now(),
      end: Date.now() + 3600000,
      axes: [],
      events: [],
      markers: [],
      sections: [
        {
          id: 'morning',
          from: Date.now(),
          to: Date.now() + 1800000, // 30 minutos
          color: 'rgba(255, 235, 59, 0.3)', // Amarillo semitransparente
          hoverColor: 'rgba(255, 235, 59, 0.4)'
        },
        {
          id: 'afternoon',
          from: Date.now() + 1800000,
          // 'to' no especificado - se extiende hasta el final de la línea de tiempo
          color: 'rgba(76, 175, 80, 0.2)', // Verde semitransparente
          hoverColor: 'rgba(76, 175, 80, 0.3)'
        }
      ]
    },
    viewConfiguration: {
      sections: {
        hitboxPadding: 2 // Relleno para la detección de hover
      }
    }
  });

  return <TimelineCanvas timeline={timeline} />;
};

Estructura de Marcadores

Cada marcador requiere la siguiente estructura:

type TimelineMarker = {
  time: number;           // Marca de tiempo para la posición del marcador
  color: string;          // Color de la línea del marcador
  activeColor: string;    // Color cuando el marcador está seleccionado (requerido)
  hoverColor: string;     // Color cuando el marcador está en hover (requerido)
  lineWidth?: number;     // Ancho opcional de la línea del marcador
  label?: string;         // Texto de etiqueta opcional
  labelColor?: string;    // Color de etiqueta opcional
  renderer?: AbstractMarkerRenderer; // Renderizador personalizado opcional
  nonSelectable?: boolean;// Si el marcador se puede seleccionar
  group?: boolean;        // Si el marcador representa un grupo
};

Agrupación y Zoom de Marcadores

La línea de tiempo agrupa automáticamente los marcadores que están cerca y proporciona funcionalidad de zoom:

const MyTimelineComponent = () => {
  const { timeline } = useTimeline({
    settings: {
      start: Date.now(),
      end: Date.now() + 3600000,
      axes: [],
      events: [],
      markers: [
        // Estos marcadores se agruparán
        { time: Date.now(), color: '#ff0000', activeColor: '#ff5252', hoverColor: '#ff1744', label: 'Evento 1' },
        { time: Date.now() + 1000, color: '#ff0000', activeColor: '#ff5252', hoverColor: '#ff1744', label: 'Evento 2' },
        { time: Date.now() + 2000, color: '#ff0000', activeColor: '#ff5252', hoverColor: '#ff1744', label: 'Evento 3' },
      ]
    },
    viewConfiguration: {
      markers: {
        collapseMinDistance: 8,        // Agrupar marcadores a menos de 8 píxeles
        groupZoomEnabled: true,        // Habilitar zoom al hacer clic en un grupo
        groupZoomPadding: 0.3,        // 30% de relleno alrededor del grupo
        groupZoomMaxFactor: 0.3,      // Factor de zoom máximo
      }
    }
  });

  // Escuchar eventos de zoom de grupo
  useTimelineEvent(timeline, 'on-group-marker-click', (data) => {
    console.log('Grupo ampliado:', data);
  });

  return <TimelineCanvas timeline={timeline} />;
};

Cómo Funciona

El componente de línea de tiempo está construido con React y proporciona una forma flexible de crear visualizaciones interactivas de líneas de tiempo. Así es como funciona:

Arquitectura del Componente

La línea de tiempo se implementa como un componente de React que se puede configurar a través de dos objetos principales:

  1. TimelineSettings: Controla el comportamiento y la apariencia principal de la línea de tiempo

    • start: Hora de inicio de la línea de tiempo
    • end: Hora de finalización de la línea de tiempo
    • axes: Matriz de configuraciones de ejes (ver estructura a continuación)
    • events: Matriz de configuraciones de eventos
    • markers: Matriz de configuraciones de marcadores
    • sections: Matriz de configuraciones de secciones
  2. ViewConfiguration: Gestiona la representación visual y la configuración de interacción

    • Controla la apariencia, los niveles de zoom y el comportamiento de interacción
    • Se puede personalizar o usar valores predeterminados

Manejo de Eventos

El componente de línea de tiempo admite varios eventos interactivos:

  • on-click: Se activa al hacer clic en la línea de tiempo; incluye los elementos detectados, la marca de tiempo, las coordenadas de la vista y las coordenadas del lienzo
  • on-context-click: Se activa al hacer clic derecho/menú contextual
  • on-select-change: Se dispara cuando cambia la selección
  • on-hover: Se activa al pasar el cursor sobre elementos de la línea de tiempo
  • on-leave: Se dispara cuando el ratón sale de los elementos de la línea de tiempo

Ejemplo de manejo de eventos:

import { useTimelineEvent } from '@gravity-ui/timeline/react';

const MyTimelineComponent = () => {
  const { timeline } = useTimeline({ /* ... */ });

  useTimelineEvent(timeline, 'on-click', (data) => {
    console.log('Línea de tiempo clickeada:', data);
  });

  useTimelineEvent(timeline, 'on-select-change', (data) => {
    console.log('Selección cambiada:', data);
  });

  return <TimelineCanvas timeline={timeline} />;
};

Integración con React

El componente utiliza hooks personalizados para la gestión de la línea de tiempo:

  • useTimeline: Gestiona la instancia de la línea de tiempo y su ciclo de vida

    • Crea e inicializa la línea de tiempo
    • Maneja la limpieza al desmontar el componente
    • Proporciona acceso a la instancia de la línea de tiempo
  • useTimelineEvent: Maneja la suscripción a eventos y la limpieza

    • Gestiona el ciclo de vida del oyente de eventos
    • Limpia automáticamente los oyentes al desmontar

El componente maneja automáticamente la limpieza y destrucción de la instancia de la línea de tiempo cuando se desmonta.

Instala @gravity-ui/uikit y sus estilos para mostrar detalles de eventos sin suscribirte a eventos de hover o calcular coordenadas tú mismo:

import '@gravity-ui/uikit/styles/fonts.css';
import '@gravity-ui/uikit/styles/styles.css';
import {EventPopup} from '@gravity-ui/timeline/react/uikit';

<>
  <TimelineCanvas timeline={timeline} />
  <EventPopup
    timeline={timeline}
    content={(event) => <EventDetails event={event} />}
  />
</>

El popup, el resaltado de hover y el cursor utilizan el mismo evento: un acierto exacto tiene prioridad sobre los eventos cercanos. Los aciertos exactos superpuestos se resuelven en el último evento en orden de dibujo. Solo cuando no hay un acierto exacto, se utiliza una tolerancia de 3 px + events.hitboxPadding. Las consultas de grupo y on-hover aún incluyen todos los candidatos.

EventPopup se abre después de 150 ms y se cierra 200 ms después de que el puntero abandona el evento. Establezca openDelay, closeDelay, placement, offset, className o aria-label cuando sea necesario. El popup permanece abierto mientras su contenido tenga el puntero o el foco, se cierra al presionar Escape o al hacer clic fuera, y utiliza el último evento en el orden de los datos cuando los eventos se superponen. hoverColor e isHovered controlan el dibujo del evento; EventPopup controla su UI de detalles.

Estructura de Eventos

Los eventos en la línea de tiempo siguen esta estructura:

type TimelineEvent = {
  id: string;             // Identificador único
  from: number;           // Marca de tiempo de inicio
  to?: number;            // Marca de tiempo de fin (opcional para eventos puntuales)
  axisId: string;         // ID del eje al que pertenece este evento
  trackIndex: number;     // Índice en la pista del eje
  renderer?: AbstractEventRenderer; // Renderizador personalizado opcional
  color?: string;         // Color de evento opcional
  hoverColor?: string;    // Color opcional para el estado resaltado
  selectedColor?: string; // Color opcional para el estado seleccionado
  cursor?: string;        // Cursor CSS opcional al pasar el puntero sobre el evento
};

Establezca cursor: 'pointer' en los eventos que realizan una acción al hacer clic. El cursor se aplica solo mientras el puntero está sobre ese evento; cuando los eventos se superponen, el último evento en el orden de los datos determina el cursor.

Colores de Gravity UI

El canvas no puede resolver las propiedades personalizadas de CSS por sí solo. Timeline resuelve un valor completo var(--token) contra su elemento canvas, por lo que los tokens semánticos de Gravity UI funcionan para eventos, marcadores, secciones, ejes, cuadrícula y regla integrados.

import '@gravity-ui/uikit/styles/fonts.css';
import '@gravity-ui/uikit/styles/styles.css';
import {ThemeProvider} from '@gravity-ui/uikit';
import {useTimeline} from '@gravity-ui/timeline/react';
import {GravityTimelineCanvas} from '@gravity-ui/timeline/react/uikit';

<ThemeProvider theme="light">
  <GravityTimelineCanvas timeline={timeline} />
</ThemeProvider>

Pase tokens directamente en cualquier campo de color, por ejemplo color: 'var(--g-color-base-positive-medium)'. GravityTimelineCanvas se redibuja automáticamente cuando cambia el tema efectivo de Gravity UI. Para un token faltante, use una alternativa CSS como var(--app-event-color, transparent) o llame a timeline.api.resolveColor(color, fallback) desde un renderizador personalizado.

Para eventos, color se usa normalmente, hoverColor al pasar el puntero, y selectedColor después de la selección:

const events = [
  {
    id: 'deploy',
    from: start,
    to: end,
    axisId: 'main',
    trackIndex: 0,
    color: 'var(--g-color-base-positive-medium)',
    hoverColor: 'var(--g-color-base-positive-medium-hover)',
    selectedColor: 'var(--g-color-base-positive-heavy)',
  },
];

Los renderizadores de eventos personalizados reciben resolveColor como su argumento opcional final; los renderizadores de marcadores y secciones personalizados lo reciben en sus datos de renderizado.

Fuentes del Canvas

Establezca viewConfiguration.font una vez para configurar la fuente predeterminada para la regla, eventos y marcadores. Un ruler.font, events.font o markers.font específico del componente tiene prioridad. El valor predeterminado sigue siendo 10px sans-serif.

El canvas no puede usar variables CSS o inherit directamente en ctx.font, por lo que Timeline resuelve tokens de valor completo en el contexto CSS del canvas:

viewConfiguration: {
  font: 'var(--g-text-caption-2-font)',
}

Use font: 'inherit' para usar la fuente calculada del elemento canvas. Los renderizadores personalizados reciben resolveFont junto con resolveColor, o pueden llamar a timeline.api.resolveFont(font). Después de que una fuente web se cargue dinámicamente, llame a timeline.api.rerender() para redibujar el texto del canvas con ella.

Uso Directo de TypeScript

La clase Timeline se puede usar directamente en TypeScript sin React. Esto es útil para integrarse con otros frameworks o aplicaciones JavaScript vanilla:

import { Timeline } from '@gravity-ui/timeline';

const timestamp = Date.now();

// Crear una instancia de timeline
const timeline = new Timeline({
  settings: {
    start: timestamp,
    end: timestamp + 3600000, // 1 hora a partir de ahora
    axes: [
      {
        id: 'main',
        tracksCount: 3,
        top: 0,
        height: 100
      }
    ],
    events: [
      {
        id: 'event1',
        from: timestamp + 1800000, // 30 minutos a partir de ahora
        to: timestamp + 2400000,   // 40 minutos a partir de ahora
        label: 'Evento de Muestra',
        axisId: 'main'
      }
    ],
    markers: [
      {
        id: 'marker1',
        time: timestamp + 1200000, // 20 minutos a partir de ahora
        label: 'Punto Importante',
        color: '#ff0000',
        activeColor: '#ff5252',
        hoverColor: '#ff1744'
      }
    ],
    sections: [
      {
        id: 'section1',
        from: timestamp,
        to: timestamp + 1800000, // Primeros 30 minutos
        color: 'rgba(33, 150, 243, 0.2)', // Fondo azul claro
        hoverColor: 'rgba(33, 150, 243, 0.3)'
      }
    ]
  },
  viewConfiguration: {
    // Opcional: personalizar la configuración de vista
    zoomLevels: [1, 2, 4, 8, 16],
    hideRuler: false,
    showGrid: true
  }
});

// Inicializar con un elemento canvas
const canvas = document.querySelector('canvas');
if (canvas instanceof HTMLCanvasElement) {
  timeline.init(canvas);
}

// Agregar oyentes de eventos
timeline.on('on-click', (detail) => {
  console.log('Timeline clickeado:', detail);
});

timeline.on('on-select-change', (detail) => {
  console.log('Selección cambiada:', detail);
});

// Limpiar cuando se termine
timeline.destroy();

La clase Timeline proporciona una API rica para gestionar la línea de tiempo:

  • Gestión de Eventos:

    // Agregar oyente de eventos
    timeline.on('eventClick', (detail) => {
      console.log('Evento clickeado:', detail);
    });
    
    // Eliminar oyente de eventos
    const handler = (detail) => console.log(detail);
    timeline.on('eventClick', handler);
    timeline.off('eventClick', handler);
    
    // Emitir eventos personalizados
    timeline.emit('customEvent', { data: 'datos personalizados' });
    
  • Control de Línea de Tiempo:

    // Actualizar datos de la línea de tiempo
    timeline.api.setEvents([
      {
        id: 'newEvent',
        from: Date.now(),
        to: Date.now() + 3600000,
        label: 'Nuevo Evento',
        axisId: 'main',
        trackIndex: 0
      }
    ]);
    
    // Actualizar ejes
    timeline.api.setAxes([
      {
        id: 'newAxis',
        tracksCount: 2,
        top: 0,
        height: 80
      }
    ]);
    
    // Actualizar marcadores
    timeline.api.setMarkers([
      {
        id: 'newMarker',
        time: Date.now(),
        label: 'Nuevo Marcador',
        color: '#00ff00',
        activeColor: '#4caf50',
        hoverColor: '#2e7d32'
      }
    ]);
    
    // Actualizar secciones
    timeline.api.setSections([
      {
        id: 'newSection',
        from: Date.now(),
        to: Date.now() + 1800000,
        color: 'rgba(255, 193, 7, 0.2)', // Fondo ámbar claro
        hoverColor: 'rgba(255, 193, 7, 0.3)'
      }
    ]);
    
    // Actualizar configuración de vista (se fusiona con la configuración actual)
    timeline.api.setViewConfiguration({ hideRuler: true });
    

Ejemplos en Vivo

Explora ejemplos interactivos en nuestro Storybook:

Desarrollo

Storybook

Este proyecto incluye Storybook para el desarrollo y la documentación de componentes.

Para ejecutar Storybook:

npm run storybook

Esto iniciará el servidor de desarrollo de Storybook en el puerto 6006. Puedes acceder a él en http://localhost:6006.

Para compilar una versión estática de Storybook para su despliegue:

npm run build-storybook

Licencia

MIT

Acerca de la librería
Apoya la librería con una estrella
Versión
1.32.2
Última actualización
22.09.2026
Repositorio
github.com/gravity-ui/timeline
Licencia
MIT License
Mantenedores
Colaboradores