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

@poe2-toolkit/tree-react

v0.5.0

Published

React renderer for the Path of Exile 2 passive tree. A thin view adapter over @poe2-toolkit/tree-core: it draws what the core computed and owns pan/zoom/hover/click. No geometry.

Readme

@poe2-toolkit/tree-react

npm types: TypeScript React 18+ license: MIT

React renderer for the Path of Exile 2 passive tree. It's a thin view layer on top of @poe2-toolkit/tree-core: the core works out where everything goes, and this package draws it on a WebGL canvas (via PixiJS) and runs panning, zooming, hovering, and clicking.

The whole tree is built once as a Pixi scene graph in world space; pan and zoom only move the world container's transform, so the GPU recomposites without re-rasterising any sprites. That keeps a full-tree pan/zoom smooth even where a Canvas2D drawImage would fall back to the CPU.

It does no geometry of its own. Positions, sizes, rotations, the hub layout, and hit-testing all come from the core. If you ever catch this package computing a coordinate, that's a bug.

The geometry lives in a framework-agnostic core, so the same Scene can just as easily be drawn from Vue, Svelte, or anything else. React is what this project happens to use.

Live demo: see this renderer in a real app at poe.rajtik.com/tree.

Who does what

| Concern | Owner | |---|---| | node positions, sizes, hub geometry, arcs, hit-test math | core | | WebGL canvas (PixiJS), device-pixel sizing, scene graph, layer order | this package | | pan, zoom, wheel, fullscreen, pointer hover and click | this package | | loading atlas bitmaps, colors, tooltips, surrounding UI | you |

Install

npm install @poe2-toolkit/tree-react @poe2-toolkit/tree-core

React 18 or newer is a peer dependency. PixiJS (pixi.js v8) is a direct dependency, pulled in automatically. It's the WebGL backend.

Usage

The tree data comes from GGG's official skill-tree export. Run it through the core's GGG adapter to get a TreeData, build a Scene from it, and hand that to TreeView.

import { buildScene } from '@poe2-toolkit/tree-core';
import { normalizeGggTree } from '@poe2-toolkit/tree-core/ggg';
import { TreeView } from '@poe2-toolkit/tree-react';

// Normalize GGG's data.json into the engine's TreeData (once per tree).
const data = normalizeGggTree(rawGggExport, '0_5');

// Build a render-ready scene for the current build (rebuild it on edits).
const scene = buildScene(data, { allocation });

// Draw it. `resources` (atlas bitmaps + manifest) is optional; leave it out
// and you get the vector debug render.
<TreeView
  scene={scene}
  resources={{ manifest, atlases }}
  activeClassId={allocation.classId}
  activeAscendancy={allocation.ascendId}
  onNodeClick={(skill) => toggle(skill)}
/>;

The pattern is state in, intent out. The scene already holds everything visual, so the component just reports what the user did (onNodeClick, onNodeHover, and the rest) and never touches the build itself.

Graphics

TreeView doesn't load any images. You give it a RenderResources:

interface RenderResources {
  manifest: SpriteManifest;                     // sprite key -> native atlas rect
  atlases: Record<string, CanvasImageSource>;   // atlas id -> bitmap
}

To draw a node, the renderer turns it into a sprite key with the helpers in spriteKeys (iconKeyFor, frameKeyFor, effectKeyFor, and friends), looks that key up in your manifest, and blits the rect from the matching atlas. The keys follow GGG's atlas naming, so pointing the renderer at a different atlas set comes down to swapping that one file. Leave resources out and you get a plain vector render of discs and rails, which is handy for debugging without art.

A node has one colour icon. Unallocated nodes draw that same icon dimmed with a 50% grey multiply tint at draw time, the game's own look, so you supply a single skill-icon atlas, not a separate inactive one.

Weapon sets. When the scene's nodes/rails carry a weaponSet tag (set by core.buildScene from the build's weaponSets), TreeView tints them apart from the gold basic tree: set I green, set II blue, on both the node frame and its active rail. A rail bridging the two sets is drawn inactive, since it belongs to neither set's view.

The hub artwork (class portrait and ornate ring) comes in through the optional centreSprites prop. Skip it and the hub falls back to a vector placeholder.

Component props

<TreeView
  scene={scene}                 // required: core.buildScene output
  resources={resources}         // atlas bitmaps + manifest (omit for vector)
  activeClassId={classId}       // rotates the active ring onto the class
  activeAscendancy={ascId}      // relocates that ascendancy disc into the hub
  centreSprites={centreSprites} // optional portrait + ring artwork
  preview={preview}             // hover highlight: pending add (gold, or the set's tint) / remove (red, ringing each removed node)
  highlight={searchHits}        // skill ids to ring with a standing teal ring (e.g. search hits)
  highlightStyle={ringStyle}    // tune the highlight rings' colours, widths and pulse
  focus={worldRect}             // pass a fresh rect to pan + zoom-fit to it
  wheelZoom                     // turn on wheel zoom (off by default)
  debugIds                      // overlay each node's skill id (debug; off by default)
  zoom={{ maxScale, minFitFactor, overscroll }} // tune zoom-in cap, zoom-out floor, pan slack
  controls={controlsRef}        // imperative zoomIn() / zoomOut()
  onNodeClick={(skill, screen) => …}
  onNodeDoubleClick={(skill) => …}
  onNodeHover={(skill, screen) => …}
  onInteractStart={() => …}     // a press started on the canvas (e.g. close popovers)
  className={className}         // forwarded to the canvas wrapper element
  style={style}                 // forwarded to the canvas wrapper element
/>

Exported types: TreeViewProps, TreeViewControls, AllocationPreview, CentreSprite, ZoomLimits, HighlightStyle, RenderResources.

highlightStyle tunes the look of the highlight rings, each field optional: glowColor / coreColor (the soft outer and bright inner ring), glowWidth / coreWidth, radius (gap from the node frame), pulseMs (period; 0 for a still ring), pulseGrow (extra radius at the peak), and glowAlpha / coreAlpha ([trough, peak]). Omit the prop for the standing teal pulse.

zoom tunes the view extents, each field optional: maxScale caps zoom-in, minFitFactor is the zoom-out floor as a multiple of the fit-the-whole-tree scale (1 stops you zooming out past the whole tree), and overscroll is how far past the edges you can pan. Omit zoom for the defaults (4, 0.85, 0.5).

For external +/- buttons, reach for the imperative handle on controls:

const controls = useRef<TreeViewControls>(null);
// …
<button onClick={() => controls.current?.zoomIn()}>+</button>

Non-goals

This package won't:

  • compute or adjust any position, size, rotation, or hub placement;
  • carry magic numbers for node, icon, or effect sizing;
  • lock itself to one data source (it only knows Scene and SpriteManifest);
  • claim to be the only frontend. A Vue, Svelte, or Livewire renderer on the same contract is every bit as valid.

Attributions and legal

This is an unofficial, fan-made project, not affiliated with, endorsed by, or sponsored by Grinding Gear Games. "Path of Exile 2" is a trademark of Grinding Gear Games, and all game content, data, and art are their property. This package ships code only and stores nothing derived from the game. Thank you to Grinding Gear Games for making Path of Exile 2. See the repository NOTICE.

License

MIT. See LICENSE.