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

@kvis/packed-radial-tree

v1.0.0

Published

Composable React library for packed radial tree visualizations of hierarchical data

Downloads

139

Readme

Packed Radial Tree

A composable React + headless library for visualizing hierarchical data using a packed radial tree layout. Perfect for ontologies, organizational charts, taxonomies, and other deep/wide tree structures.

NPM Version TypeScript License

Features

  • Composable architecture — pure layout engine, React hooks, and SVG components — use only what you need
  • Headless corecomputePackedRadialLayout() runs without React (server, web workers, custom renderers)
  • React hooks APIusePackedRadialTree() for fully custom UIs
  • Drop-in component<PackedRadialTree> for batteries-included use
  • Layout caching — drill-down/navigate-back is instant via built-in LayoutCache
  • Optional UI componentsTreeInfo, TreeControls, SelectionPanel for downstream composition
  • Tree-shakable — specific d3 modules, sideEffects: false, subpath exports
  • Multi-instance safe — scoped Jotai store per <TreeProvider>
  • Interactive — click, double-click, hover, zoom, pan, drill-down, breadcrumbs
  • Themeable — built-in light/dark modes plus full theme override
  • TypeScript-first — complete type safety and autocomplete

Installation

pnpm add @kvis/packed-radial-tree
# or
npm install @kvis/packed-radial-tree
# or
yarn add @kvis/packed-radial-tree

Peer Dependencies

pnpm add react react-dom

Jotai is now bundled internally — you no longer need to install it as a peer dependency.

Quick Start

import { PackedRadialTree, createSampleOntology } from '@kvis/packed-radial-tree'

function App() {
  const data = createSampleOntology()

  return (
    <PackedRadialTree
      data={data}
      width={800}
      height={800}
      onNodeSelect={(node) => console.log('Selected:', node)}
      onNodeDrillDown={(node) => console.log('Drilled into:', node)}
    />
  )
}

That's it — no Provider wrapping, no atom setup. The component manages its own scoped state.

Architecture

The library is split into three composable layers:

@kvis/packed-radial-tree
├── core/              Pure layout engine (no React)
├── react/             React hooks + components
└── (root)             Re-exports everything

When to use which layer

| If you want to... | Use | |---|---| | Render a tree quickly with no setup | <PackedRadialTree> | | Build a custom UI on top of the same layout/state | usePackedRadialTree() hook | | Render with props-only components (no Jotai store) | Compose <TreeNodes>, <TreeLinks>, <TreeLabels> directly | | Compute layouts on the server or in a worker | computePackedRadialLayout() from core | | Cache layouts across drill-down navigation | LayoutCache from core |

Headless Core API

Compute a layout with no React dependency:

import {
  buildHierarchy,
  computePackedRadialLayout,
  defaultPackedTreeOptions,
  LayoutCache,
} from '@kvis/packed-radial-tree'

const root = buildHierarchy(myTreeData)
const cache = new LayoutCache(50)

const layout = computePackedRadialLayout(root, defaultPackedTreeOptions, cache)
// → { root, nodes, links, sizeScale }

// Subsequent calls with the same options return the cached result
const cached = computePackedRadialLayout(root, defaultPackedTreeOptions, cache)

LayoutCache is keyed on rootId + layout-affecting options (maxDepth, isInterleaved, isPacked, isCollisionResolved, sizeColumn, sizeRange, radiusExponent). Visual-only changes like colorMode or showLabels do not invalidate the cache.

React Hooks API

Build custom UIs against the same shared state used by <PackedRadialTree>:

import {
  TreeProvider,
  usePackedRadialTree,
  TreeNodes,
  TreeLinks,
  ZoomableContainer,
  defaultPackedTreeOptions,
} from '@kvis/packed-radial-tree'

function CustomTree({ data }) {
  return (
    <TreeProvider data={data} options={defaultPackedTreeOptions}>
      <CustomTreeRenderer />
    </TreeProvider>
  )
}

function CustomTreeRenderer() {
  const {
    layout,
    selectedNode,
    highlightedNodeIds,
    zoomTransform,
    handlers,
  } = usePackedRadialTree()

  if (!layout) return null

  return (
    <ZoomableContainer
      width={800}
      height={800}
      onTransformChange={handlers.setZoomTransform}
    >
      <TreeLinks
        links={layout.links}
        transform={zoomTransform}
        options={defaultPackedTreeOptions}
        selectedNode={selectedNode}
        highlightedNodeIds={highlightedNodeIds}
      />
      <TreeNodes
        nodes={layout.nodes}
        sizeScale={layout.sizeScale}
        transform={zoomTransform}
        selectedNode={selectedNode}
        highlightedNodeIds={highlightedNodeIds}
        options={defaultPackedTreeOptions}
        onNodeClick={handlers.selectNode}
        onNodeDoubleClick={handlers.drillDown}
        onShowTooltip={() => {}}
        onHideTooltip={() => {}}
      />
    </ZoomableContainer>
  )
}

