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

@crosmos/graph

v0.1.0

Published

Official graph visualization package for Crosmos Memory

Readme

@crosmos/graph

Force-directed knowledge-graph rendering for React. Cluster-aware out of the box. Bring your own data — feed <ForceGraph> two arrays and you're done.

npm version npm downloads license

Install · Quickstart · Props · Theme · SSR

Crosmos knowledge graph — 500 nodes, 554 edges

Install

npm install @crosmos/graph react react-dom react-force-graph-2d

[!NOTE] react, react-dom, and react-force-graph-2d are peer dependencies — install them in the host app so you don't end up with duplicates.

Quickstart

import { ForceGraph, type BaseNode, type BaseEdge } from "@crosmos/graph";
import "@crosmos/graph/styles.css";

interface MyNode extends BaseNode { label: string; team: string; }
interface MyEdge extends BaseEdge { label: string; }

export function MyGraph({ nodes, edges }: { nodes: MyNode[]; edges: MyEdge[] }) {
  return (
    <ForceGraph<MyNode, MyEdge>
      nodes={nodes}
      edges={edges}
      getNodeLabel={(n) => n.label}
      getEdgeLabel={(e) => e.label}
      onNodeClick={(n) => console.log("clicked", n.team)}
    />
  );
}

The package only exposes generic shapes — BaseNode { id } and BaseEdge { id, source, target }. Carry your domain fields on the node/edge directly; accessor functions stay fully type-safe. Fetch with TanStack Query / SWR / RSC / anything — the package does no fetching.

Props

<ForceGraph<TNode, TEdge>> accepts:

| Prop | Type | Default | Description | | --------------------- | ----------------------------------------------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------- | | nodes (required) | TNode[] | — | Must extend BaseNode { id: string }. | | edges (required) | TEdge[] | — | Must extend BaseEdge { id: string; source: string; target: string }. | | getNodeLabel | (node: TNode) => string | n.label \|\| n.name \|\| n.id | Text rendered below each node. | | getNodeWeight | (node: TNode) => number | n.weight ?? 0 | Drives degree-based collision radius and clustering hub priority. | | getEdgeLabel | (edge: TEdge) => string | e.label ?? "" | Text rendered on the midpoint of the edge. Empty string hides it. | | onNodeClick | (node: TNode) => void | — | Fires after the camera centres on the node. | | onEdgeClick | (edge: TEdge) => void | — | Fires after the camera centres on the edge midpoint. | | onBackgroundClick | () => void | — | Fires on canvas background click. | | theme | Partial<GraphTheme> | DEFAULT_THEME | Deep-merged with defaults — see Theme. | | disableClustering | boolean | false | Skip Louvain detection and the cluster force; revert to single-gravity-well layout. | | showZoomLevel | false \| "top-right" \| "top-left" \| "bottom-right" \| "bottom-left" | false | Render a live zoom-percentage indicator at the given corner. | | className | string | "cg-root" | Replaces the container class entirely. | | aria-label | string | "Knowledge graph" | Container aria-label. | | emptyState | ReactNode | "No entities to display" | Rendered when nodes is empty and isLoading is falsy. | | loadingState | ReactNode | — | Rendered while the canvas lib is dynamic-imported, or when nodes is empty and isLoading is true. | | isLoading | boolean | — | Sets aria-busy on the container. | | ref | Ref<ForceGraphHandle> | — | Imperative handle — see below. |

Imperative handle

const ref = useRef<ForceGraphHandle>(null);
// ...
<ForceGraph ref={ref} {...} />

| Method | Signature | Description | | ----------------- | -------------------------------------------------------- | ------------------------------------------------------ | | zoom | (scale: number, durationMs?: number) => void | Animate to an absolute zoom level. | | zoomToFit | (durationMs?: number, paddingPx?: number) => void | Fit all nodes in view. | | centerAt | (x: number, y: number, durationMs?: number) => void | Pan to graph coordinates. | | pauseAnimation | () => void | Stop the simulation. | | resumeAnimation | () => void | Resume the simulation. | | refresh | () => void | Force a single canvas repaint. |

Theme

Pass any subset of GraphTheme to theme. Deep-merged with DEFAULT_THEME.

<ForceGraph
  theme={{
    node:    { color: "#0ea5e9", hoverColor: "#f97316" },
    link:    { defaultAlpha: 0.55, dimRgbTuple: "100,116,139" },
    cluster: { intraLinkDistance: 100, interLinkDistance: 320, strength: 0.22 },
    label:   { zoomGrowthCap: 2 },
  }}
  {...rest}
/>

theme.node

| Key | Type | Default | Description | | ----------- | -------- | ----------------------------- | ------------------------------------------ | | radius | number | 4 | Visible node radius in graph units. | | fontSize | number | 8 | Label font size at zoom 1×. | | labelGap | number | 3 | Vertical gap between node and label. | | labelColor| string | oklch(0.95 0 0) | Label text colour. | | color | string | #ffffff | Default fill. | | hoverColor| string | oklch(0.5544 0.1146 158.24) | Fill while hovered. |

theme.link

| Key | Type | Default | Description | | ------------------- | -------- | ---------------------------- | ------------------------------------------------- | | color | string | rgba(148,163,184,0.4) | Line colour at rest. | | defaultAlpha | number | 0.4 | Alpha at rest (used to derive the dimmed state). | | defaultWidth | number | 0.5 | Line width at rest. | | highlightedWidth | number | 1.5 | Line width while highlighted. | | curvatureSpacing | number | 0.25 | Curvature applied to parallel edges. | | dimRgbTuple | string | "148,163,184" | RGB tuple used to build the dimmed rgba(...). |

