Example · Customization
Custom Node Rendering
Real-world apps need more than a labeled circle. Org charts want avatars and titles. Network topologies want status icons. SNA dashboards want degree indicators. Use the renderer hook to draw whatever you want — straight to canvas — without dropping framework support.
Avatars and multi-line labels
import { create } from '@topokit/renderer-canvas';
const avatarCache = new Map<string, HTMLImageElement>();
function loadAvatar(url: string): HTMLImageElement {
let img = avatarCache.get(url);
if (!img) {
img = new Image();
img.crossOrigin = 'anonymous';
img.src = url;
avatarCache.set(url, img);
}
return img;
}
const app = create(container, {
nodes, edges,
layout: 'hierarchical',
renderNode: (ctx, node, { x, y, scale }) => {
const r = 28;
// Card background
ctx.fillStyle = node.data.isManager ? '#1e293b' : '#0f172a';
ctx.strokeStyle = '#334155';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.roundRect(x - 90, y - r, 180, 2 * r, 12);
ctx.fill();
ctx.stroke();
// Avatar (clipped to circle)
const img = loadAvatar(node.data.avatar);
if (img.complete) {
ctx.save();
ctx.beginPath();
ctx.arc(x - 66, y, 18, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(img, x - 84, y - 18, 36, 36);
ctx.restore();
}
// Multi-line label
ctx.fillStyle = '#f1f5f9';
ctx.font = '600 13px Inter, sans-serif';
ctx.textBaseline = 'middle';
ctx.fillText(node.label, x - 40, y - 8);
ctx.fillStyle = '#94a3b8';
ctx.font = '400 11px Inter, sans-serif';
ctx.fillText(node.data.title, x - 40, y + 10);
// Status badge in the corner
if (node.data.status === 'alert') {
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.arc(x + 82, y - 22, 5, 0, Math.PI * 2);
ctx.fill();
}
},
// Update the hit area so clicks land on the card, not just the center.
nodeBounds: () => ({ width: 180, height: 56 }),
}); Tip — bind to LOD
At low zoom, drawing 36×36 avatars wastes paint time. Use the scale
argument in your renderNode callback to skip details until the user is zoomed in enough to see them.