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.8

Published

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

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.
  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: left to right it holds Group by (groupBy / onGroupByChange), Clear (onClear), search, zoom out/in, reset view, settings, 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.
  • 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.
  • Settings 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.
  • onEventUpdate / onEventDelete — opt into editing (time, title, description, metadata properties) and deletion of the selected event from the details sidebar. Both hand you the id of the event in your data plus (for updates) the changes; the component owns no data, so you apply them and pass the new data back in.
  • 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(…). | | 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 "Save timeline to workspace" to the Save menu; 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. | | 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. |

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).