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

@octostar/timeline-component

v0.1.11

Published

Reusable Octostar timeline — one JSON of events drives interchangeable horizontal, vertical, and force-directed graph views.

Downloads

2,785

Readme

@octostar/timeline-component

A reusable React timeline for communication data. Give it one JSON of events (who talked to whom, and when) and it renders three interchangeable views of the same data — no backend, no OpenSearch, no LLM:

  • Horizontal — a swimlane timeline, one lane per participant, with points, intervals, and connection lines between lanes.
  • Vertical — the same data as a top-to-bottom timeline.
  • Graph — a force-directed node-link view where each participant is a node and each exchange between two participants is an aggregated, weighted edge.

The three views are driven by a single in-memory model, so switching between them is instant and lossless.

Extracted from the Octostar CommLab app. Packaged as a standalone library and consumed by octostar-frontend, mirroring @octostar/map-component.

Versioning & Releases

Versioning is driven by GitVersion 6 (GitVersion.yml, GitHubFlow/v1). Every push to main (outside *.md/docs/**) computes the next semantic version, pushes the matching v<version> git tag, publishes the package to npm, and creates a GitHub release with generated notes. The default increment is patch; override from a commit/PR message with +semver: major|minor|patch|none.

The publish pipeline is dormant by default so early pushes don't reserve version tags. To arm it: (1) configure @octostar/timeline-component on npm with an OIDC trusted publisher pointing at this repo's .github/workflows/publish.yml (no NPM_TOKEN secret needed), then (2) set the repo variable NPM_PUBLISH_ENABLED=true (Settings → Secrets and variables → Actions → Variables). CI (typecheck + test + build) runs on every push/PR regardless.

Prerequisites

  • Node.js 20+ (see .nvmrc)
  • pnpm 9+
nvm use
pnpm install

Develop (standalone demo)

pnpm dev          # http://localhost:3200 — the demo in client/ with a sample dataset

The demo (client/index.html + client/src/{App,main}.tsx) mounts <Timeline> with a 24-message / 5-participant sample and exercises the view button and the imperative handle.

Build (library)

pnpm build:lib

Outputs to dist/:

  • dist/index.mjs — ES module
  • dist/index.cjs — CommonJS module
  • dist/types/index.d.ts — type declarations

The library ships no CSS — it uses antd defaults and inline styles, so the consuming app's antd theme applies directly.

Usage

import { Timeline, type TimelineData } from '@octostar/timeline-component';

const data: TimelineData = {
  // `tracks` (participants / lanes / graph nodes) is optional — it is
  // synthesized from the events if omitted. Lanes default to most-active-first
  // (an event counts for its own lane and for the lane it connects to); give
  // any track an explicit `order` to keep the provided ordering instead.
  tracks: [
    { id: '[email protected]', name: 'Alice' },
    { id: '[email protected]', name: 'Bob' },
  ],
  events: [
    { id: 'm1', from: '[email protected]', to: '[email protected]', time: '2024-03-04T09:12:00Z', title: 'Kickoff' },
  ],
};

<Timeline data={data} height={640} defaultView="horizontal" />;

Timestamps are ISO 8601 strings. Always include an explicit UTC offset (Z or +02:00): an offset-less datetime like 2024-03-04T09:12:00 is parsed as the viewer's local time, so the same JSON renders differently per user (normalization reports each such value as an info issue). Date-only strings (2024-03-04) parse as UTC midnight, which is at least viewer-independent.

Peer dependencies: react >=18, react-dom >=18, antd ^6, @ant-design/icons ^6.

Public API

  • <Timeline data={...} /> — the orchestrator with a single toolbar and an imperative handle (setView, zoom, select, export). The parent owns the data: there are no mutating methods on the handle — to add/update/remove events or tracks, pass a new data prop (single source of truth). The viewport frames the data once, when it first arrives; later data changes never move it (so streaming events in keeps the user's zoom) — call fitToData() on the handle to re-frame on demand.
  • One toolbar, no chrome around it — a single row that never wraps (it scrolls sideways when the pane is too narrow for it): left to right it holds New event (onEventAdd), Clear (onClear, confirmed first), search, zoom out/in, untangle, event labels (a button cycling off → auto → all), reset view, then the view button — it opens a picker that describes Horizontal, Vertical and Graph with a small visual example each, so every view is reachable from any other. The graph covers that toolbar, so it gets its own floating copy of the view button in the top-right corner — plus a time filter (seeded from the window the previous view was showing) that drops out-of-range events and the participants left with none. After the Load and Save menus, the last button is View options (the cog): it toggles a side panel, so every change shows on the timeline as you make it.
  • Search finds an event by title, text, participant, category or metadata, then selects it, centers the viewport on it and rings it with a temporary halo.
  • Lanes default to most-active-first order — hosted events plus connections pointing at the lane; ties keep first-seen order. Any explicit order on an input track keeps the provided ordering.
  • Coincident points fan out vertically (earliest at the lane center, alternating above/below) and the lane grows to fit, so simultaneous events stay individually visible; the fan relaxes automatically as zooming in separates them.
  • View options holds the lane grouping pair — Lanes · From / To (groupBy / groupByTo and their callbacks) — alongside track height and hidden tracks. Events with no value for the "From" field share one lane, named Events.
  • View options offers "Color connections by direction" (default off): the horizontal view's connection lines take a hue instead of arrowheads — orange flows toward a lower lane, indigo toward an upper lane, grey both ways.
  • Clicking a lane label focuses the track: its events open in the sidebar and every other lane fades; click again (or close the panel) to unfocus. In the graph, clicking a node opens the same track panel and clicking an edge opens the communications between its two endpoints — rows select the event, switching the sidebar to its Preview.
  • The selected event's sidebar is a Preview card (structured after the linkchart component's entity Preview: type-colored icon, label and type, two-column metadata table). onOpenRecord — opt into an Open record button on that card; it hands you the id of the event in your data, your own input event (the exact object from data.events), and its index in data.events, and what opening means (a record viewer, a tab) is yours; only entity-backed events get it. onCreateRecord — events with no linked record get a Save as record button instead (the map's dropped-point affordance); write the created ids back into the event's metadata to flip the button to "Open record". onEventDelete — opt into deleting the selected event from the sidebar; removing it is yours (e.g. events.filter((_, i) => i !== eventIndex)). Note: when your input events carry no id (or duplicate ids), the id these callbacks hand you was synthesized during normalization and matches nothing in your data — the event and index arguments always identify the source unambiguously. A record-backed event is edited in the record viewer, not the timeline; its delete button reads "Remove from timeline" (the record is kept). The Delete key removes the selected event too.
  • Events that live only in the timeline: onEventAdd — opt into a New event button in the toolbar and double-click on an empty spot of the horizontal view (prefilled with that time and lane). The form hands you a new input event with a fresh local-… id; append it to your data. Its lane is written where the current lane grouping reads it (the grouping's metadata field, or trackId without grouping). onEventAddAsRecord — adds Create as record instead to that form, handing you the same event to create a real record from. onEventUpdate — events with no linked record get an Edit button on their Preview card (title, start/end, lane, description, properties); you receive your own input event with the edits applied and every other field kept. onEventsDelete — the lane menu's Remove this lane's events, in one batch (falls back to onEventDelete per event). EventEditorModal and eventFromDraft are exported for hosts that offer New event before any <Timeline> is mounted (an empty timeline).
  • Saved timeline files ("Download timeline", onSaveToWorkspace) hold your own data as given — never the lane-grouped display copy, which rewrites ids and drops links and participants — and download as timeline-<date>.timeline (TIMELINE_FILE_EXTENSION, JSON inside). Import accepts .timeline and .json. Clear asks for confirmation.
  • Integration props for embedding hosts: selectedEventId (controlled selection), onEventClick, onTimeRangeChange (fired on every zoom/pan), and mergeLanesBy — merge lanes whose tracks share the same value of a custom Track field (e.g. mergeLanesBy="company"). Lanes are always identified by track id: one lane per entity is the base state (null), and merging never derives identity from display values, so homonymous entities with distinct ids never mix.
  • onAddToReport — opt into an Add to report button (left of the Load control). It captures the timeline itself at 3× — the time axis, the lanes and the lane labels, without the header or the toolbar — and hands you a TimelineSnapshot (dataUrl plus its display and pixel sizes). The component owns no reports: append the image yourself (in Octostar, through the shared "Add to report" flow). Reject with a message containing cancel to treat a dismissed report picker as a no-op. Available in the horizontal and vertical views.
  • getGraphEdges() — client-side derivation of aggregated, weighted edges.
  • normalizeTimelineData() — accepts both the full (trackId / connectToTrackId) and convenience (from / to / time) event forms. Returns { tracks, events, issues }; nothing is dropped silently — invalid timestamps, duplicate ids, self-connections etc. are reported in issues (and logged to the console once in dev builds).
  • buildTimelineData(events, groupBy?) — derive the lanes from the events themselves: split a flat event list into one lane per value of an event metadata field ({ axis: 'event', field: 'entity_type' }, the default) or of a linked entity's attribute ({ axis: 'link', field: 'company' }, see EventLink). Returns a ready-to-render TimelineData plus its own issues (GroupedTimelineData) — same no-silent-drops contract as normalizeTimelineData. Used internally when the groupBy props are set, and exported for hosts that group outside the component. A connection event's counterpart (to / connectToTrackId) is a participant id, not a lane id, so it is resolved to a lane by identity: whichever lane that participant's own events landed on. A → B grouped by person connects lane A to lane B; to: '[email protected]' grouped by metadata.person still connects to the Sara Greco lane. A counterpart that resolves to no lane (or to the source's own lane, or to several) leaves the event a plain point and says so in issues — no lane is ever invented to hold an edge.
  • groupingOptions(events) — the grouping dimensions discoverable from the events (their metadata keys, plus linked-entity fields when any event has links), as { value, label } options for a selector.
  • encodeGroupBy(groupBy) / decodeGroupBy(encoded) — serialize a GroupBy to/from its stable "axis::field" string form (e.g. for URL or saved-state persistence). decodeGroupBy falls back to DEFAULT_GROUP_BY on malformed input.
  • Connections with duration: a connection event may carry an endTime ("A called B for 12 minutes"). The horizontal view renders it as a translucent bridge spanning both lanes across the interval, on top of the usual connector + direction arrow at startTime.
  • Types: TimelineData, TimelineInputEvent, TimelineView, TimelineHandle, NormalizationIssue, …
  • timeline-data.schema.json — a JSON Schema of the canonical input format, shipped in the package (@octostar/timeline-component/timeline-data.schema.json). Designed for LLM constrained decoding: point a local model's grammar at it and every generated JSON is a valid TimelineData.

<Timeline> props

| Prop | Type | Default | Description | | --- | --- | --- | --- | | data | TimelineData | — (required) | The events (and optionally participants) to visualize. The parent owns the data; pass a new data to change it. | | view | TimelineView | — | Controlled active view ('horizontal' \| 'vertical' \| 'graph'). Omit to let the component manage it internally. | | defaultView | TimelineView | 'horizontal' | Initial view when uncontrolled. | | density | TimelineDensity | — | Controlled density of the horizontal view ('off' \| 'heatmap' \| 'auto'), rendered as a segmented control in the toolbar. A separate axis from view: orientation and density are independent, so switching to the graph and back keeps a heatmap. Only the horizontal view renders density — the control is disabled in the vertical view. | | defaultDensity | TimelineDensity | 'off' | Initial density when uncontrolled. | | onDensityChange | (density: TimelineDensity) => void | — | Fired when the density changes, from the built-in control or from getTimeline().setViewMode(…). | | expandOverlaps | boolean | — | Controlled de-overlap mode ("Expand overlapping events" in View options). When on, events that draw at the same position at the current zoom are spread out: coincident points fan vertically (their lane grows to fit) and connection endpoints ride separate from/to rows. The treatment relaxes automatically as zooming in separates the events. Horizontal view with density 'off' only. Omit to let the component manage it internally. | | defaultExpandOverlaps | boolean | false | Initial de-overlap mode when uncontrolled. | | onExpandOverlapsChange | (expandOverlaps: boolean) => void | — | Fired when the de-overlap mode changes from View options. | | height | number \| string | 600 | Height of the visualization area: a number is pixels, a CSS length ('100%', '100vh') is applied as given. Inside a parent with a definite height, pass '100%' and let the component follow it — do not measure the parent and hand the number back, or the component keeps whatever the parent measured before layout settled. | | onViewChange | (view: TimelineView) => void | — | Fired when the active view changes. | | selectedEventId | string \| null | — | Controlled selection: the id of the event to select (null for none). Omit for internal selection; pair with onEventSelect to stay in sync. | | onEventSelect | (event: TimelineEvent \| null) => void | — | Fired when an event is selected (or deselected, with null). | | onEventClick | (event: TimelineEvent, nativeEvent: MouseEvent) => void | — | Fired when the user clicks an event on the canvas. | | onTimeRangeChange | (range: [Date, Date]) => void | — | Fired whenever the visible time window changes (zoom, pan, programmatic). | | mergeLanesBy | string \| null | — | Merge lanes whose tracks share the same value of this custom Track field (e.g. 'company'). Controlled-only — nothing in the UI changes it. Lanes are always identified by track id — null is the one-lane-per-entity base state, and merging never derives identity from display values, so homonymous entities with distinct ids never mix. | | groupBy | GroupBy | — | Controlled lane grouping. Providing any of groupBy / defaultGroupBy / onGroupByChange makes the component derive the lanes from each event's own metadata (or a linked entity's attributes, see EventLink) instead of using data.tracks. | | defaultGroupBy | GroupBy | { axis: 'event', field: 'entity_type' } | Initial grouping when groupBy is not controlled. | | onGroupByChange | (groupBy: GroupBy) => void | — | Fired when the grouping changes from the built-in control. | | onClear | () => void | — | When provided, a "Clear" button appears in the toolbar. The component owns no data, so clearing is the parent's job (typically emptying data). | | onSaveToWorkspace | (json: string) => void \| Promise<void> | — | Adds the workspace Save entry to the Save menu ("Save to workspace…", or "Save to " once saved); receives the exact JSON that "Download timeline" writes. Return a promise for async saves; reject with a message containing "cancel" to treat a dismissed dialog as a no-op. | | onSaveAsToWorkspace | (json: string) => void \| Promise<void> | — | Adds "Save a copy…" to the Save menu once savedFileName is set; same JSON as onSaveToWorkspace. | | savedFileName | string \| null | — | The workspace file the timeline is saved in. Unset: one "Save to workspace…" entry. Set: "Save to " plus "Save a copy…". | | onImport | (data: TimelineData) => void | — | Adds the Load control; receives the parsed { tracks, events } from a file, the workspace, or a drop. Feed it back through data. Required for any restore path to take effect. | | onLoadFromWorkspace | (entities?: unknown[]) => Promise<TimelineData \| null> \| TimelineData \| null | — | Adds "Load from workspace" to the Load menu and enables dragging workspace records onto the timeline. Called with no argument from the menu (show your own picker) or with the dragged records. Return null (or reject with "cancel") to cancel; the result is routed to onImport. | | onAddToReport | (snapshot: TimelineSnapshot) => void \| Promise<void> | — | Adds an Add to report button; receives a high-resolution PNG of the timeline itself (time axis, lanes and lane labels — no header or toolbar) plus its display and pixel sizes. The component owns no reports, so appending the image is the parent's job. Reject with a message containing "cancel" to treat a dismissed picker as a no-op. | | enableDropImport | boolean | true | Whether dropping a timeline JSON (file, raw text, or workspace record) onto the timeline imports it. Set false when the host owns drag-and-drop over the timeline area; the Load menu keeps working. | | renderEventDetails | (event: TimelineEvent) => React.ReactNode | — | Custom renderer for the selected event's detail body. | | onEventAdd | (event: TimelineInputEvent) => void | — | Adds New event (toolbar, and double-click on empty canvas). Append the event it hands you to data; it is selected once it renders. | | onEventAddAsRecord | (event: TimelineInputEvent) => void | — | Adds Create as record instead to the New event form. | | onEventUpdate | (eventId: string, event: TimelineInputEvent, eventIndex: number) => void | — | Adds Edit to the Preview card of events with no linked record. Replace the event at eventIndex with the one handed to you. | | onEventDelete | (eventId: string, event: TimelineInputEvent, eventIndex: number) => void | — | Adds a delete button to the Preview card and enables the Delete key. | | onEventsDelete | (eventIds: string[], events: TimelineInputEvent[], eventIndexes: number[]) => void | — | Adds Remove this lane's events to the lane context menu (confirmed first). Falls back to onEventDelete, highest index first. |

Test

pnpm test         # vitest (jsdom) — getGraphEdges, normalizeTimelineData, buildTimelineData, …

Consuming it in octostar-frontend

Once the publish pipeline is armed (see Versioning & Releases), the frontend consumes the published @octostar/timeline-component npm package. Until then — or for local iteration — use a packed tarball:

pnpm build:lib && pnpm pack        # → octostar-timeline-component-<version>.tgz

apps/octostar/package.json then references it via file:../octostar-timeline-component/octostar-timeline-component-<version>.tgz, and apps/octostar/src/components/timeline/TimelineHost.tsx registers it as the launchable Timeline app (mirrors map-nt/MapNT.tsx).