vis-timeline-canvas

A high-performance, canvas-based fork of vis-timeline. It keeps the familiar data model (plain arrays or vis-data DataSets of items and groups) but replaces the one-DOM-node-per-item renderer with a single <canvas>, so it stays fast into the tens of thousands of items.

Migrating an existing integration? See transition.html. Go/no-go analysis: risks.html. Deferred perf work: optimizations.html.

Install

An ESM package, installable from source — a git URL, a local path, or a packed tarball:

# from the git repo
npm install git+ssh://git@gitlab.com:willjessop12/canvas-timeline.git

# or from a local checkout / packed tarball
npm install /path/to/canvas-timeline
npm pack   # -> vis-timeline-canvas-x.y.z.tgz, then `npm install ./that.tgz`

The package depends on vis-timeline and vis-data so it stays consistent and interoperable with an existing vis-timeline / vis-data integration. It does not import vis-timeline's source (renderer) — it only duck-types DataSets — so it stays light; vis-timeline is declared for ecosystem consistency and the parallel-run migration path (see transition.html).

For convenience the package re-exports the data containers, so you can import the timeline and its DataSet from one place:

import { CanvasTimeline, DataSet } from 'vis-timeline-canvas';

To pull in only the renderer (no vis-data re-export), use the subpath:

import { CanvasTimeline } from 'vis-timeline-canvas/renderer';

Quick start

import { CanvasTimeline, DataSet } from 'vis-timeline-canvas';

const items = new DataSet([
  { id: 1, content: 'Kickoff', start: '2024-01-01T09:00', end: '2024-01-01T11:00', group: 'a' },
  { id: 2, content: 'Review',  start: '2024-01-01T13:00', end: '2024-01-01T15:00', group: 'b' },
]);
const groups = [{ id: 'a', content: 'Team A' }, { id: 'b', content: 'Team B' }];

const timeline = new CanvasTimeline('#timeline', items, {
  groups,
  theme: 'dark',
  height: 500,
});

timeline.on('select', ({ items }) => console.log('selected', items));
timeline.on('itemMove', ({ items }) => items.forEach(m => dataset.update(m)));

Note the two API differences from vis-timeline highlighted up front:

  1. Groups are passed in the options object ({ groups }), not as a third constructor argument.
  2. You persist edits. Drag/resize emit itemMove / itemResize; the renderer never writes back to your DataSet on its own.

Options reference

Every option is optional. Defaults are shown in parentheses.

Sizing & layout

OptionDefaultWhat it does
height400Body height in px (the axis and minimap are drawn outside this).
itemHeight32Height of a single item row in px.
itemMargin4Vertical gap between stacked rows in px. Set 0 for dense rows.
itemSpacing2Horizontal gap reserved between items when deciding if they fit on the same row. 0 lets adjacent items touch.
itemBorderRadiusnullGlobal corner radius override. 0 = square corners; null keeps the per-item radius.
groupMinHeight40Minimum height of a group band in px.
groupLabelWidth150Width of the left group-label panel in px.
groupOrder'order'Field name to sort groups by, or a comparator (a, b) => number over the raw group objects.

Time window & zoom

OptionDefaultWhat it does
start / enddata rangeInitial visible window (use both).
min / maxnullHard pan/zoom bounds. If both are set, the bounds are exactly this range.
minZoom60000Smallest visible duration in ms (max zoom-in).
maxZoomnullLargest visible duration in ms (max zoom-out). null = the full data range.
zoomSpeed0.002Wheel-zoom sensitivity.
With no min/max, the bounds are padded ~5% around the data and expanded to a 24-hour minimum span so single-item timelines still open at a sane scale.

Stacking, editing & snapping

OptionDefaultWhat it does
stacktrueStack overlapping items into rows. false overlaps them on one row.
timeResolution60000The time-resolution unit in ms for manual edits. Drag/resize/keyboard-nudge are quantized to multiples of this, and committed values snap to the nearest unit on mouseup. Default one minute.
snap'minute'Edit quantization: 'minute' snaps to timeResolution; false = free editing; or a function (ms) => ms.
snapToItemstrueMagnetically snap edited edges to nearby item edges and the now-line.
snapDistance6Magnetic snap radius in px.

Selection & interaction

OptionDefaultWhat it does
tooltipDelay300Hover delay (ms) before the title tooltip shows.
verticalScrollbartrueShow the draggable scrollbar on the left of the body (auto-hides when content fits).
scrollbarWidth12Scrollbar width in px.
keyboardtrueEnable desktop keyboard navigation + the focus ring (see below).
fullscreenButtontrueShow a fullscreen toggle button in the zoom-control overlay.

Appearance