theme.edge

| Key | Type | Default | Description | | ---------------------- | -------- | ------------- | ------------------------------------ | | fontSize | number | 6 | Edge label size at zoom 1×. | | labelPaddingX | number | 3 | Horizontal padding of label rect. | | labelPaddingY | number | 1 | Vertical padding of label rect. | | labelBackgroundColor | string | transparent | Background fill of the label rect. |

theme.label

| Key | Type | Default | Description | | ----------------- | -------- | ------- | ------------------------------------------------------------------------------ | | opacityMinZoom | number | 1.2 | Zoom at which labels start fading in. | | opacityMaxZoom | number | 1.7 | Zoom at which labels reach full opacity. | | zoomGrowthCap | number | 3 | Above this zoom, labels stop growing in screen pixels and zoom de-clutters. |

theme.hover

| Key | Type | Default | Description | | ----------------------- | -------- | ------- | ------------------------------------------------- | | labelShiftY | number | 2 | Vertical shift when hovered. | | hiddenLabelOpacity | number | 1 | Target opacity for labels that were hidden. | | animationDurationMs | number | 150 | Fade-in / fade-out duration. | | highlightOpacityBoost | number | 0.6 | Floor for label opacity while hovered. | | dimOpacity | number | 0.15 | Multiplier applied to non-hovered nodes. |

theme.force

| Key | Type | Default | Description | | ------------------ | -------- | ------- | -------------------------------------------------------- | | linkDistance | number | 120 | Fallback target link length when clustering is disabled. | | chargeStrength | number | -250 | forceManyBody strength (negative = repulsion). | | chargeDistanceMax| number | 500 | Maximum distance over which charge applies. | | alphaDecay | number | 0.015 | Simulation cooldown rate. | | velocityDecay | number | 0.6 | Per-tick velocity damping. | | boundaryStrength | number | 0.01 | forceX/forceY strength pulling toward (0, 0). |

theme.cluster

| Key | Type | Default | Description | | ------------------- | -------- | ------- | ------------------------------------------------------------ | | intraLinkDistance | number | 120 | Target link length between two nodes in the same community. | | interLinkDistance | number | 280 | Target link length for cross-community bridges. | | strength | number | 0.14 | Centroid pull strength (alpha-scaled on top of this). |

theme.click / theme.edgeClick

| Key | Type | Default | Description | | ------------------------- | -------- | ------- | ---------------------------------------- | | centerAnimationDuration | number | 300 | Pan animation duration (ms). | | targetZoom | number | 2 | Zoom level after a node/edge click. | | animationDuration | number | 300 | (edgeClick) Pan animation duration (ms). | | padding | number | 80 | (edgeClick) Padding around the edge. |

theme.fontFamily

| Key | Type | Default | | ------------ | -------- | ------------------------------------------------ | | fontFamily | string | Satoshi, Inter, ui-sans-serif, system-ui, sans-serif |

CSS variables (zoom indicator)

Override on .cg-root. The package only styles the container and the optional zoom indicator — everything inside the canvas is painted via theme values, not CSS.

| Variable | Default (light) | Purpose | | ---------------- | -------------------------- | -------------------------------- | | --cg-muted-fg | oklch(0.35 0.02 99.4974) | Empty-state / zoom-label colour. | | --cg-zoom-bg | transparent | Zoom indicator background. | | --cg-zoom-fg | var(--cg-muted-fg) | Zoom indicator text. |

The zoom indicator picks up cg-zoom-label--{position} modifier classes — override any of them in your own CSS to retarget placement.

Clustering

Enabled by default. On each nodes/edges change, the renderer runs Louvain community detection and feeds the result into the simulation as:

  • Variable link distance — intra-community links pull to theme.cluster.intraLinkDistance (120 by default); inter-community bridges pull to theme.cluster.interLinkDistance (280).
  • Centroid force — every tick each node is nudged toward its community's centroid at strength theme.cluster.strength · alpha.

Pass disableClustering to turn the whole thing off — the rest of the simulation (charge, collision, link spring) is untouched.

[!TIP] Bundle impact: graphology + graphology-communities-louvain add ~60 KB gzipped. Both are bundled into the package — no peer.

Mock data for development

import { MOCK_NODES, MOCK_EDGES } from "@crosmos/graph/mock";

500 nodes / ~550 edges of production-faithful data — single USER hub, the 7 canonical entity types (person, organization, technology, project, concept, location, object), the 23-relation canonical vocabulary, bimodal confidence, deterministic across loads. Useful for stress-testing the renderer or screenshots.

Sub-path exports

| Import path | What's there | | --------------------------- | --------------------------------------------------------------------------------------------- | | @crosmos/graph | ForceGraph, BaseNode, BaseEdge, theme types, DEFAULT_THEME, mergeTheme. | | @crosmos/graph/mock | 500-node mock dataset. | | @crosmos/graph/styles.css | Container + zoom-indicator default styles. |

SSR / Next.js

The renderer wraps react-force-graph-2d, which is canvas-based and client-only. <ForceGraph> is marked "use client" and dynamic-imports the lib internally, so it's safe to mount inside Next.js server components — the container will render its loadingState (or nothing) on the server and hydrate on the client.

[!IMPORTANT] Don't import @crosmos/graph from a server-only module. It needs the browser to actually draw.

Browser support

Modern evergreen browsers — Chrome / Edge / Firefox / Safari latest two majors. The CSS defaults use oklch() color (Chrome ≥ 111, Safari ≥ 15.4, Firefox ≥ 113).

License

MIT © Crosmos Labs