Skip to content

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.

Running the code below: custom-renderers.tsx
custom-renderers.tsx
import { 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:

custom-renderers.renderers.ts
// 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 own Container, Graphics, Sprite, Text and Texture: the ones the package draws with. Build from these and import pixi.js only with import type, so PixiJS reaches your page by one lazy path, the package’s. A second runtime import of pixi.js in 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), width and height (the item’s box, which in a fixed scene need not be square; size is 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) and reducedMotion. The second argument is the laid-out item; its item field 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, not create.
  • Animation. animated: true asks for frames while a view of that kind is on screen; under prefers-reduced-motion none 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.max items (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/pixi holds only the contract’s types and defineRenderers. Import it only from your renderers module, so it and PixiJS stay out of your page’s first load.