OptionDefaultWhat it does
theme'light''light', 'dark', or a partial object merged over the light preset.
hour24trueShow axis/probe time-of-day on a 24-hour clock (13:00) instead of 12-hour with AM/PM.
fontFamilysystem sansCSS font-family for all canvas text.
fontSize12Base font size in px (axis/probe use one px smaller).
uniformItemColorfalseDraw every item in one grey color (uniformItemBg) with its own color as a bottom accent strip.
uniformItemBg'#9e9e9e'The uniform fill color used when uniformItemColor is on.
accentBorderHeight3Height of the bottom accent strip in px.

Icons

OptionDefaultWhat it does
iconsnullRegistry of named icons: { name: { glyph, color } } or { name: { src } }.
iconAtlasnullSprite sheet: { src | image, icons: { name: {x,y,w,h} } }.
iconSize14Icon draw size in px.
iconGap3Gap between leading icons in px.

Overlays

OptionDefaultWhat it does
timeProbefalseVertical line following the mouse; the time chip shows on the minimap (or the bottom axis if there's no minimap).
minimapfalseOverview minimap with drag-to-zoom.
minimapHeight48Minimap height in px.
topAxisfalseAlso draw the time axis above the body (in addition to below).
followInterval1000Follow-now tick interval in ms (see setFollowNow).

Item fields

{
  id, start,            // required
  end,                  // omit => a "box" item (point-in-time)
  content,              // plain text label (HTML is NOT parsed)
  group,                // group id
  type,                 // 'range' | 'box' | 'block'
  title,                // plain-text tooltip
  // colors (preferred over a CSS `style` string)
  backgroundColor, borderColor, color,
  // canvas extras
  accentColor,          // bottom strip color in uniform mode
  pattern,              // true | 'stripes' — diagonal stripe overlay (errors)
  patternColor,
  noText,               // hide the label; renders a compact marker
  icon, icons,          // leading icon name(s)
  // block-only (type:'block')
  layer,                // 'back' (default) | 'front' (covers items)
  onTop,                // alias for layer:'front'
  opacity,              // override block fill alpha
  gradient,             // true (auto) | ['#a','#b'] color stops
  gradientDirection,    // 'vertical' (default) | 'horizontal'
}

Group fields

{
  id, content,
  order,                // sort key
  nestedGroups,         // [childId, ...] to nest groups
  showNested,           // start expanded (default true)
  visible,              // hide the group
  backgroundColor, color,
  accentColor,          // highlight tint
}

Events

Subscribe with timeline.on(name, cb) / unsubscribe with off.

EventPayload
select{ items: id[] }
doubleClick{ item, event } (also fired by Enter on the focused item)
itemMove{ items: [{ id, start, end }] }persist these
itemResize{ item, start, end, edge }persist these
groupClick{ group, event }
groupCollapse{ groupId, collapsed }
followNow{ enabled }
rangechange{ start, end } (continuous during pan/zoom)
rangechanged{ start, end } (debounced after motion settles)
itemover / itemout{ item, event }
contextmenu{ item | null, event } (right-click)
render{ visibleCount }

Methods

setItems · setGroups · getGroups · getItemCount · fit · setWindow · getWindow · moveTo · zoomIn · zoomOut · toggleFullscreen · isFullscreen · setSelection · getSelection · toggleGroupCollapse · setGroupHighlight · clearGroupHighlights · setFollowNow · isFollowingNow · setTheme · on · off · redraw · destroy. See src/canvas-timeline.d.ts for full signatures.

Keyboard (when keyboard is on)

Click the timeline to focus it, then:


How this fork diverges from vis-timeline

These are the behaviors most likely to surprise you when migrating. Worked examples are in EXAMPLES.html.

See transition.html for the full migration plan and a parallel-run harness.


Project layout

The renderer is one class spread across focused modules. The big method groups are defined as "mixin" classes and copied onto CanvasTimeline.prototype at load (see src/mixin.js), so they share one this while living in separate, readable files.

index.js               package entry: re-exports CanvasTimeline + DataSet/DataView
index.d.ts             package types
src/
  canvas-timeline.js   core: constructor, options, public API, setup, data
  mixin.js             applyMixins(target, ...sources)
  timeline/
    layout.js          viewport/time-bounds math, stacking, snapping
    rendering.js       the canvas draw path (grid, items, blocks, axis, minimap)
    interaction.js     pointer + keyboard input, drag/resize, hit-testing
  theme.js             light/dark presets + buildTheme()
  colors.js            lighten / darken / withAlpha
  canvas-draw.js       roundRect / truncateText
  time-format.js       axis + probe label formatting
  canvas-timeline.d.ts TypeScript declarations

Run the tests with npm test (layout/unit assertions + a headless render smoke test).

← Back to demos & docs · Examples & recipes →