Timeline

Библиотека на основе React для создания интерактивных визуализаций временной шкалы с отрисовкой на canvas.

@gravity-ui/timeline npm package Release storybook

English

React-библиотека для построения интерактивных временных шкал с рендерингом на canvas.

Документация

Подробности см. в Документации.

Превью

Базовая шкала с событиями и осями:

Базовая шкала с событиями

Кастомный вариант с раскрывающимися вложенными событиями (пример NestedEvents):

Вложенные события на шкале

Возможности

  • Рендеринг на canvas для высокой производительности
  • Интерактивная шкала с масштабированием и панорамированием
  • Гибкая настройка wheel и trackpad-жестов, включая передачу вертикального скролла родителю
  • Поддержка событий, маркеров, секций, осей и сетки
  • Фоновые секции для визуальной организации и выделения периодов
  • Умная группировка маркеров с автоматическим зумом по группе — клик по сгруппированным маркерам приближает их по отдельности
  • Виртуализированный рендеринг для больших наборов данных (включается, когда содержимое шкалы выходит за пределы видимой области)
  • Настраиваемый внешний вид и поведение
  • Поддержка TypeScript с полными типами
  • Интеграция с React и кастомными хуками

Установка

npm install @gravity-ui/timeline

Использование

Компонент временной шкалы можно использовать в React-приложениях с такой базовой настройкой:

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 час
      axes: [],
      events: [],
      markers: [],
      sections: []
    },
    viewConfiguration: {
      // Опциональная конфигурация вида
    }
  });

  // timeline — экземпляр Timeline
  // api — экземпляр CanvasApi (то же, что timeline.api)
  // start — функция инициализации шкалы с canvas
  // stop — функция уничтожения шкалы

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

Структура оси

Каждая ось задаётся так:

type TimelineAxis = {
  id: string;          // Уникальный идентификатор оси
  tracksCount: number; // Количество треков на оси
  top: number;         // Вертикальная позиция (px)
  height: number;      // Высота одного трека (px)
};

Горизонтальные линии оси

Положение горизонтальных линий задаётся через viewConfiguration.axes.linePosition:

  • "center" (по умолчанию) рисует линию через центр каждого трека.
  • "between" рисует линию после каждого трека, по его нижней границе. Режим подходит для табличных строк с событиями по центру.
viewConfiguration: {
  axes: {
    linePosition: 'between'
  }
}

Гибкая настройка взаимодействий камеры

ZoomMode задаёт привычный preset взаимодействий, а camera.interactions позволяет переопределить отдельный жест. Это удобно, когда шкала находится внутри вертикально прокручиваемой страницы: горизонтальное перемещение и зум с trackpad сохраняются, а обычный wheel передаётся родительскому контейнеру.

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,
    },
  },
});

Для каждого взаимодействия доступны 'zoom', 'pan' и 'pass-through'. pinch соответствует Ctrl+wheel, который браузер генерирует для zoom-жеста на trackpad. zoomSensitivity.in и zoomSensitivity.out независимо задают множитель скорости zoom in и zoom out: 1 — значение по умолчанию, меньшие значения делают масштабирование плавнее, а 0 отключает zoom в соответствующем направлении. Малые delta трекпада сглаживаются автоматически. minRange и maxRange задаются в миллисекундах: минимум по умолчанию — 5 секунд, а максимум не ограничен, пока его не указали; используйте maxRange, чтобы ограничить дальность zoom out. Настройку можно попробовать в интерактивном примере Camera interactions в Storybook.

Структура секции

Каждая секция должна иметь такую структуру:

type TimelineSection = {
  id: string;               // Уникальный идентификатор секции
  from: number;             // Начальная метка времени
  to?: number;              // Конечная метка (опционально, по умолчанию — конец шкалы)
  color: string;            // Цвет фона секции
  hoverColor?: string;      // Цвет при наведении (опционально)
  renderer?: AbstractSectionRenderer; // Опциональный кастомный рендерер (экспортируется из пакета)
};

Секции задают фоновую подсветку периодов и помогают визуально организовать содержимое:

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 минут
          color: 'rgba(255, 235, 59, 0.3)', // полупрозрачный жёлтый
          hoverColor: 'rgba(255, 235, 59, 0.4)'
        },
        {
          id: 'afternoon',
          from: Date.now() + 1800000,
          // 'to' не указан — до конца шкалы
          color: 'rgba(76, 175, 80, 0.2)', // полупрозрачный зелёный
          hoverColor: 'rgba(76, 175, 80, 0.3)'
        }
      ]
    },
    viewConfiguration: {
      sections: {
        hitboxPadding: 2 // Отступ для определения наведения
      }
    }
  });

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

Структура маркера

Каждый маркер должен иметь такую структуру:

type TimelineMarker = {
  time: number;           // Метка времени позиции маркера
  color: string;          // Цвет линии маркера
  activeColor: string;    // Цвет при выборе (обязательно)
  hoverColor: string;     // Цвет при наведении (обязательно)
  lineWidth?: number;     // Толщина линии (опционально)
  label?: string;         // Подпись (опционально)
  labelColor?: string;    // Цвет подписи (опционально)
  renderer?: AbstractMarkerRenderer; // Опциональный кастомный рендерер
  nonSelectable?: boolean;// Нельзя выбрать
  group?: boolean;        // Маркер представляет группу
};

Группировка маркеров и зум

Шкала автоматически группирует близкие маркеры и поддерживает зум:

const MyTimelineComponent = () => {
  const { timeline } = useTimeline({
    settings: {
      start: Date.now(),
      end: Date.now() + 3600000,
      axes: [],
      events: [],
      markers: [
        // Эти маркеры будут сгруппированы
        { time: Date.now(), color: '#ff0000', activeColor: '#ff5252', hoverColor: '#ff1744', label: 'Событие 1' },
        { time: Date.now() + 1000, color: '#ff0000', activeColor: '#ff5252', hoverColor: '#ff1744', label: 'Событие 2' },
        { time: Date.now() + 2000, color: '#ff0000', activeColor: '#ff5252', hoverColor: '#ff1744', label: 'Событие 3' },
      ]
    },
    viewConfiguration: {
      markers: {
        collapseMinDistance: 8,        // Группировать маркеры в пределах 8 пикселей
        groupZoomEnabled: true,        // Зум по клику на группу
        groupZoomPadding: 0.3,         // Отступ 30% вокруг группы
        groupZoomMaxFactor: 0.3,       // Максимальный коэффициент зума
      }
    }
  });

  // Слушаем зум по группе
  useTimelineEvent(timeline, 'on-group-marker-click', (data) => {
    console.log('Группа увеличена:', data);
  });

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

Как это устроено

Компонент временной шкалы построен на React и даёт гибкий способ создавать интерактивные шкалы. Кратко об устройстве:

Архитектура компонента

Шкала настраивается двумя основными объектами:

  1. TimelineSettings — ядро шкалы и отображение:

    • start: начало шкалы
    • end: конец шкалы
    • axes: конфигурации осей (см. структуру ниже)
    • events: конфигурации событий
    • markers: конфигурации маркеров
    • sections: конфигурации секций
  2. ViewConfiguration — вид и взаимодействие:

    • Внешний вид, уровни зума, поведение при взаимодействии
    • Можно кастомизировать или использовать значения по умолчанию

Обработка событий

Поддерживаются события:

  • on-click — клик по шкале; содержит найденные элементы, время, координаты viewport и координаты внутри canvas
  • on-context-click — правый клик / контекстное меню
  • on-select-change — изменение выделения
  • on-hover — наведение на элементы
  • on-leave — курсор покинул элементы

Пример обработки:

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

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

  useTimelineEvent(timeline, 'on-click', (data) => {
    console.log('Клик по шкале:', data);
  });

  useTimelineEvent(timeline, 'on-select-change', (data) => {
    console.log('Выделение изменилось:', data);
  });

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

Интеграция с React

Используются кастомные хуки:

  • useTimeline — экземпляр шкалы и жизненный цикл:

    • Создание и инициализация
    • Очистка при размонтировании
    • Доступ к экземпляру шкалы
  • useTimelineEvent — подписки на события и очистка:

    • Управление подписчиками
    • Автоочистка при размонтировании

При размонтировании компонента экземпляр шкалы автоматически уничтожается.

Поповер события

Установите @gravity-ui/uikit и подключите его стили, чтобы показывать детали события без подписок на наведение и расчёта координат:

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} />}
  />
</>

Тултип, подсветка и курсор используют одно событие: точное попадание приоритетнее соседних событий. При перекрытии выбирается последнее событие в порядке отрисовки. Только при отсутствии точного попадания применяется допуск 3 px + events.hitboxPadding. Групповой поиск и on-hover по-прежнему включают всех кандидатов.

EventPopup открывается через 150 мс и закрывается через 200 мс после ухода курсора с события. При необходимости задайте openDelay, closeDelay, placement, offset, className или aria-label. Поповер остаётся открытым, пока указатель или фокус находятся в его содержимом; Escape и клик снаружи его закрывают. При перекрытии выбирается последнее событие в порядке данных. hoverColor и isHovered отвечают за отрисовку события, EventPopup — за UI его деталей.

Структура события

События на шкале описываются так:

type TimelineEvent = {
  id: string;             // Уникальный идентификатор
  from: number;           // Начальная метка времени
  to?: number;            // Конечная метка (опционально для точечных событий)
  axisId: string;        // ID оси
  trackIndex: number;    // Индекс трека на оси
  renderer?: AbstractEventRenderer; // Опциональный кастомный рендерер
  color?: string;        // Цвет события (опционально)
  hoverColor?: string;   // Цвет при наведении (опционально)
  selectedColor?: string;// Цвет при выделении (опционально)
  cursor?: string;       // CSS-курсор при наведении на событие (опционально)
};