The hook returns:

  • layout — computed LayoutResult (or null while initializing)
  • selectedNode, ancestors, descendants, highlightedNodeIds
  • breadcrumbs — ancestors of the current drill-down root
  • zoomTransform — current d3 zoom transform
  • handlersselectNode, resetSelection, drillDown, navigateToBreadcrumb, setZoomTransform, resetZoom

Optional UI Components

Compose these into your own layouts. They accept props and don't require the <PackedRadialTree> wrapper.

<TreeInfo> — Tree statistics panel

import { TreeInfo, usePackedRadialTree } from '@kvis/packed-radial-tree'

function Sidebar({ data }) {
  const { layout } = usePackedRadialTree()
  return <TreeInfo data={data} layout={layout} />
}

Shows total nodes, leaf count, max depth, visible nodes, and root label.

<TreeControls> — Interactive configuration panel

import { TreeControls } from '@kvis/packed-radial-tree'

function Controls({ data, options, setOptions }) {
  return (
    <TreeControls
      data={data}
      options={options}
      onChange={(partial) => setOptions({ ...options, ...partial })}
    />
  )
}

Provides controls for max depth, interleave/pack/collision toggles, size column, color scale, label/tooltip visibility, and color mode.

<SelectionPanel> — Selected node details

import { SelectionPanel, usePackedRadialTree } from '@kvis/packed-radial-tree'

function Details() {
  const { selectedNode, ancestors, descendants, handlers } = usePackedRadialTree()

  return (
    <SelectionPanel
      selectedNode={selectedNode}
      ancestors={ancestors}
      descendants={descendants}
      onNavigate={handlers.navigateToBreadcrumb}
    />
  )
}

Shows node id, label, metrics, ancestor path (clickable), and direct children.

Data Format

interface TreeNode {
  id: string
  label?: string
  value?: number
  color?: string
  children?: TreeNode[]
  metrics?: Record<string, number>
  // subtreeSize, leafCount, internalCount are auto-computed
}

Building data

import {
  createTreeFromHierarchy,
  transformFlatDataToTree,
} from '@kvis/packed-radial-tree'

// From hierarchical input
const tree = createTreeFromHierarchy({
  id: 'root',
  label: 'Science',
  children: [
    { id: 'biology', label: 'Biology', value: 50 },
    { id: 'physics', label: 'Physics', value: 40 },
  ],
})

// From flat parent-child rows
const tree = transformFlatDataToTree([
  { id: 'root', label: 'Science', parentId: null },
  { id: 'biology', label: 'Biology', parentId: 'root' },
  { id: 'genetics', label: 'Genetics', parentId: 'biology' },
])

Configuration

interface PackedTreeOptions {
  // Layout
  maxDepth: number
  isInterleaved: boolean
  isPacked: boolean
  isCollisionResolved: boolean
  radiusExponent?: number

  // Sizing
  sizeColumn: string
  sizeRange: [number, number]

  // Coloring
  colorBy?: string
  colorScale: 'sequential' | 'diverging' | 'binary' | 'directional' | 'directionalWithThreshold'
  polarity: 1 | -1
  numBins: number

  // Visual
  showLabels?: boolean
  showTooltips?: boolean
  strokeWidth?: number
  strokeColor?: string
  colorMode?: 'light' | 'dark'
}

Interactions

| Action | Result | |---|---| | Click | Toggle node selection | | Double-click | Drill down into node (cached) | | Hover | Show tooltip | | Wheel / pinch | Zoom | | Drag | Pan | | Breadcrumb click | Navigate up the hierarchy (cached) |

Theming

Built-in light and dark themes:

<PackedRadialTree data={data} options={{ colorMode: 'dark' }} />

Or use the theme directly:

import { getTheme, lightTheme, darkTheme } from '@kvis/packed-radial-tree'

const theme = getTheme('dark')
<div style={{ background: theme.background, color: theme.text }} />

Multiple Instances

Multiple <PackedRadialTree> instances on the same page are fully isolated — each <TreeProvider> creates its own scoped Jotai store, so selection and zoom state never leak between trees.

<>
  <PackedRadialTree data={dataset1} />
  <PackedRadialTree data={dataset2} />
</>

API Reference

Core (headless)

| Export | Description | |---|---| | computePackedRadialLayout(root, options, cache?) | Compute layout (pure function) | | computeTreeMetrics(data) | Populate subtreeSize/leafCount/internalCount | | buildHierarchy(data) | Create d3 hierarchy with metrics | | LayoutCache | LRU cache for layout results | | assignAngles, interleaveNodes, resolveCollisions, applyCirclePacking | Layout algorithm primitives | | cloneHierarchyUpToDepth, findNodeInHierarchy, getAllAncestors | Hierarchy utilities | | createSizeScale, createRadiusScale, createColorScale | Scale factories | | LayoutResult | Layout return type |

