Examples & recipes

A collection of copy-paste snippets, weighted toward the places where vis-timeline-canvas diverges from DOM vis-timeline. For the full option list see README.html.


Construction & wiring

Divergence: groups are passed in the options object, not as a third arg.
import { DataSet } from 'vis-data';
import { CanvasTimeline } from 'vis-timeline-canvas';

const items  = new DataSet(rawItems);
const groups = new DataSet(rawGroups);

const tl = new CanvasTimeline(container, items, {
  groups,                       // <-- in options, not the 3rd argument
  height: 500,
  start: '2024-01-01',
  end:   '2024-01-08',
});

Either a plain array or a vis-data DataSet works. With a DataSet, the renderer subscribes to add/update/remove and redraws automatically.


Persisting edits (drag / resize)

Divergence: the renderer never mutates your data; you write edits back.
tl.on('itemMove', ({ items }) => {
  items.forEach(m => dataset.update(m));        // m = { id, start, end }
});
tl.on('itemResize', ({ item, start, end }) => {
  dataset.update({ id: item, start, end });
});

Magnetic snapping is on by default — dragged edges snap to nearby item edges and the now-line. Tune or disable it:

new CanvasTimeline(el, items, { snapToItems: true, snapDistance: 8 });
new CanvasTimeline(el, items, { snapToItems: false, snap: false }); // free drag

Replacing HTML templates with data fields

Divergence: template / className / CSS are not supported. Precompute the visual outcome into fields the canvas understands.
// vis-timeline (before):
options.template = (item) => `<span class="badge ${item.sev}">${item.label}</span>`;

// canvas (after): compute fields up front.
const SEV_COLOR = { info: '#4a9eff', warn: '#f9a825', error: '#e53935' };
items.update(rawItems.map(it => ({
  id: it.id,
  content: it.label,
  backgroundColor: SEV_COLOR[it.sev],
  pattern: it.sev === 'error',     // diagonal red stripes
  icon: it.sev,                    // a leading icon (see Icons below)
})));

Uniform-grey mode turns each item's color into a bottom accent strip — handy when color is a category, not the item's identity:

new CanvasTimeline(el, items, {
  uniformItemColor: true,
  uniformItemBg: '#9e9e9e',
  accentBorderHeight: 4,
});

Rich content without templates

Plain-text tooltips come from the title field. For anything richer, render your own DOM overlay on an event:

tl.on('doubleClick', ({ item, event }) => openDetailPopover(item, event));
tl.on('contextmenu', ({ item, event }) => showContextMenu(item, event)); // right-click

Reacting to pan/zoom

Divergence (now closed): earlier versions had no rangechange. They exist now, plus you can always read the window directly.
tl.on('rangechange',  ({ start, end }) => updateUrlDebounced(start, end)); // continuous
tl.on('rangechanged', ({ start, end }) => fetchForWindow(start, end));      // settled

const { start, end } = tl.getWindow(); // pull on demand

Icons (glyphs & sprite atlas)

Leading icons render at the start of an item. Reference them by name; a name that isn't registered is drawn as a raw glyph (so emoji work directly).

const tl = new CanvasTimeline(el, items, {
  icons: {
    error:   { glyph: '⚠', color: '#e53935' },
    done:    { glyph: '✓', color: '#43a047' },
    deploy:  { src: '/icons/rocket.png' },     // standalone image
  },
});

items.update([
  { id: 1, content: 'Build',  start, end, icon: 'done' },
  { id: 2, content: 'Outage', start, end, icons: ['error', '🔥'] }, // mix + emoji
]);

Many icons in one texture? Use a sprite atlas:

new CanvasTimeline(el, items, {
  iconAtlas: {
    src: '/sprites/status.png',
    icons: {
      ok:   { x: 0,  y: 0, w: 16, h: 16 },
      warn: { x: 16, y: 0, w: 16, h: 16 },
      bad:  { x: 32, y: 0, w: 16, h: 16 },
    },
  },
});
// item.icon = 'warn'

Block items

A block spans its parent group's whole band (the parent + all subgroups) for a time range — useful for maintenance windows, releases, incidents.

items.add({
  id: 'mw1', type: 'block', group: 'platform',
  content: 'Maintenance', start, end, backgroundColor: '#ff7043',
});

By default a block sits behind items (translucent). Put it on the front layer to cover the items beneath, and/or fill it with a gradient:

items.add({
  id: 'mask', type: 'block', layer: 'front', opacity: 0.8,   // covers items
  group: 'platform', start, end, backgroundColor: '#37474f',
});

items.add({
  id: 'rel', type: 'block', group: 'platform', start, end,
  backgroundColor: '#ff7043',
  gradient: ['#ff7043', '#e53935'],        // or gradient: true (auto)
  gradientDirection: 'vertical',
});

Theming

const tl = new CanvasTimeline(el, items, { theme: 'dark' });
tl.setTheme('light');                       // switch at runtime

// Custom theme: a partial object merged over the light preset.
tl.setTheme({
  canvasBg: '#0b0f17',
  gridLine: '#1b2230',
  selectionBorder: '#22d3ee',
});

Highlight specific groups (e.g. the one the user is focused on):

tl.setGroupHighlight('platform', '#ffd54f');
tl.clearGroupHighlights();

Follow-now, minimap, time probe

const tl = new CanvasTimeline(el, items, { minimap: true, timeProbe: true });

tl.setFollowNow(true, 0.85);   // lock to now; "now" sits 85% across the window
tl.isFollowingNow();           // => true
// Grabbing the timeline to pan automatically turns follow off and emits followNow.

Parallel run with vis-timeline

Both renderers consume the same DataSet, so you can mount them side by side behind a flag during migration. See transition.html for the full adapter; the short version:

const items  = new DataSet(rawItems);
const groups = new DataSet(rawGroups);

new Timeline(document.getElementById('old'), items, groups, oldOptions);
new CanvasTimeline(document.getElementById('new'), items, { ...mapOptions(oldOptions), groups });

// One writer: edits flow to both renderers.
items.update({ id: 42, start: newStart });

← README · Demos & docs