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

v1.0.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, arcConnectorKeyFor, lineConnectorKeyFor), 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 red, set II green by default (the in-game colours), 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 set tints and the removal-preview colour are tunable through the colors prop; the defaults are also exported as DEFAULT_TREE_COLORS if you want to extend rather than replace them.

The hub artwork (class portrait and ornate ring) comes in through the optional centreSprites prop, holding up to three CentreSprites (portrait, ringStatic, ringActive). Each is a source image url plus the sub-rect to crop (sx, sy, sw, sh). Skip the prop 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 (magenta, ringing each removed node)
  edgeOverlays={overlays}       // paint groups of edges, each in its own colour (a diff, a route, a plan)
  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
  colors={palette}              // tune the weapon-set tints + removal-preview colour (defaults: set I red, set II green, removal magenta)
  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, EdgeOverlay, CentreSprite, ZoomLimits, HighlightStyle, RenderResources. Every prop and every field on these types carries its own doc-comment, shown on hover in your editor and shipped in the package's .d.ts — so the per-field detail lives there, not duplicated here.

preview is an AllocationPreview: kind (add or remove), the nodes (skill ids) and edges (edge keys, min-max of the two node ids) it would touch, and an optional weaponSet (1 or 2) that tints an add into that set's colour. Pass null to clear it.

edgeOverlays paints arbitrary groups of edges over the base render, each in its own colour. It is a multi-colour generalisation of preview's edge highlight, and the renderer stays agnostic about what a group means: you decide, whether a build diff, a planned route, or anything else. Each group is { edges, color, includeInactive? }, where edges is a set of edge keys (min-max of the two node ids), color is 0xRRGGBB, and includeInactive also paints edges whose endpoints are not both allocated (the dim base rails), so you can surface a path the build does not yet hold. Groups are drawn in array order, so a later one wins where two share an edge.

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.