React

| Export | Description | |---|---| | <PackedRadialTree> | Drop-in component | | <TreeProvider> | Scoped Jotai store wrapper | | usePackedRadialTree() | Main hook (layout + state + handlers) | | useTreeZoom() | Zoom-only hook | | useTreeSelection() | Selection-only hook | | <TreeNodes>, <TreeLinks>, <TreeLabels> | Composable SVG primitives | | <TreeBreadcrumbs>, <TreeTooltip>, <ZoomableContainer> | Composable UI primitives | | <TreeInfo>, <TreeControls>, <SelectionPanel> | Optional downstream components |

Utilities

| Export | Description | |---|---| | createTreeFromHierarchy(data) | Convert hierarchical data to TreeNode | | transformFlatDataToTree(items, rootId?) | Build tree from flat parent-child rows | | createSampleOntology() | 200+ node sample dataset | | validateTreeStructure(node) | Detect circular references | | getAvailableColumns(data) | Discover numeric columns | | getDefaultSizeColumn(data) | Best default size column | | darkenColor(color, factor) | Color manipulation helper | | getTheme, lightTheme, darkTheme | Theming | | defaultPackedTreeOptions | Default options object |

Development

This repo uses pnpm as the package manager (enforced via the packageManager field in package.json).

git clone <repository>
cd packed-radial-tree
pnpm install
pnpm demo       # Start demo dev server
pnpm build      # Build library
pnpm test       # Run test suite (68 tests)

If you don't have pnpm installed:

# Via corepack (built into Node 16+)
corepack enable
corepack prepare pnpm@latest --activate

# Or via npm
npm install -g pnpm

Publishing to npm

This package is configured for publishing to the @kvis scope on npm. Even though the package is published via npm registry, all build/version commands use pnpm locally.

1. One-time setup

# Log in to npm (creates ~/.npmrc with your auth token)
pnpm login

# Verify you're logged in as the correct user
pnpm whoami

If publishing under a scope (@kvis), make sure you have publish rights to that org:

npm org ls kvis

2. Pre-publish checklist

Before every release, verify the package is healthy:

# Type check and test
pnpm exec tsc --noEmit
pnpm test

# Build the library
pnpm build

# Inspect what will be published (do NOT actually publish)
pnpm pack --dry-run

pnpm pack --dry-run lists every file that will end up in the tarball. Confirm it only contains the dist/ folder, package.json, README.md, and LICENSE.

3. Bump the version

Use pnpm version to bump and tag in one step:

pnpm version patch   # 1.0.0 → 1.0.1 (bug fix)
pnpm version minor   # 1.0.0 → 1.1.0 (new feature, backward compatible)
pnpm version major   # 1.0.0 → 2.0.0 (breaking change)

This updates package.json, creates a git commit, and tags it (e.g. v1.0.1).

4. Publish

For a scoped package, the first publish needs --access public:

# First-time publish for the scope
pnpm publish --access public

# Subsequent publishes
pnpm publish

To preview the publish without uploading:

pnpm publish --dry-run

Note: pnpm publish automatically runs the build script via the prepublishOnly lifecycle if defined. If you want pnpm to skip git status checks, add --no-git-checks.

5. Push the version tag

git push origin main --follow-tags

Releasing a beta / pre-release

Use a dist-tag to publish without affecting latest:

pnpm version prerelease --preid=beta   # 1.0.0 → 1.0.1-beta.0
pnpm publish --tag beta

Consumers install it explicitly:

pnpm add @kvis/packed-radial-tree@beta

What ships in the tarball

The files field in package.json whitelists only dist/, so the published package contains:

dist/
  index.js          # UMD build
  index.esm.js      # ES module build
  index.d.ts        # TypeScript declarations
  ...
package.json
README.md
LICENSE

Source files, tests, the demo, and config files are not published.

Verifying the published package

After publishing, verify it works in a fresh project:

mkdir /tmp/verify-prt && cd /tmp/verify-prt
pnpm init
pnpm add @kvis/packed-radial-tree react react-dom
node -e "console.log(Object.keys(require('@kvis/packed-radial-tree')))"

You should see all exported symbols (PackedRadialTree, usePackedRadialTree, computePackedRadialLayout, etc.).

Unpublishing (emergency only)

npm allows unpublishing a version within 72 hours of publishing:

npm unpublish @kvis/[email protected]

After 72 hours, deprecate instead:

npm deprecate @kvis/[email protected] "Critical bug, use 1.0.2"

License

MIT — see LICENSE.

Acknowledgments

  • Layout algorithms powered by d3 (specific modules: d3-hierarchy, d3-scale, d3-force, d3-zoom)
  • Internal state managed by Jotai (bundled, not exposed)
  • Rendering uses raw SVG — no UI framework dependency