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

@prnt/dagr-react

v0.1.4

Published

React bindings for Dagr: the DagrCanvas component and hooks.

Readme

@prnt/dagr-react

Dagr as one React component: a graph goes in, a picture comes out.

pnpm add @prnt/dagr-react @prnt/dagr-graph @prnt/dagr-render three react react-dom

@prnt/dagr-graph, @prnt/dagr-render, react and react-dom are peer dependencies. @prnt/dagr-layout is a real dependency and comes with the install.

import { Graph } from '@prnt/dagr-graph';
import { DagrCanvas, Html } from '@prnt/dagr-react';

const graph = new Graph();
graph.addNode({ id: 'plan' });
graph.addNode({ id: 'build' });
graph.addEdge({ source: 'plan', target: 'build' });

export function Board() {
  return (
    <DagrCanvas graph={graph} style={{ width: '100%', height: 480 }}>
      <Html node="plan">Plan</Html>
      <Html node="build">Build</Html>
    </DagrCanvas>
  );
}

DagrCanvas is the component. useDagr is the layout on its own, Html puts React content in world coordinates, and useDagrCanvas reaches the renderer. retarget, toMotionDelta, and toMotionRoster support callers driving scene motion themselves. useGraphInteraction adds controlled selection and click-versus-pan gestures around hit geometry the caller supplies.

Add animate and an edit glides to its new layout instead of cutting to it:

<DagrCanvas graph={graph} animate />

That is the whole of it. useDagr holds a layout engine across renders, so an edit is a LayoutDelta rather than a cold run, and the component drives @prnt/dagr-render's springs and loop off that delta through its own coalesced frame. The camera is fitted once and then it is yours: a following camera is fitBounds on the sprung box handed to onFrame, which is your line of code rather than the component's.

Shared selection and gestures

useGraphInteraction works with DOM, SVG, canvas, and future GPU picking because the package does not guess where your nodes are. Your synchronous hit provider receives a surface-relative CSS point, the same point converted through your camera, the displayed DPR you supply, and an opaque displayed-scene revision. It returns a stable node, port, or empty result stamped with the revision it queried.

useGraphInteraction({
  surfaceRef,
  displayedRevision: frame,
  devicePixelRatio: renderedDpr,
  screenToWorld: (point) => camera.screenToWorld(point),
  hitTarget: (query) => hitIndex.query(query),
  selection,
  onSelectionChange: setSelection,
  onPanStart: () => camera.stop(),
  onPanBy: (delta) => camera.panByScreen(delta),
});

Coordinates stay in CSS pixels until your provider chooses otherwise. Do not multiply the point by DPR before screenToWorld; use devicePixelRatio only when reading a device-pixel buffer. Change displayedRevision whenever target geometry, draw order, membership, or its paired DPR changes on screen, including animation frames. Hits and misses both carry a stamp. The hook rejects a provider result with an older stamp and rejects a click when the revision changed between press and release.

Selection is controlled. A click selects on release only when press and release hit the same stable identity. Five CSS pixels turns the sequence into a pan and suppresses its pointer-generated click. Pointer cancellation never selects.

Native controls are excluded. A focusable graph target opts in with data-dagr-interaction-target; a nested input still wins and remains native. The hook does not handle keyboard events, so keep graph targets focusable and handle their keyboard click (event.detail === 0) in your component. Set touch-action: none on the surface for touch panning and user-select: none for drag presentation.

The hook itself has no hit geometry: you supply the provider. DagrCanvas supplies exact node hits (M5.2b); port geometry and connection gestures are M6.3. Edges are not hit targets in this slice.

Navigation, node events and level of detail

const api = useRef<DagrCanvasApi>(null);

<DagrCanvas
  graph={graph}
  navigation                      // wheel/pinch zoom, drag pan, keyboard; inside camera limits
  label="Relationship graph"
  apiRef={api}                    // api.current.focusNode(id), .fit()
  onNodeClick={(id) => open(id)}  // exact CPU hit test on the drawn silhouettes
  onNodeHover={(id) => hover(id)}
  onBackgroundClick={close}
  nodeTiers={tiers}               // dot -> label -> card, gated by on-screen width
  nodeData={nodeData}             // memoised; required when tiers use your own data type
/>

nodeTiers are the renderer's RichNodeTiers: below the first gate a node is only its instanced GPU shape, and only nodes in view mount DOM, pooled across a pan. Elements are tagged data-dagr-node-id; Enter or Space on a focused one calls onNodeClick; focus is restored to a node's element when the overlay recycles it. Hits cover node silhouettes only (no edges or ports), and navigation is off by default. When neither WebGPU nor WebGL 2 exists, probe with detectBackendSupport() from @prnt/dagr-render and render a fallback; onError covers a device that dies late. Full details: the React guide.

Read this first: the graph prop is watched, not compared

A Graph is mutable, so comparing it by identity the way React compares everything would mean graph.addNode(...) changed nothing on screen until you also replaced the object. Instead the hook subscribes to the graph through useSyncExternalStore, and both of these redraw:

graph.addNode({ id: 'ship' });
setGraph(rebuildFromScratch());

There is one narrow window this leaves open and it is real. React subscribes in an effect, after the render that read the store, and effects run child first. A child's mount effect that edits the graph runs before the canvas has subscribed, so that one edit is not picked up until the next one arrives, which reports both. Edit in a parent effect, or in an event handler, and it is picked up straight away.

One more thing worth knowing before your first multi-step edit: wrap it in graph.batch. Each mutating call is a patch and a relayout of its own, so adding a node and then wiring it up is three of each, computing two layouts that are never drawn: React commits once, holding the last. The component notices and reseats rather than animating from a delta it cannot trust, so the drawing is right either way, and a batch is one patch, one layout and one glide.

Reporting layout changes

onLayout(result, delta, from, continues) reports each committed layout. Use continues && delta !== null before counting moved/added/removed nodes. continues is false for cold runs, after layout failures, and when React skips an intermediate layout. It works with animation enabled or disabled and does not mean rendering has finished. Handlers taking fewer arguments still work.

Documentation

The component, the hook, the animation, the overlay and the two conversions are on the React bindings page.

MIT © prnt.design

Node groups

Pass groups={[{ id: 'processing', label: 'Processing', nodeIds: ['parse', 'validate'] }]} to DagrCanvas to draw a boundary that follows those nodes. This is visual membership, not a layout constraint. See the grouping guide.

Default navigation limits

DagrCanvas constrains its camera by default. The minimum zoom fits the complete graph, including group labels and padding. The maximum fits a single node with 5% margin (or fitPadding); for mixed node sizes the smallest fitting node sets the ceiling. Panning stays within the padded content bounds and keeps part of an actual node visible, including in sparse graphs. An axis smaller than the viewport stays centered. Limits follow resizing, layout changes, and animated bounds without resetting a valid close-up.

fit={false} only skips the initial fit. Use cameraLimits={false} to opt out of content constraints when implementing a custom camera policy. Low-level Camera2D instances remain unrestricted until setContentBounds is called.

Edge paths

Set edgePath={{ style: 'smooth' }} or edgePath={{ style: 'orthogonal', direction: 'vertical' }} on DagrCanvas. The default polyline preserves routed segments. Options update live, including during animation. These visual styles preserve route anchors but do not avoid obstacles; see the React guide.