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

@taskctrl/canvas-timeline

v0.3.0

Published

High-performance canvas-based timeline component for React

Readme

@taskctrl/canvas-timeline

High-performance canvas-based timeline component for React. Renders 1000+ groups and 5000+ items at 60fps using a hybrid architecture: three stacked canvas layers for grid, items, and overlays, with DOM for headers, sidebar, and interactive elements.

Installation

yarn add @taskctrl/canvas-timeline

Peer dependencies: react, react-dom, dayjs

Basic Usage

import {
  CanvasTimeline,
  TodayMarker,
  DateHeader,
  TimelineHeaders,
  SidebarHeader,
} from '@taskctrl/canvas-timeline'
import type { Group, Item, CanvasItemRenderer } from '@taskctrl/canvas-timeline'

const groups: Group[] = [
  { id: 1, title: 'Group A' },
  { id: 2, title: 'Group B' },
]

const items: Item[] = [
  { id: 1, group: 1, start_time: Date.now(), end_time: Date.now() + 86400000 },
  { id: 2, group: 2, start_time: Date.now(), end_time: Date.now() + 172800000 },
]

const itemRenderer: CanvasItemRenderer = (ctx, item, bounds, state, h) => {
  ctx.fillStyle = state.selected ? '#3B82F6' : '#93C5FD'
  h.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, 3)
  ctx.fillStyle = '#1F2937'
  h.fillText(item.title ?? '', bounds.x + 6, bounds.y + bounds.height / 2, bounds.width - 12)
}

function MyTimeline() {
  return (
    <CanvasTimeline
      groups={groups}
      items={items}
      defaultTimeStart={Date.now() - 7 * 86400000}
      defaultTimeEnd={Date.now() + 21 * 86400000}
      sidebarWidth={200}
      lineHeight={40}
      itemHeightRatio={0.8}
      stackItems={true}
      canMove={true}
      canResize={false}
      canChangeGroup={false}
      dragSnap={86400000}
      minZoom={86400000}
      maxZoom={365 * 86400000}
      itemRenderer={itemRenderer}
      sidebarGroupRenderer={(group) => <div>{group.title}</div>}
      onItemClick={(id) => console.log('clicked', id)}
      onTimeChange={(start, end) => console.log('time changed', start, end)}
    >
      <TodayMarker color="#FD7171" width={2} label="Today" />
      <TimelineHeaders>
        <SidebarHeader width={200}>
          {({ getRootProps }) => <div {...getRootProps()}>Groups</div>}
        </SidebarHeader>
        <DateHeader unit="year" height={28} />
        <DateHeader unit="month" height={28} />
        <DateHeader unit="week" height={28} />
        <DateHeader unit="day" height={28} />
      </TimelineHeaders>
    </CanvasTimeline>
  )
}

Architecture

Three stacked canvas layers with independent redraw cycles:

| Layer | Content | Redraws on | |-------|---------|------------| | Grid (z:0) | Row backgrounds, grid lines, day/weekend shading | View change, theme change | | Items (z:1) | Items via custom renderer, dependency arrows | View change, data change, hover | | Overlay (z:2) | Cursor line, markers, drag ghost | Cursor move, drag |

DOM elements (headers, sidebar) sit outside the canvas stack. The sidebar is virtualized for large group counts.

API Reference

<CanvasTimeline>

