{"_id":"@abrhak/canvas-timeline","name":"@abrhak/canvas-timeline","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@abrhak/canvas-timeline","version":"0.1.0","description":"High-performance canvas-based timeline component for React","scripts":{"build":"rimraf ./dist && tsc && vite build","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"eslint --ext .ts --ext .tsx ./src"},"main":"./dist/canvas-timeline.cjs.js","module":"./dist/canvas-timeline.es.js","sideEffects":false,"exports":{".":{"types":["./dist/index.d.ts"],"import":"./dist/canvas-timeline.es.js","require":"./dist/canvas-timeline.cjs.js"}},"types":"./dist/index.d.ts","author":{"name":"TaskCtrl AS"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/TaskCtrl-As/canvas-timeline.git"},"peerDependencies":{"dayjs":"^1.11.10","react":"^18 || ^19","react-dom":"^18 || ^19"},"devDependencies":{"@rollup/plugin-typescript":"^12.1.0","@testing-library/dom":"^10.4.0","@testing-library/jest-dom":"^6.5.0","@testing-library/react":"^16.0.1","@types/react":"^18.2.41","@types/react-dom":"^18.2.17","@typescript-eslint/eslint-plugin":"^8.8.1","@typescript-eslint/parser":"^8.8.1","@vitejs/plugin-react-swc":"^3.7.1","@vitest/coverage-v8":"^3.2.4","dayjs":"^1.11.20","eslint":"^8.57.1","jsdom":"^25.0.1","prettier":"^3.1.0","react":"^19.2.5","react-dom":"^19.2.5","rimraf":"^6.0.1","rollup-plugin-typescript-paths":"^1.5.0","tslib":"^2.8.1","typescript":"^5.2.2","vite":"^5.4.11","vite-plugin-dts":"^4.3.0","vitest":"^3.0.0"},"_id":"@abrhak/canvas-timeline@0.1.0","gitHead":"7bcb06bc4e6fb15685eeb8054d23b7311d88b931","bugs":{"url":"https://github.com/TaskCtrl-As/canvas-timeline/issues"},"homepage":"https://github.com/TaskCtrl-As/canvas-timeline#readme","_nodeVersion":"22.21.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-HD4o5Ie/jCOwCJYnDM9A0JWWa0y3NeVqtICfDUK9JzZBEgaYnlkfpQqXgcT7R6EvvP4voExzyxY+OAetVAHpRQ==","shasum":"956ae36f7a3af2cc812a18a0a3d4fc6877ba5ff1","tarball":"https://registry.npmjs.org/@abrhak/canvas-timeline/-/canvas-timeline-0.1.0.tgz","fileCount":28,"unpackedSize":329833,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCiO2e02QgtlfsnLNKQ6MYfbGNjtzmm7ZD46U6Aut7VEAIgdk1x8Gi443+uAmEkpGbHeLLzl0gceAwpitKCH/NvqD0="}]},"_npmUser":{"name":"abrhak","email":"abrha@taskctrl.no"},"directories":{},"maintainers":[{"name":"abrhak","email":"abrha@taskctrl.no"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/canvas-timeline_0.1.0_1777769442823_0.13284112437511464"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-03T00:50:42.734Z","0.1.0":"2026-05-03T00:50:43.004Z","modified":"2026-05-03T00:50:43.658Z"},"maintainers":[{"name":"abrhak","email":"abrha@taskctrl.no"}],"description":"High-performance canvas-based timeline component for React","homepage":"https://github.com/TaskCtrl-As/canvas-timeline#readme","repository":{"type":"git","url":"git+https://github.com/TaskCtrl-As/canvas-timeline.git"},"author":{"name":"TaskCtrl AS"},"bugs":{"url":"https://github.com/TaskCtrl-As/canvas-timeline/issues"},"license":"MIT","readme":"# @taskctrl/canvas-timeline\n\nHigh-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.\n\n## Installation\n\n```bash\nyarn add @taskctrl/canvas-timeline\n```\n\nPeer dependencies: `react`, `react-dom`, `dayjs`\n\n## Basic Usage\n\n```tsx\nimport {\n  CanvasTimeline,\n  TodayMarker,\n  DateHeader,\n  TimelineHeaders,\n  SidebarHeader,\n} from '@taskctrl/canvas-timeline'\nimport type { Group, Item, CanvasItemRenderer } from '@taskctrl/canvas-timeline'\n\nconst groups: Group[] = [\n  { id: 1, title: 'Group A' },\n  { id: 2, title: 'Group B' },\n]\n\nconst items: Item[] = [\n  { id: 1, group: 1, start_time: Date.now(), end_time: Date.now() + 86400000 },\n  { id: 2, group: 2, start_time: Date.now(), end_time: Date.now() + 172800000 },\n]\n\nconst itemRenderer: CanvasItemRenderer = (ctx, item, bounds, state, h) => {\n  ctx.fillStyle = state.selected ? '#3B82F6' : '#93C5FD'\n  h.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, 3)\n  ctx.fillStyle = '#1F2937'\n  h.fillText(item.title ?? '', bounds.x + 6, bounds.y + bounds.height / 2, bounds.width - 12)\n}\n\nfunction MyTimeline() {\n  return (\n    <CanvasTimeline\n      groups={groups}\n      items={items}\n      defaultTimeStart={Date.now() - 7 * 86400000}\n      defaultTimeEnd={Date.now() + 21 * 86400000}\n      sidebarWidth={200}\n      lineHeight={40}\n      itemHeightRatio={0.8}\n      stackItems={true}\n      canMove={true}\n      canResize={false}\n      canChangeGroup={false}\n      dragSnap={86400000}\n      minZoom={86400000}\n      maxZoom={365 * 86400000}\n      itemRenderer={itemRenderer}\n      sidebarGroupRenderer={(group) => <div>{group.title}</div>}\n      onItemClick={(id) => console.log('clicked', id)}\n      onTimeChange={(start, end) => console.log('time changed', start, end)}\n    >\n      <TodayMarker color=\"#FD7171\" width={2} label=\"Today\" />\n      <TimelineHeaders>\n        <SidebarHeader width={200}>\n          {({ getRootProps }) => <div {...getRootProps()}>Groups</div>}\n        </SidebarHeader>\n        <DateHeader unit=\"year\" height={28} />\n        <DateHeader unit=\"month\" height={28} />\n        <DateHeader unit=\"week\" height={28} />\n        <DateHeader unit=\"day\" height={28} />\n      </TimelineHeaders>\n    </CanvasTimeline>\n  )\n}\n```\n\n## Architecture\n\nThree stacked canvas layers with independent redraw cycles:\n\n| Layer | Content | Redraws on |\n|-------|---------|------------|\n| Grid (z:0) | Row backgrounds, grid lines, day/weekend shading | View change, theme change |\n| Items (z:1) | Items via custom renderer, dependency arrows | View change, data change, hover |\n| Overlay (z:2) | Cursor line, markers, drag ghost | Cursor move, drag |\n\nDOM elements (headers, sidebar) sit outside the canvas stack. The sidebar is virtualized for large group counts.\n\n## API Reference\n\n### `<CanvasTimeline>`\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `groups` | `Group[]` | Array of group objects |\n| `items` | `Item[]` | Array of item objects |\n| `defaultTimeStart` | `number` | Initial visible start time (ms timestamp) |\n| `defaultTimeEnd` | `number` | Initial visible end time (ms timestamp) |\n| `visibleTimeStart` | `number?` | Controlled visible start time |\n| `visibleTimeEnd` | `number?` | Controlled visible end time |\n| `sidebarWidth` | `number` | Sidebar width in pixels |\n| `lineHeight` | `number` | Row height in pixels |\n| `itemHeightRatio` | `number` | Item height as ratio of lineHeight (0-1) |\n| `stackItems` | `boolean` | Stack overlapping items vertically |\n| `buffer` | `number?` | Buffer multiplier (default: 3) |\n| `canMove` | `boolean` | Enable item dragging |\n| `canResize` | `boolean` | Enable item resizing |\n| `canChangeGroup` | `boolean` | Enable moving items between groups |\n| `dragSnap` | `number` | Snap interval for dragging (ms) |\n| `minZoom` | `number` | Minimum visible duration (ms) |\n| `maxZoom` | `number` | Maximum visible duration (ms) |\n| `theme` | `Partial<TimelineTheme>?` | Theme overrides |\n| `dayStyle` | `(date: Date) => DayStyle \\| null` | Per-day column styling (holidays, etc.) |\n| `rowStyle` | `(group: Group) => RowStyle \\| null` | Per-row background styling |\n| `showCursorLine` | `boolean?` | Show vertical cursor line on hover |\n| `itemRenderer` | `CanvasItemRenderer` | Canvas render function for items |\n| `groupRenderer` | `CanvasItemRenderer?` | Canvas render function for group-level items |\n| `sidebarGroupRenderer` | `(group: Group) => ReactNode` | Sidebar row renderer |\n| `dependencies` | `Dependency[]?` | Dependency arrows between items |\n| `selected` | `number[]?` | Array of selected item IDs |\n| `onItemClick` | `(id, e) => void` | Item click handler |\n| `onItemDoubleClick` | `(id, e) => void` | Item double-click handler |\n| `onItemContextMenu` | `(id, e) => void` | Item right-click handler |\n| `onItemMove` | `(id, newStartTime) => void` | Item drag-move handler |\n| `onItemHover` | `(id \\| null, e) => void` | Item hover handler |\n| `onCanvasDoubleClick` | `(groupId, time) => void` | Empty canvas double-click |\n| `onCanvasContextMenu` | `(groupId, time, e) => void` | Empty canvas right-click |\n| `onTimeChange` | `(start, end) => void` | Called on scroll/pan |\n| `onZoom` | `(start, end) => void` | Called on zoom |\n\n### Types\n\n```typescript\ninterface Group {\n  id: number | string\n  title: string\n  type?: string\n  [key: string]: unknown\n}\n\ninterface Item {\n  id: number\n  group: number | string\n  start_time: number  // ms timestamp\n  end_time: number    // ms timestamp\n  type?: string\n  [key: string]: unknown\n}\n\ninterface ItemBounds {\n  x: number; y: number; width: number; height: number\n}\n\ninterface ItemState {\n  selected: boolean\n  hovered: boolean\n  dragging: boolean\n  filtered: boolean\n}\n```\n\n### Item Renderer\n\nThe `itemRenderer` function receives the canvas context and draw helpers:\n\n```typescript\nconst renderer: CanvasItemRenderer = (ctx, item, bounds, state, h) => {\n  // h.roundRect  - filled rounded rectangle\n  // h.fillText   - text with auto-truncation\n  // h.gradient   - 50/50 linear gradient\n  // h.leftBar    - colored left edge bar\n  // h.icon       - vector icon ('check', 'danger-red', 'danger-yellow')\n  // h.badge      - pill-shaped badge with text\n}\n```\n\n### Header Components\n\n**`<DateHeader>`** - Auto-hiding date interval header.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `unit` | `'year' \\| 'month' \\| 'week' \\| 'day' \\| 'hour'` | Time unit |\n| `height` | `number?` | Header height (default: 28) |\n| `className` | `string?` | CSS class for cells |\n| `labelFormat` | `string \\| ((start, end, unit) => string)?` | Custom label format |\n| `minCellWidth` | `number?` | Min cell width before auto-hide (set 0 to disable) |\n\nDateHeaders automatically hide when zoomed out too far for their unit to be meaningful.\n\n**`<TodayMarker>`** - Vertical line at current time.\n\n| Prop | Type | Default |\n|------|------|---------|\n| `color` | `string?` | `'#FD7171'` |\n| `width` | `number?` | `6` |\n| `label` | `string?` | - |\n\n**`<CustomMarker>`** - Vertical line at a specific date.\n\n| Prop | Type | Default |\n|------|------|---------|\n| `date` | `number` | required |\n| `color` | `string?` | `'#3B82F6'` |\n| `width` | `number?` | `4` |\n| `label` | `string?` | - |\n\n### Theming\n\n```typescript\nimport { DEFAULT_THEME } from '@taskctrl/canvas-timeline'\n\n<CanvasTimeline\n  theme={{\n    grid: { line: '#E0E0E0', rowAlt: '#FAFAFA', weekend: 'rgba(0,0,0,0.02)' },\n    marker: { today: '#FF0000', cursor: '#0066CC' },\n  }}\n/>\n```\n\n## Interactions\n\n| Input | Action |\n|-------|--------|\n| Trackpad pinch / Ctrl+wheel | Zoom (cursor-anchored) |\n| Trackpad two-finger horizontal | Pan timeline |\n| Shift+wheel | Horizontal scroll |\n| Wheel (vertical) | Vertical scroll |\n| Click item | `onItemClick` |\n| Double-click item | `onItemDoubleClick` |\n| Right-click item | `onItemContextMenu` |\n| Drag item (4px threshold) | Move item, `onItemMove` on drop |\n| Double-click canvas | `onCanvasDoubleClick` |\n\n## Performance\n\n- Spatial indexing via interval tree for O(log n + k) item queries\n- Sweep-line stacking algorithm O(n log n)\n- Vertical culling: only visible rows are drawn\n- Adaptive grid: day/week/month lines based on zoom level\n- Day background batching: consecutive same-style days merged into single fill\n- Stable event handlers: wheel listener never detaches during interaction\n- `useLayoutEffect` drawing: no flicker between state update and paint\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-2becd3fb0c415afd10cb5c55f2ccfa0b"}