Для событий, которые выполняют действие по клику, задайте cursor: 'pointer'. Курсор применяется только над этим событием; при перекрытии используется последнее событие в порядке данных.

Цвета Gravity UI

Canvas сам не умеет резолвить CSS-переменные. Timeline резолвит значение вида var(--token) относительно своего canvas, поэтому семантические токены Gravity UI работают во встроенных событиях, маркерах, секциях, осях, сетке и ruler.

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

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

Передавайте токен в любом поле цвета, например color: 'var(--g-color-base-positive-medium)'. GravityTimelineCanvas автоматически перерисовывает шкалу при смене фактической темы Gravity UI. Для отсутствующего токена используйте CSS fallback, например var(--app-event-color, transparent), или вызывайте timeline.api.resolveColor(color, fallback) в кастомном renderer.

Для событий color используется обычно, hoverColor — при наведении, а selectedColor — после выбора:

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)',
  },
];

Кастомный event renderer получает resolveColor последним необязательным аргументом, а marker и section renderers — в объекте параметров.

Шрифты canvas

Укажите viewConfiguration.font один раз, чтобы задать шрифт по умолчанию для ruler, events и markers. Локальные ruler.font, events.font и markers.font имеют приоритет. Дефолт остаётся 10px sans-serif.

Canvas не умеет напрямую применять CSS-переменные или inherit в ctx.font, поэтому Timeline резолвит whole-value токены в CSS-контексте canvas:

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

font: 'inherit' использует computed font самого canvas. Кастомные renderer’ы получают resolveFont рядом с resolveColor, либо могут вызвать timeline.api.resolveFont(font). После динамической загрузки web-font вызовите timeline.api.rerender(), чтобы canvas перерисовал текст новым шрифтом.

Прямое использование в TypeScript

Класс Timeline можно использовать без React (например, с другими фреймворками или в vanilla JS):

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

const timestamp = Date.now();

// Создание экземпляра
const timeline = new Timeline({
  settings: {
    start: timestamp,
    end: timestamp + 3600000, // через 1 час
    axes: [
      {
        id: 'main',
        tracksCount: 3,
        top: 0,
        height: 100
      }
    ],
    events: [
      {
        id: 'event1',
        from: timestamp + 1800000, // через 30 минут
        to: timestamp + 2400000,    // через 40 минут
        label: 'Sample Event',
        axisId: 'main'
      }
    ],
    markers: [
      {
        id: 'marker1',
        time: timestamp + 1200000, // через 20 минут
        label: 'Important Point',
        color: '#ff0000',
        activeColor: '#ff5252',
        hoverColor: '#ff1744'
      }
    ],
    sections: [
      {
        id: 'section1',
        from: timestamp,
        to: timestamp + 1800000, // первые 30 минут
        color: 'rgba(33, 150, 243, 0.2)', // светло-синий фон
        hoverColor: 'rgba(33, 150, 243, 0.3)'
      }
    ]
  },
  viewConfiguration: {
    zoomLevels: [1, 2, 4, 8, 16],
    hideRuler: false,
    showGrid: true
  }
});

// Инициализация с canvas
const canvas = document.querySelector('canvas');
if (canvas instanceof HTMLCanvasElement) {
  timeline.init(canvas);
}

// Подписка на события
timeline.on('on-click', (detail) => {
  console.log('Клик по шкале:', detail);
});

timeline.on('on-select-change', (detail) => {
  console.log('Выделение изменилось:', detail);
});

// Очистка
timeline.destroy();

Класс Timeline предоставляет API для управления шкалой:

  • События:

    timeline.on('eventClick', (detail) => { /* ... */ });
    timeline.off('eventClick', handler);
    timeline.emit('customEvent', { data: 'custom data' });
    
  • Управление данными:

    timeline.api.setEvents([...]);
    timeline.api.setAxes([...]);
    timeline.api.setMarkers([...]);
    timeline.api.setSections([...]);
    // setViewConfiguration сливается с текущей конфигурацией
    timeline.api.setViewConfiguration({ hideRuler: true });
    

Примеры

Интерактивные примеры в Storybook:

  • Basic Timeline — простая шкала с событиями и осями
  • Endless Timeline — бесконечная шкала
  • Markers — шкала с маркерами и подписями
  • Camera interactions — настройка wheel, горизонтального скролла и zoom-жеста trackpad
  • Custom Events — кастомный рендеринг событий
  • Integrations — RangeDateSelection, DragHandler, NestedEvents, Popup, List

Разработка

Storybook

В проекте есть Storybook для разработки и документации компонентов.

Запуск:

npm run storybook

Сервер будет доступен на http://localhost:6006.

Сборка статической версии Storybook:

npm run build-storybook

Лицензия

MIT

О библиотеке
Поддержите библиотеку звездой
Версия
1.32.2
Последнее обновление
22.09.2026
Репозиторий
github.com/gravity-ui/timeline
Лицензия
MIT License
Maintainers
Участники