npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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:

  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

| 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/groupTemplate are 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 new block. point and background are not implemented.
  • Plain-text tooltips only (the title field). For rich content, render your own overlay on doubleClick/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 declarations

Run 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).