@kvis/packed-radial-tree
v1.0.0
Published
Composable React library for packed radial tree visualizations of hierarchical data
Downloads
139
Maintainers
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.
Features
- Composable architecture — pure layout engine, React hooks, and SVG components — use only what you need
- Headless core —
computePackedRadialLayout()runs without React (server, web workers, custom renderers) - React hooks API —
usePackedRadialTree()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 components —
TreeInfo,TreeControls,SelectionPanelfor 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-treePeer Dependencies
pnpm add react react-domJotai 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 everythingWhen 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— computedLayoutResult(ornullwhile initializing)selectedNode,ancestors,descendants,highlightedNodeIdsbreadcrumbs— ancestors of the current drill-down rootzoomTransform— current d3 zoom transformhandlers—selectNode,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 pnpmPublishing 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 whoamiIf publishing under a scope (@kvis), make sure you have publish rights to that org:
npm org ls kvis2. 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-runpnpm 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 publishTo preview the publish without uploading:
pnpm publish --dry-runNote:
pnpm publishautomatically runs thebuildscript via theprepublishOnlylifecycle 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-tagsReleasing 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 betaConsumers install it explicitly:
pnpm add @kvis/packed-radial-tree@betaWhat 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
LICENSESource 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.
