库 / Timeline

Timeline

一个基于 React 的库,用于使用 canvas 渲染构建交互式时间线可视化。

@gravity-ui/timeline npm package Release storybook

English version

一个基于 React 的库,用于构建具有 Canvas 渲染的交互式时间轴可视化。

文档

详情请参阅 文档。

预览

带有事件和轴的基本时间轴:

Basic timeline with events

带有可展开嵌套事件的自定义渲染(NestedEvents 示例):

Nested events timeline

特性

  • 基于 Canvas 的渲染,性能高
  • 交互式时间轴,支持缩放和平移
  • 灵活的滚轮和触控板交互,包括垂直滚动穿透
  • 支持事件、标记、区域、轴和网格
  • 背景区域用于视觉组织和时间段高亮
  • 智能标记分组,自动缩放到组 - 点击分组标记可缩放到其独立组件
  • 虚拟化渲染,提高大型数据集的性能(仅在时间轴内容超出视口时激活)
  • 可自定义的外观和行为
  • 支持 TypeScript,提供完整的类型定义
  • React 集成,提供自定义 Hook

安装

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 提供了熟悉的交互预设,而 camera.interactions 则允许您覆盖单个手势。当时间轴位于可垂直滚动的页面中时,这非常有用:保留水平平移和触控板缩放,但允许正常的滚轮滚动到达父容器。

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 键的滚轮触控板手势。zoomSensitivity.in 和 zoomSensitivity.out 分别独立地乘以放大和缩小的速度:1 是默认值,较低的值更平缓,0 则在该方向禁用缩放。小的触控板偏移会自动进行平滑处理。minRange 和 maxRange 是以毫秒为单位的时长;最小值默认为 5 秒,最大值除非配置否则不受限制,因此设置 maxRange 可以限制用户可以缩小的程度。请参阅交互式的 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 构建,提供了一种灵活的方式来创建交互式时间轴可视化。其工作原理如下:

组件架构

该时间轴实现为一个 React 组件,可以通过两个主要对象进行配置:

  1. TimelineSettings: 控制时间轴的核心行为和外观

    • start: 时间轴的开始时间
    • end: 时间轴的结束时间
    • axes: 轴配置数组(见下方结构)
    • events: 事件配置数组
    • markers: 标记配置数组
    • sections: 区段配置数组
  2. ViewConfiguration: 管理视觉表示和交互设置

    • 控制外观、缩放级别和交互行为
    • 可以自定义或使用默认值

事件处理

时间轴组件支持多种交互事件:

  • on-click: 点击时间轴时触发;包含命中的元素、时间戳、视口坐标和画布坐标
  • 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 会针对其 canvas 元素解析完整的 var(--token) 值,因此 Gravity UI 的语义化 token 可用于内置事件、标记、区域、轴、网格和标尺。

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>

直接在任何颜色字段中传递 token,例如 color: 'var(--g-color-base-positive-medium)'。当有效的 Gravity UI 主题更改时,GravityTimelineCanvas 会自动重绘。对于缺失的 token,可以使用 CSS 回退值,例如 var(--app-event-color, transparent),或者从自定义渲染器调用 timeline.api.resolveColor(color, fallback)。

对于事件,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)',
  },
];

自定义事件渲染器会接收 resolveColor 作为其最后一个可选参数;自定义标记和区域渲染器则在它们的渲染数据中接收。

Canvas 字体

设置 viewConfiguration.font 一次即可配置标尺、事件和标记的默认字体。组件特定的 ruler.font、events.font 或 markers.font 将具有更高的优先级。默认值为 10px sans-serif。

Canvas 在 ctx.font 中无法直接使用 CSS 变量或 inherit,因此 Timeline 会在 canvas 的 CSS 上下文中解析完整的 token 值:

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

使用 font: 'inherit' 来使用 canvas 元素的计算字体。自定义渲染器会接收 resolveFont 和 resolveColor,或者可以调用 timeline.api.resolveFont(font)。在 Web 字体动态加载后,请调用 timeline.api.rerender() 以使用新字体重绘 canvas 文本。

直接使用 TypeScript

Timeline 类可以直接在 TypeScript 中使用,无需 React。这对于与其他框架或原生 JavaScript 应用程序集成非常有用:

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('Timeline clicked:', detail);
});

timeline.on('on-select-change', (detail) => {
  console.log('Selection changed:', detail);
});

// 完成后清理
timeline.destroy();

Timeline 类提供了一个丰富的 API 来管理时间轴:

  • 事件管理:

    // 添加事件监听器
    timeline.on('eventClick', (detail) => {
      console.log('Event clicked:', detail);
    });
    
    // 移除事件监听器
    const handler = (detail) => console.log(detail);
    timeline.on('eventClick', handler);
    timeline.off('eventClick', handler);
    
    // 触发自定义事件
    timeline.emit('customEvent', { data: 'custom data' });
    
  • 时间轴控制:

    // 更新时间轴数据
    timeline.api.setEvents([
      {
        id: 'newEvent',
        from: Date.now(),
        to: Date.now() + 3600000,
        label: '新事件',
        axisId: 'main',
        trackIndex: 0
      }
    ]);
    
    // 更新轴线
    timeline.api.setAxes([
      {
        id: 'newAxis',
        tracksCount: 2,
        top: 0,
        height: 80
      }
    ]);
    
    // 更新标记
    timeline.api.setMarkers([
      {
        id: 'newMarker',
        time: Date.now(),
        label: '新标记',
        color: '#00ff00',
        activeColor: '#4caf50',
        hoverColor: '#2e7d32'
      }
    ]);
    
    // 更新区域
    timeline.api.setSections([
      {
        id: 'newSection',
        from: Date.now(),
        to: Date.now() + 1800000,
        color: 'rgba(255, 193, 7, 0.2)', // 淡琥珀色背景
        hoverColor: 'rgba(255, 193, 7, 0.3)'
      }
    ]);
    
    // 更新视图配置(与当前配置合并)
    timeline.api.setViewConfiguration({ hideRuler: true });
    

实时示例

在我们的 Storybook 中探索交互式示例:

  • 基础时间轴 - 带有事件和轴线的简单时间轴
  • 无限时间轴 - 带有事件和轴线的无限时间轴
  • 标记 - 带有垂直标记和标签的时间轴
  • 相机交互 - 配置滚轮、水平滚动和触控板捏合行为
  • 自定义事件 - 带有自定义事件渲染的时间轴
  • 集成 - RangeDateSelection、DragHandler、NestedEvents、Popup、List

开发

Storybook

本项目包含 Storybook,用于组件开发和文档。

运行 Storybook:

npm run storybook

这将启动 Storybook 开发服务器,端口为 6006。您可以通过 http://localhost:6006 访问它。

构建 Storybook 的静态版本以进行部署:

npm run build-storybook

许可证

MIT

关于库
用星标支持该库
版本
1.32.2
最后更新
22.09.2026
代码仓库
github.com/gravity-ui/timeline
许可证
MIT License
维护者
贡献者