@irricrops/carpincho
v0.1.11
Published
Extensible hourly Gantt chart library built with Konva.js
Maintainers
Readme
Carpincho
Extensible hourly Gantt chart library built with Konva.js.
Features
- Hour-based timeline (zoom from 1h to 15 days per viewport)
- Hierarchical rows with collapsible groups
- Draggable, resizable tasks (edit mode)
- Markers: circle with icon/emoji/SVG, MDI icon path, deadline style (dashed line + triangle + label)
- Pan with middle-mouse drag, Shift+wheel or left-drag on background
- Zoom with Ctrl+wheel, anchored to cursor position
- Light/Dark themes
- Context menus: header, sidebar, grid, task
- Customizable tooltips (task, row, header day)
- Custom column cell renderers via Konva nodes
- Custom label prefix renderer per row
- Timezone-aware (IANA timezone + BCP 47 locale)
- TypeScript support
Installation
npm install @irricrops/carpincho konvaNote: konva is a required peer dependency (>= 9.0.0).
Install from git (without npm)
npm install github:irricrops/carpincho
# or specific branch
npm install github:irricrops/carpincho#mainUsage
<div id="gantt-container" style="width: 800px; height: 400px;"></div>
<script type="module">
import Carpincho from '@irricrops/carpincho';
const gantt = new Carpincho(document.getElementById('gantt-container'), {
start: new Date('2026-04-15T00:00:00'),
end: new Date('2026-04-22T00:00:00'),
timezone: 'America/Santiago',
locale: 'es-ES',
rows: [
{
id: 'row-1',
label: 'Irrigation Zone A',
columns: { status: 'Active', flow: '120 L/h' },
tasks: [
{
id: 't1',
start: new Date('2026-04-15T08:00'),
end: new Date('2026-04-15T12:00'),
label: 'Morning irrigation',
color: '#4a90d9'
}
]
}
],
rowHeight: 90,
sidebarWidth: 190,
columns: [
{ key: 'status', label: 'Status', width: 80 },
{ key: 'flow', label: 'Flow', width: 75 }
]
});
</script>API
Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| start | Date | required | Timeline start |
| end | Date | required | Timeline end |
| rows | RowOrGroup[] | [] | Initial rows/groups |
| rowHeight | number | 120 | Height per row in pixels |
| lanesPerRow | number | 3 | Task lanes per row (for overlapping tasks) |
| sidebarWidth | number | 200 | Sidebar name column width |
| sidebarLabel | string | 'Name' | Sidebar header label |
| columns | ColumnDef[] | [] | Extra sidebar columns |
| headerHeight | number | 72 | Total header height (3 × 24px) |
| headerBandH | number | 24 | Height of each header band |
| zoomStep | number | 1.15 | Zoom multiplier per step |
| zoomRange | { min?, max? } | { min: 3600, max: 1296000 } | ms/px limits |
| scrollbarSize | number | 12 | Scrollbar size in px |
| sidebarIndent | number | 16 | Indent per depth level |
| sidebarToggleW | number | 20 | Toggle area width in sidebar |
| markerRadius | number | 13 | Marker circle radius |
| markerPadding | number | 3 | Padding between markers on the same day |
| lanePadV | number | 4 | Vertical padding inside lane |
| lanePadH | number | 3 | Horizontal padding inside lane |
| labelPad | number | 6 | Text padding inside task bar |
| tooltipMaxWidth | number | 260 | Max tooltip width |
| tooltipOffset | number | 14 | Distance between cursor and tooltip |
| dblClickDelay | number | 300 | ms threshold for double-click detection |
| resizeDebounceMs | number | 50 | ResizeObserver debounce |
| timezone | string | 'UTC' | IANA timezone |
| locale | string | 'es-ES' | BCP 47 locale for date formatting |
| theme | 'light' \| 'dark' | 'light' | Color theme |
| editMode | boolean | false | Enable task dragging/resizing |
| readonly | boolean | false | Disable all interactions |
| snapMs | number | 300000 | Snap interval when dragging (5 min) |
| portalTarget | HTMLElement | document.body | Container for tooltips/context menus |
| labelPrefix | number | 0 | Reserved width before label in sidebar |
| debug | boolean | false | Log render performance to console |
| tooltipRenderer | (task, rowLabel, tz) => string | null | Custom task tooltip HTML |
| rowTooltipRenderer | (item, type) => string | null | Custom row tooltip HTML |
| headerDayTooltipRenderer | (tick, tz) => string | null | Custom header day tooltip HTML |
| labelPrefixRenderer | (item, type, x, y, w, h, gantt) => Konva.Node[] | null | Custom Konva nodes before sidebar label |
Data Types
Task
{
id: string
start: Date
end: Date
label?: string
color?: string
meta?: unknown // arbitrary user data, available in events
draggable?: boolean // overrides editMode per task
}Marker
{
id: string
type: 'marker'
date?: Date // point in time (used as start if start not provided)
start?: Date
label?: string
color?: string
meta?: unknown
icon?: string // SVG string, emoji, or single character
iconPath?: string // MDI path data (rendered via Konva.Path, viewBox 24)
iconColor?: string
markerLabel?: string // short text inside the circle (replaces icon)
markerStyle?: 'deadline' // dashed vertical line + ▼ triangle + multiline label
lines?: string[] // multiline label for deadline markers
}Row
{
id: string
label?: string
columns?: Record<string, unknown>
tasks?: (Task | Marker)[]
height?: number // custom row height override
dividerBefore?: boolean // thick separator line above this row
lanesPerRow?: number // override global lanesPerRow for this row
}Group
{
id: string
label?: string
columns?: Record<string, unknown>
tasks?: (Task | Marker)[] // rendered in the group header row
children: Row[]
collapsed?: boolean
height?: number
dividerBefore?: boolean
lanesPerRow?: number
}ColumnDef
{
key: string
label: string
width: number
colorFn?: (value: unknown) => string
renderer?: (item, type, x, y, w, h, gantt) => Konva.Node[]
}MenuItem
{ label: string | ((payload) => string), action: (payload) => void, danger?: boolean, disabled?: boolean, visible?: (payload) => boolean }
{ separator: true }
{ subheader: string | ((payload) => string) }Methods
Data:
gantt.setRows(rows) // replace all rows
gantt.addRow(row) // append a row or group
gantt.removeRow(id) // remove by id (recursive)
gantt.toggleGroup(id) // collapse/expand group
gantt.addTask(itemId, task) // add task to a row
gantt.updateTask(itemId, taskId, patch) // patch task properties
gantt.removeTask(itemId, taskId) // remove task from a rowNavigation:
gantt.scrollTo(date) // center the date in the viewport
gantt.zoom(dir) // zoom in (+1) or out (-1)
gantt.extendRange('left' | 'right', ms) // extend the visible rangeAppearance:
gantt.setColumns(cols) // replace sidebar columns
gantt.setTheme('light' | 'dark') // switch theme
gantt.setMenuItems(key, items) // configure context menu itemsEdit mode:
gantt.setEditMode(enabled) // toggle task drag/resizeBatch updates:
gantt.startBatch() // suspend re-renders
gantt.endBatch() // flush a single re-renderEvents:
const off = gantt.on(event, fn) // subscribe; returns unsubscribe fnLifecycle:
gantt.destroy() // cleanup: stage, listeners, timers, DOMEvents
| Event | Payload | Description |
|-------|---------|-------------|
| data:changed | — | Rows or tasks were modified |
| zoom:changed | { msPerPx } | Zoom level changed |
| scroll:horizontal | { visibleStart, visibleEnd } | Horizontal pan |
| range:extended | { visibleStart, visibleEnd } | Range extended |
| render:complete | — | Render cycle completed |
| editmode:changed | { editMode } | Edit mode toggled |
| task:click | { item, task } | Click on task/marker |
| task:dblclick | { item, task, clientX, clientY } | Double-click on task |
| task:beforemove | { itemId, taskId, start, end, cancel() } | Before drag confirmed — call cancel() to abort |
| task:moved | { itemId, taskId, start, end } | Drag completed |
| gantt:dblclick | { date, rowItem, rowType, clientX, clientY } | Double-click on empty grid area |
| grid:click | { date, rowItem, rowType, rowId, clientX, clientY } | Click on empty grid area |
| label:click | { item, type } | Click on sidebar label |
| contextmenu:header | { date, clientX, clientY } | Right-click on header |
| contextmenu:task | { task, item, date, clientX, clientY } | Right-click on task |
| contextmenu:grid | { item, rowType, rowId, date, clientX, clientY } | Right-click on grid |
| contextmenu:sidebar | { item, rowType, clientX, clientY } | Right-click on sidebar |
Context Menus
Configure menus for different zones:
gantt.setMenuItems('header', [
{ label: 'Zoom to day', action: () => {} },
{ label: 'Zoom to week', action: () => {} },
{ separator: true },
{ label: 'Add task', action: () => {}, disabled: true }
]);
gantt.setMenuItems('grid', [
{ label: 'Add task here', action: (p) => console.log(p.date) },
{ label: 'Add row', action: () => {} }
]);
gantt.setMenuItems('task', [
{ label: 'Edit task', action: (p) => {} },
{ label: 'Delete', action: (p) => {}, danger: true }
]);
gantt.setMenuItems('sidebar', [
{ label: 'Add row', action: () => {} },
{ label: 'Delete row', action: () => {}, danger: true }
]);Menu item options:
label: text or function returning textaction(payload): click handlerdanger: red stylingdisabled: greyed outseparator: divider linesubheader: non-interactive section headervisible(payload): function to conditionally show the item
Advanced: Exported Classes
import { EventBus, TimelineModel, RowManager } from '@irricrops/carpincho';| Class | Description |
|-------|-------------|
| EventBus | Pub/sub (Map of event → Set of listeners) |
| TimelineModel | Zoom, pan, ticks, dateToX() / xToDate() |
| RowManager | Row/group tree, flat layout computation |
Interactions Reference
| Interaction | Behavior |
|-------------|----------|
| Mouse wheel | Vertical scroll |
| Shift + wheel | Horizontal pan |
| Ctrl/Meta + wheel | Zoom anchored to cursor |
| Left drag (background) | Horizontal + vertical pan |
| Left drag (task, editMode) | Move with snap + task:beforemove (cancelable) |
| Right click | Context menu (header / task / grid / sidebar) |
| Double click (background) | gantt:dblclick event |
| Double click (task) | task:dblclick event |
| Click sidebar label | label:click event |
| Click group toggle | Collapse/expand group |
Development
npm install
npm run dev # Start demo server
npm run build # Build for productionLicense
MIT
