Timeline
@gravity-ui/timeline

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:

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

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:
-
TimelineSettings: Controla el comportamiento y la apariencia principal de la línea de tiempo
start: Hora de inicio de la línea de tiempoend: Hora de finalización de la línea de tiempoaxes: Matriz de configuraciones de ejes (ver estructura a continuación)events: Matriz de configuraciones de eventosmarkers: Matriz de configuraciones de marcadoressections: Matriz de configuraciones de secciones
-
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 lienzoon-context-click: Se activa al hacer clic derecho/menú contextualon-select-change: Se dispara cuando cambia la selecciónon-hover: Se activa al pasar el cursor sobre elementos de la línea de tiempoon-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.
Popup de Eventos
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:
- Línea de Tiempo Básica - Línea de tiempo simple con eventos y ejes
- Línea de Tiempo Infinita - Línea de tiempo infinita con eventos y ejes
- Marcadores - Línea de tiempo con marcadores verticales y etiquetas
- Interacciones de Cámara - Configura el comportamiento de la rueda, el desplazamiento horizontal y el pellizco del trackpad
- Eventos Personalizados - Línea de tiempo con renderizado de eventos personalizado
- Integraciones - RangeDateSelection, DragHandler, NestedEvents, Popup, List
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