| Prop | Type | Description | |------|------|-------------| | groups | Group[] | Array of group objects | | items | Item[] | Array of item objects | | defaultTimeStart | number | Initial visible start time (ms timestamp) | | defaultTimeEnd | number | Initial visible end time (ms timestamp) | | visibleTimeStart | number? | Controlled visible start time | | visibleTimeEnd | number? | Controlled visible end time | | sidebarWidth | number | Sidebar width in pixels | | lineHeight | number | Row height in pixels | | itemHeightRatio | number | Item height as ratio of lineHeight (0-1) | | stackItems | boolean | Stack overlapping items vertically | | buffer | number? | Buffer multiplier (default: 3) | | canMove | boolean | Enable item dragging | | canResize | boolean | Enable item resizing | | canChangeGroup | boolean | Enable moving items between groups | | dragSnap | number | Snap interval for dragging (ms) | | minZoom | number | Minimum visible duration (ms) | | maxZoom | number | Maximum visible duration (ms) | | theme | Partial<TimelineTheme>? | Theme overrides | | dayStyle | (date: Date) => DayStyle \| null | Per-day column styling (holidays, etc.) | | rowStyle | (group: Group) => RowStyle \| null | Per-row background styling | | showCursorLine | boolean? | Show vertical cursor line on hover | | itemRenderer | CanvasItemRenderer | Canvas render function for items | | groupRenderer | CanvasItemRenderer? | Canvas render function for group-level items | | sidebarGroupRenderer | (group: Group) => ReactNode | Sidebar row renderer | | dependencies | Dependency[]? | Dependency arrows between items | | selected | number[]? | Array of selected item IDs | | onItemClick | (id, e) => void | Item click handler | | onItemDoubleClick | (id, e) => void | Item double-click handler | | onItemContextMenu | (id, e) => void | Item right-click handler | | onItemMove | (id, newStartTime) => void | Item drag-move handler | | onItemHover | (id \| null, e) => void | Item hover handler | | onCanvasDoubleClick | (groupId, time) => void | Empty canvas double-click | | onCanvasContextMenu | (groupId, time, e) => void | Empty canvas right-click | | onTimeChange | (start, end) => void | Called on scroll/pan | | onZoom | (start, end) => void | Called on zoom |

Types

interface Group {
  id: number | string
  title: string
  type?: string
  [key: string]: unknown
}

interface Item {
  id: number
  group: number | string
  start_time: number  // ms timestamp
  end_time: number    // ms timestamp
  type?: string
  [key: string]: unknown
}

interface ItemBounds {
  x: number; y: number; width: number; height: number
}

interface ItemState {
  selected: boolean
  hovered: boolean
  dragging: boolean
  filtered: boolean
}

Item Renderer

The itemRenderer function receives the canvas context and draw helpers:

const renderer: CanvasItemRenderer = (ctx, item, bounds, state, h) => {
  // h.roundRect  - filled rounded rectangle
  // h.fillText   - text with auto-truncation
  // h.gradient   - 50/50 linear gradient
  // h.leftBar    - colored left edge bar
  // h.icon       - vector icon ('check', 'danger-red', 'danger-yellow')
  // h.badge      - pill-shaped badge with text
}

Header Components

<DateHeader> - Auto-hiding date interval header.

| Prop | Type | Description | |------|------|-------------| | unit | 'year' \| 'month' \| 'week' \| 'day' \| 'hour' | Time unit | | height | number? | Header height (default: 28) | | className | string? | CSS class for cells | | labelFormat | string \| ((start, end, unit) => string)? | Custom label format | | minCellWidth | number? | Min cell width before auto-hide (set 0 to disable) |

DateHeaders automatically hide when zoomed out too far for their unit to be meaningful.

<TodayMarker> - Vertical line at current time.

| Prop | Type | Default | |------|------|---------| | color | string? | '#FD7171' | | width | number? | 6 | | label | string? | - |

<CustomMarker> - Vertical line at a specific date.

| Prop | Type | Default | |------|------|---------| | date | number | required | | color | string? | '#3B82F6' | | width | number? | 4 | | label | string? | - |

Theming

import { DEFAULT_THEME } from '@taskctrl/canvas-timeline'

<CanvasTimeline
  theme={{
    grid: { line: '#E0E0E0', rowAlt: '#FAFAFA', weekend: 'rgba(0,0,0,0.02)' },
    marker: { today: '#FF0000', cursor: '#0066CC' },
  }}
/>

Interactions

| Input | Action | |-------|--------| | Trackpad pinch / Ctrl+wheel | Zoom (cursor-anchored) | | Trackpad two-finger horizontal | Pan timeline | | Shift+wheel | Horizontal scroll | | Wheel (vertical) | Vertical scroll | | Click item | onItemClick | | Double-click item | onItemDoubleClick | | Right-click item | onItemContextMenu | | Drag item (4px threshold) | Move item, onItemMove on drop | | Double-click canvas | onCanvasDoubleClick |

Performance

  • Spatial indexing via interval tree for O(log n + k) item queries
  • Sweep-line stacking algorithm O(n log n)
  • Vertical culling: only visible rows are drawn
  • Adaptive grid: day/week/month lines based on zoom level
  • Day background batching: consecutive same-style days merged into single fill
  • Stable event handlers: wheel listener never detaches during interaction
  • useLayoutEffect drawing: no flicker between state update and paint

License

MIT