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

@gantt-chart/core

v0.1.11

Published

Framework-agnostic Gantt engine: layout, stacking, virtualization, selection, drag, state.

Readme

@gantt-chart/core

The Gantt engine: data normalization, the group tree, stacking, layout, virtualization, selection, drag, viewport and state. Nothing here touches the DOM, React or ECharts.

npm install @gantt-chart/core

Using it directly

import { GanttEngine } from '@gantt-chart/core';

const engine = new GanttEngine({
  tasks: [{ id: 't1', groupId: 'g1', start: 0, end: 86_400_000 }],
  groups: [{ id: 'g1', label: 'Build' }],
  size: { width: 900, height: 400 },
});

engine.viewport.fitTime();

const frame = engine.getVisible(); // only the bars that intersect the viewport
for (const item of frame.items) {
  // item.start/end/y/height are resolved, including any in-flight drag offset
}

The pipeline

data → rows → layout(+stacking) → virtualize, each stage memoized on its inputs. getRows(), getLayout() and getVisible() are cheap to call repeatedly — they return the cached result until something they depend on changes.

LayoutResult is deliberately array-shaped: rowY/rowHeight for binary search over rows, rankToTask + rowOffsets as a CSR index of displayed tasks in visual order (row, then start time), taskLane for stacking, and maxEndPrefix — a running maximum of end within each row — which is what lets the virtualizer stop a backwards scan early instead of walking a whole row.

Sub-engines

| accessor | what it owns | | --- | --- | | engine.viewport | time window, scroll, zoom, fit, scroll-into-view | | engine.selection | click semantics, ranges over visual order, marquee, keyboard focus | | engine.drag | gestures, mode derivation, snapping, change proposals | | engine.contextMenu | menu target, position, selection snapshot | | engine.overlays | extra render layers for plugins | | engine.store | immutable snapshot state, subscriptions | | engine.events | typed event bus (GanttEventMap) |

Editing model

Gestures never mutate task data. A drag writes a DragState into the store, the virtualizer applies that offset while building the frame, and on commit the engine emits TaskChange[] — each carrying its own previous snapshot. Accept them with engine.applyChanges(changes), or apply them yourself and re-render.

Because every change carries its own inverse, GanttHistory can offer undo/redo without ever copying the dataset:

const history = new GanttHistory({ limit: 200 });
engine.on('drag:end', ({ changes, cancelled }) => {
  if (!cancelled && changes.length) {
    engine.applyChanges(changes);
    history.push(changes);
  }
});

const undone = history.undo();
if (undone) engine.applyChanges(undone.changes);

Stacking

Overlap is resolved into lanes by LaneAllocator, in data space, so zoom never reshuffles a row. minGap widens the interval used for overlap tests, maxLanes caps a row, lane pins a task, and floating exempts one from overlap entirely. Milestones (start === end) are widened infinitesimally so two at the same instant do not share a lane.

Disabled rows

engine.setRowDisabled(groupId, true) switches a row off: its bars fade and its label mutes. toggleRowDisabled, setDisabledRows(ids) (off for exactly these, on for everything else) and enableAllRows() are the rest of the API, and group.disabled seeds the state the way group.collapsed does.

row:disable fires once per row that actually changed, in row order, whichever of those calls did it — a bulk enable reports each row it switched on rather than leaving a listener to diff the set. Setting a row to the state it already has says nothing, and neither does the group.disabled seed: that is the data speaking, not a change to report back to it.

What that costs the row is interaction.disabledRows, and it is the chart's decision rather than the row's:

| | 'block' (default) | 'interactive' | | --- | --- | --- | | row.disabled | true | true | | row.inert | true | false | | click, range, marquee, select-all/invert, keyboard focus | skipped | normal | | drag from it / drop onto it | refused | normal | | hover emphasis, cursor | withheld | offered | | selection, hover and gestures held when it goes inert | dropped | kept |

row.disabled is the state — style from it. row.inert is disabled and 'block' — gate from it, via isRowInert / isTaskRowInert, which is what every interaction engine here asks. 'interactive' is for charts where disabled is the consumer's own concept and only wants showing; the app then decides in its own handlers what may still be done to the row.

Either way it is an input rule, never a data lock: selection.set, applyChanges and every other explicit call still reach the row, and hitTest stays truthful about what is under a pixel — result.row.inert is what tells a caller the hit is out of bounds.

Because none of it changes geometry, disabling sits outside the layout inputs: the row model and everything downstream of it are kept, and only GanttRow.disabled / GanttRow.inert are re-stamped — switching the option at runtime included. Toggling a row in a 100 000-task chart therefore costs a walk of the row list, not a re-stack.

Hit testing

hitTest(point) answers what is under a pixel without scanning the dataset: binary search for the row, then for the last task starting before that time, then a short backwards scan bounded by maxEndPrefix. The same minItemWidth tolerance the renderer uses is applied, so a 1px bar is still clickable.

nearestRow(y) and getRow(groupId) are the two ways in without a pixel: one from a coordinate, the other from a group — the latter follows collapse, so a hidden group reports the ancestor row its tasks roll up onto.

Data notes

Bad input is tolerated and reported through warnings rather than thrown: duplicate ids, reversed ranges, non-finite times, missing parents and parent cycles all have defined behaviour. Tasks referencing an unknown groupId get an implicit group so nothing silently disappears.