Custom renderers
When the built-in shapes are not enough, draw an item yourself with PixiJS 8. Give the item a
kind, and hand the view a module of renderers keyed by kind, loaded lazily. Items whose kind
has no renderer keep their shape.
custom-renderers.tsximport { createSceneSource, DataExplorer } from "@data-xplorer/core";
// Items with a `kind` are drawn by your renderer for that kind; the rest keep their shape.const source = createSceneSource<null>({ load: () => ({ id: "garden", heading: "Garden", clusters: [], nodes: ["Rose", "Tulip", "Daisy", "Iris", "Poppy", "Lily", "Fern"].map((name, i) => ({ id: name, label: name, weight: 3 + (i % 4), href: `#${name}`, drillsInto: null, shape: "disc" as const, ramp: i / 6, kind: name === "Fern" ? null : "flower", // Fern stays a plain disc })), edges: [], }),});
export default function CustomRenderers() { // The renderers module, and PixiJS with it, load only when the explorer first needs them. return <DataExplorer source={source} focus={null} renderers={() => import("./custom-renderers.renderers")} />;}The module it loads. It builds its views from the classes create(kit) is handed, and imports
pixi.js only as types:
// Import this module only through the dynamic import in custom-renderers.tsx, so it stays out of// the page's first load. It imports PixiJS only as types: its classes come from `create(kit)`, so// PixiJS reaches the page by one lazy path, the package's own.import type { Container } from "pixi.js";import { defineRenderers } from "@data-xplorer/core/pixi";
const PETALS = 6;
export default defineRenderers({ flower: { // One view, reused for whichever item it is given next: draw at a fixed design size of 100. create(kit) { const view: Container = new kit.Container(); const petals = new kit.Graphics(); for (let i = 0; i < PETALS; i++) { const a = (i / PETALS) * Math.PI * 2; petals.ellipse(Math.cos(a) * 26, Math.sin(a) * 26, 22, 22).fill(0xffffff); } view.addChild(petals, new kit.Graphics().circle(0, 0, 16).fill(0xfacc15)); return view; }, // Each frame: scale to the item's size, tint with its colour, turn slowly. update(view, node, frame) { view.scale.set(frame.size / 100); view.children[0]!.tint = frame.tint ?? 0xffffff; view.rotation = frame.reducedMotion ? 0 : (frame.timeMs / 4000 + node.x) % (Math.PI * 2); }, destroy: (view) => view.destroy({ children: true }), animated: true, // ask for frames while a flower is on screen },});- The kit.
create(kit)is handed PixiJS’s ownContainer,Graphics,Sprite,TextandTexture: the ones the package draws with. Build from these and importpixi.jsonly withimport type, so PixiJS reaches your page by one lazy path, the package’s. A second runtime import ofpixi.jsin a lazily loaded module made Bun’s bundler emit a PixiJS chunk as one app’s page entry (it built, then rendered blank). Other bundlers may cope; the kit makes it a non-question. - The frame gives
size(world units: draw centred on 0, 0),widthandheight(the item’s box, which in a fixed scene need not be square;sizeis the largest circle that fits it),screenSize(pixels, for level of detail),opacity,hovered,selected,timeMs,tint(the colour the item would have had),texture(the item’s own image, once ready) andreducedMotion. The second argument is the laid-out item; itsitemfield is your item as you wrote it. - The explorer still does culling, transitions, hit-testing, the outline, rings, halos, glows and labels. A custom view replaces only the shape.
- Pooling. Views are pooled per kind and reused for whichever item comes next, so keep
per-item state in
update, notcreate. - Animation.
animated: trueasks for frames while a view of that kind is on screen; underprefers-reduced-motionnone are asked for. - Failures. A kind with no renderer, or a renderer that throws, falls back to the shape and is reported once; one that throws is switched off for the session. Throws are isolated; drawing mistakes are not, so a broken polygon can still spoil the frame.
- Cap. At most
customViews.maxitems (200 by default) are drawn by renderers at once; past it, those furthest from the middle of the view draw their shape. @data-xplorer/core/pixiholds only the contract’s types anddefineRenderers. Import it only from your renderers module, so it and PixiJS stay out of your page’s first load.