Migration Plan: vis-timeline → vis-timeline-canvas

This document is written to be followed by a coding agent. It describes how to migrate an existing vis-timeline integration onto the canvas renderer (src/canvas-timeline.js), and — importantly — how to run both in parallel behind a flag so you can validate before cutting over.

Guiding principle: the canvas renderer keeps the same data model (vis-data DataSets of items and groups) but replaces the rendering and the options/template surface. Migration effort is proportional to how much you rely on custom HTML templates and editing callbacks, not to how much data you have.

1. API mapping cheat-sheet

Construction

// vis-timeline
import { Timeline } from 'vis-timeline/standalone';
const tl = new Timeline(container, itemsDataSet, groupsDataSet, options);

// vis-timeline-canvas
import { CanvasTimeline } from './src/canvas-timeline.js';
const tl = new CanvasTimeline(container, itemsDataSet, {
  ...options,                 // see option mapping below
  groups: groupsDataSet       // groups move INTO the options object
});

Both accept either a plain array or a vis-data DataSet. When given a DataSet, the canvas renderer subscribes to add/update/ remove and redraws automatically — same as vis-timeline.

Item fields

FieldSupportedNotes
id, start, end, groupYesIdentical semantics. No end ⇒ a box item.
contentPlain text onlyRendered as canvas text. HTML strings are not parsed. See §4.
typebox / range / blockpoint and background not implemented; block is new (spans a parent's subgroups).
classNameNo CSSCanvas can't apply stylesheet classes. Map styling to data fields (below).
style (CSS string)Colors onlyParses background-color, border-color, color.
backgroundColor/borderColor/colorYesDirect fields, preferred over CSS strings.
titleYesShown in the hover tooltip.
accentColor, pattern, patternColor, noTextNewCanvas-only extras (accent strip, error stripes, textless markers).

Options

vis-timelinecanvas equivalent
heightheight (number, px)
min / maxmin / max (hard pan bounds)
start / endstart / end (initial window)
stackstack
zoomMin / zoomMaxminZoom / maxZoom (ms)
groupOrdergroupOrder (field name or comparator)
margin.itemitemMargin + itemSpacing
orientationNot supported (axis is bottom; labels left)
editableDrag-move & edge-resize are always on; emit itemMove/itemResize — you persist
template / groupTemplateNot supported — see §4

Events

vis-timelinecanvasPayload
selectselect{ items: id[] }
doubleClickdoubleClick{ item, event }
drag/moveitemMove{ items: [{id,start,end}] }
resizeitemResize{ item, start, end, edge }
group clickgroupClick{ group, event }
rangechange / rangechangedYesPayload is { start: Date, end: Date }; first initial render is intentionally skipped.
Action for the agent: grep the existing codebase for template:, groupTemplate, CSS-driven className styling, and type: 'point' / 'background'. These are the migration blockers; also grep .on('rangechange / .on('rangechanged to verify consumers only need { start, end }. List every hit before writing code.

2. Recommended phased rollout

  1. Phase 0 — Inventory. Produce the grep report above. Decide per call-site whether the feature is supported, has a data-driven equivalent, or is a true gap. Treat template/groupTemplate, CSS class styling, and point/background item types as blockers until mapped or explicitly dropped.
  2. Phase 1 — Adapter. Write a thin TimelineAdapter (see §3) exposing the subset of the vis-timeline API your app actually calls, backed by CanvasTimeline. Do not touch call-sites yet.
  3. Phase 2 — Parallel run. Render both timelines from the same DataSets behind a flag (§3). Ship to a staging/canary cohort. Compare visually and functionally.
  4. Phase 3 — Flip default. Default the flag on; keep the old path one release as a kill-switch.
  5. Phase 4 — Remove vis-timeline. Delete the old path, the adapter shims for unsupported features, and the dependency.

3. Parallel-run harness (the important part)

Both renderers consume the same vis-data DataSet, so they stay in sync for free — edits made through the DataSet propagate to whichever renderer is mounted. Mount one at a time (toggle) or both stacked (side-by-side compare).

// timeline-factory.js — single switch point for the whole app.
import { Timeline } from 'vis-timeline/standalone';
import { CanvasTimeline } from './src/canvas-timeline.js';

const USE_CANVAS = localStorage.getItem('timeline2') === 'on'; // or your flag system

export function createTimeline(container, items, groups, options) {
  if (USE_CANVAS) {
    return new CanvasTimelineAdapter(container, items, groups, options);
  }
  return new Timeline(container, items, groups, options);
}

// Adapter: present a vis-timeline-shaped surface over CanvasTimeline.
class CanvasTimelineAdapter {
  constructor(container, items, groups, options = {}) {
    this._tl = new CanvasTimeline(container, items, { ...mapOptions(options), groups });
  }
  on(evt, cb)        { this._tl.on(evt, cb); }           // supported events in §1
  off(evt, cb)       { this._tl.off(evt, cb); }
  setItems(i)        { this._tl.setItems(i); }
  setGroups(g)       { this._tl.setGroups(g); }
  setWindow(s, e)    { this._tl.setWindow(s, e); }
  getWindow()        { return this._tl.getWindow(); }
  setSelection(ids)  { this._tl.setSelection(ids); }
  getSelection()     { return this._tl.getSelection(); }
  fit()              { this._tl.fit(); }
  moveTo(t)          { this._tl.moveTo(t); }
  destroy()          { this._tl.destroy(); }
}

function mapOptions(o) {
  return {
    height: o.height, stack: o.stack, min: o.min, max: o.max,
    start: o.start, end: o.end, minZoom: o.zoomMin, maxZoom: o.zoomMax,
    groupOrder: o.groupOrder, itemMargin: o.margin?.item,
    // carry through any canvas-only extras you opt into:
    theme: o.theme, minimap: o.minimap, uniformItemColor: o.uniformItemColor
  };
}

Side-by-side compare mode

// Mount BOTH against one DataSet to eyeball parity during Phase 2.
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 });

// Edits flow to both:  items.update({ id: 42, start: newStart });
Why this works: the canvas renderer never writes to the DataSet on its own; user drag/resize changes its internal preview state and emits itemMove/ itemResize. Make your persistence layer the single writer (call items.update(...) in those handlers) and both renderers observe the same source of truth.

4. Handling the real blockers

HTML templates → data-driven styling

The canvas draws plain text plus colored rectangles; it cannot render arbitrary DOM per item. Convert template logic into precomputed item fields:

// Before (vis-timeline):
options.template = (item) => `<span class="badge ${item.sev}">${item.label}</span>`;

// After: precompute fields the canvas understands.
items.forEach(it => items.update({
  id: it.id,
  content: it.label,
  backgroundColor: SEV_COLOR[it.sev],
  pattern: it.sev === 'error',         // red stripes for errors (Feature 2)
}));

For genuinely rich tooltips, keep using the title field (plain text) or render your own absolutely-positioned DOM overlay on the doubleClick event.

Editing persistence

tl.on('itemMove',  ({ items: moved }) => moved.forEach(m => dataset.update(m)));
tl.on('itemResize', ({ item, start, end }) => dataset.update({ id: item, start, end }));

5. Acceptance checklist before flipping the default

→ Risks & go/no-go analysis · ← Back to demos