Skip to content

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

customNode.ts
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.