vis-timeline-canvas
v1.0.0
Published
Standalone, high-performance canvas renderer for vis-timeline-style data — a drop-in component with vis-data interop
Maintainers
Readme
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.
- Drop-in-ish data model — items/groups look like vis-timeline's.
- Canvas rendering — viewport culling, cached text widths and group heights.
- Extras not in stock vis-timeline — minimap, follow-now, time probe, uniform-color + accent strips, error stripes, block-across-subgroups, blocks that gradient/cover, per-instance theming, leading item icons, a left scrollbar, and desktop keyboard navigation.
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://[email protected]: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:
- Groups are passed in the options object (
{ groups }), not as a third constructor argument. - You persist edits. Drag/resize emit
itemMove/itemResize; the renderer never writes back to yourDataSeton its own.
Options reference
Every option is optional. Defaults are shown in parentheses.
Sizing & layout
| Option | Default | What it does |
| --- | --- | --- |
| height | 400 | Body height in px (the axis and minimap are drawn outside this). |
| itemHeight | 32 | Height of a single item row in px. |
| itemMargin | 4 | Vertical gap between stacked rows in px. Set 0 for dense rows. |
| itemSpacing | 2 | Horizontal gap reserved between items when deciding if they fit on the same row. 0 lets adjacent items touch. |
| itemBorderRadius | null | Global corner radius override. 0 = square corners; null keeps the per-item radius. |
| groupMinHeight | 40 | Minimum height of a group band in px. |
| groupLabelWidth | 150 | Width 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. |
| subgroupOrder | null | How to sort subgroups (nested groups) within their parent: a raw-group field name or a comparator (a, b) => number. null keeps the parent's nestedGroups array order. |
Time window & zoom
| Option | Default | What it does |
| --- | --- | --- |
| start / end | data range | Initial visible window (use both). |
| min / max | null | Hard pan/zoom bounds. If both are set, the bounds are exactly this range. |
| minZoom | 60000 | Smallest visible duration in ms (max zoom-in). |
| maxZoom | null | Largest visible duration in ms (max zoom-out). null = the full data range. |
| zoomSpeed | 0.002 | Wheel-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
| Option | Default | What it does |
| --- | --- | --- |
| stack | true | Stack overlapping items into rows. false overlaps them on one row. |
| timeResolution | 60000 | The 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. |
| snapToItems | true | Magnetically snap edited edges to nearby item edges and the now-line. |
| snapDistance | 6 | Magnetic snap radius in px. |
Selection & interaction
| Option | Default | What it does |
| --- | --- | --- |
| tooltipDelay | 300 | Hover delay (ms) before the title tooltip shows. |
| verticalScrollbar | true | Show the draggable scrollbar on the left of the body (auto-hides when content fits). |
| scrollbarWidth | 12 | Scrollbar width in px. |
| keyboard | true | Enable desktop keyboard navigation + the focus ring (see below). |
| fullscreenButton | true | Show a fullscreen toggle button in the zoom-control overlay. |
| groupTable | null | Optional sortable/searchable metadata table rendered to the left of group labels. |
Call timeline.setFilter((rawItem, item) => boolean) to filter items across
stacking, drawing, hit-testing, and the minimap. Pass null to clear. Throwing
predicates fail open per item and emit a filter event with { active, visible }.
Appearance
| Option | Default | What it does |
| --- | --- | --- |
| theme | 'light' | 'light', 'dark', or a partial object merged over the light preset. |
| hour24 | true | Show axis/probe time-of-day on a 24-hour clock (13:00) instead of 12-hour with AM/PM. |
| fontFamily | system sans | CSS font-family for all canvas text. |
| fontSize | 12 | Base font size in px (axis/probe use one px smaller). |
| uniformItemColor | false | Draw 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. |
| accentBorderHeight | 3 | Height of the bottom accent strip in px. |
| itemStyles | {} | Named canvas item style definitions. Items can reference them with styleType / itemStyle, or definitions can include a selector over item data/properties. |
| styleBuilderButton | false | Show a toolbar button that opens the item style builder dialog. You can also call openItemStyleBuilder() directly. |
| itemDebugTooltip | false | Show raw + normalized item JSON in the hover tooltip, useful for building style selectors. |
Icons
| Option | Default | What it does |
| --- | --- | --- |
| icons | null | Registry of named icons: { name: { glyph, color } } or { name: { src } }. |
| iconAtlas | null | Sprite sheet: { src \| image, icons: { name: {x,y,w,h} } }. |
| iconSize | 14 | Icon draw size in px. |
| iconGap | 3 | Gap between leading icons in px. |
Overlays
| Option | Default | What it does |
| --- | --- | --- |
| timeProbe | false | Vertical line following the mouse; the time chip shows on the minimap (or the bottom axis if there's no minimap). |
| minimap | false | Overview minimap with drag-to-zoom. |
| minimapHeight | 48 | Minimap height in px. |
| topAxis | false | Also draw the time axis above the body (in addition to below). |
| followInterval | 1000 | Follow-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)
styleType, itemStyle, // named canvas style definition
properties, // string-like selector properties
// 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'
}Item style builder
Canvas cannot apply arbitrary CSS classes per item. Instead, define reusable canvas-native item styles and apply them by id or selector:
const timeline = new CanvasTimeline('#timeline', items, {
itemStyles: [{
id: 'critical',
selector: { field: 'severity', operator: 'equals', value: 'critical' },
style: {
backgroundGradient: ['#dc2626', '#7f1d1d'],
color: '#fff',
accentBorderColor: '#facc15',
accentBorderWidth: 5,
bottomAccentColor: '#fee2e2',
bottomAccentHeight: 3,
borderRadius: 6,
paddingX: 12,
backgroundGradientType: 'repeating', // optional: 'linear' (default) or 'repeating'
hoverBackgroundColor: '#ef4444',
selectedBackgroundColor: '#f97316',
icons: ['alert']
}
}]
});
timeline.openItemStyleBuilder();Selectors read from item.properties first, then from top-level item fields.
Use fields like properties.status for explicit property-map matching, or set
styleType: 'critical' on an item to apply a style directly.
When multiple selectors match, the first style in itemStyles wins. The style
builder includes a small manager where styles can be reordered by dragging; that
order is the selector precedence.
Group fields
{
id, content,
order, // sort key
nestedGroups, // [childId, ...] to nest groups (any depth — subgroups
// can hold subgroups; collapse hides the whole subtree)
showNested, // start expanded (default true)
visible, // hide the group
backgroundColor, color,
accentColor, // highlight tint
}Group metadata table
Use groupTable to add DOM columns that stay aligned with canvas group bands:
new CanvasTimeline('#timeline', items, {
groups,
groupTable: {
search: true,
columns: [
{ key: 'owner', label: 'Owner', width: 120, sortable: true },
{ key: 'status', label: 'Status', width: 90, sortable: true },
{ key: 'count', label: 'Items', width: 70, align: 'right', sortable: true }
]
}
});Column values are read from each raw group object. While the table is shown it
replaces the group-name label panel (add a content column to display
group names; pass showGroupLabels: true to keep the panel too).
Search filters visible groups by column values and group content. Each column
also gets its own clearable plain-text filter input (disable the row with
columnFilters: false); active column filters AND together.
Sorting is multi-column: a header click cycles that column ascending → descending → unsorted, and clicking additional columns subsorts in click order (the header shows ▲/▼ plus the sort priority). Sorting is intended for flat group lists.
Events
Subscribe with timeline.on(name, cb) / unsubscribe with off.
| Event | Payload |
| --- | --- |
| 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) |
| itemStyleChange | { style } (a style was saved through upsertItemStyle or the builder) |
| 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 ·
setItemStyles · getItemStyles · upsertItemStyle ·
openItemStyleBuilder · destroy. See
src/canvas-timeline.d.ts for full signatures.
Keyboard (when keyboard is on)
Click the timeline to focus it, then:
- ← / → — move the selection to the previous/next item within the same group (stays on one row band; pans when nothing is selected).
- ↑ / ↓ — move to the nearest-in-time item in the adjacent group.
- Shift + ← / → — nudge the selected item(s) in time.
- + / − — zoom in/out. F — fit. Home / End — first/last item.
- Esc — clear selection. Enter — activate (emits
doubleClick).
How this fork diverges from vis-timeline
These are the behaviors most likely to surprise you when migrating. Worked
examples are in EXAMPLES.html.
- No HTML templates.
template/groupTemplateare not supported — the canvas draws plain text + shapes. Convert template logic into data-driven fields (backgroundColor,pattern,icons, …). className/ CSS classes do nothing. Style via data fields instead.- Groups go in options, not the third constructor argument.
- You persist edits via
itemMove/itemResize. - Item types:
box,range, and the newblock.pointandbackgroundare not implemented. - Plain-text tooltips only (the
titlefield). For rich content, render your own overlay ondoubleClick/contextmenu.
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, viewport/selection/follow/
theme public API, events, low-level draw/measure helpers
mixin.js applyMixins(target, ...sources)
timeline/
groups.js group loading, nesting, collapse, highlights
data.js item loading/normalizing + vis-data DataSet sync
setup.js DOM construction, canvas sizing, event wiring
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 declarationsRun the tests with npm test (layout/unit assertions + a headless render
smoke test). npm run test:visual runs the Playwright visual-regression suite
(pixel comparison of rendered scenarios; see
test/visual/README.md).
