ModsCode / Gallery / Designs / Chord diagram
Chord diagram
Handoffs between agents around a ring: ribbon width by handoffs each way, with the busiest pair in Claude orange and its count.


hooks/designs/chord.mjs
// chord: a week of handoffs between six agents of a multi-agent Claude Code run, from a 6 x 6 matrix
// (row hands work to column, 317 handoffs in all, none to itself). The layout is d3-chord's chord()
// with a 0.04 rad gap between agents and each agent's ribbons sorted largest first, the ring is
// d3-shape's arc() and the ribbons d3-chord's ribbon() (all ISC, Mike Bostock). Ribbons are an 8% ink
// fill with a 1px edge at 22%, so the crossings read as lines, the way candle reads as wicks: the
// ribbons cover the whole disc, and a 25% fill, then a 14% fill with 35% edges, each turned it into a
// grey plate brighter than candle. The ring is a 6px band at candle's 55% body. Coder and Reviewer swap
// work most (42 + 34 = 76 handoffs), the review loop, and that one ribbon is Claude orange with its count
// written inside its wide Coder end (a one-entry key was dropped for that); the arcs stay ink, so Claude orange
// marks one pair only. Each name sits outside the ring, anchored away from it by the side it is on,
// with the handoffs that agent sent. Sample data only.
import { chord, ribbon, arc } from '../vendor/d3.mjs'
const W = 380
const H = 320
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const CLAUDE = '#D77757'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const CX = 190
const CY = 156
const R_IN = 104
const R_OUT = 110
const AGENTS = ['Planner', 'Coder', 'Reviewer', 'Tester', 'Docs', 'Researcher']
// MATRIX[i][j]: handoffs from agent i to agent j in the week
const MATRIX = [
[0, 38, 6, 8, 5, 22],
[10, 0, 42, 30, 9, 6],
[4, 34, 0, 8, 3, 2],
[3, 26, 5, 0, 2, 1],
[2, 4, 6, 1, 0, 3],
[18, 12, 1, 2, 4, 0],
]
const PAIR = [1, 2] // Coder and Reviewer: the one Claude orange ribbon
const f = n => n.toFixed(1)
// ribbon() has no digits(), so its path is rounded here to one decimal
const round = d => d.replace(/-?\d+\.\d+(?:e-?\d+)?|-?\d+e-?\d+/g, m => Number(m).toFixed(1))
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start', font = FONT } = {}) =>
`<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${font}" fill="${fill}">${esc(s)}</text>`
function draw() {
const chords = chord().padAngle(0.04).sortSubgroups((a, b) => b - a)(MATRIX)
const ring = arc().innerRadius(R_IN).outerRadius(R_OUT).digits(1)
const band = ribbon().radius(R_IN - 1)
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
parts.push(`<g transform="translate(${CX} ${CY})">`)
const isPair = c => (c.source.index === PAIR[0] && c.target.index === PAIR[1]) || (c.source.index === PAIR[1] && c.target.index === PAIR[0])
for (const c of [...chords].sort((a, b) => isPair(a) - isPair(b))) {
const look = isPair(c) ? `fill="${CLAUDE}" fill-opacity="0.55"` : `fill="${INK}" fill-opacity="0.08"`
parts.push(`<path d="${round(band(c))}" ${look} stroke="${isPair(c) ? CLAUDE : INK}" stroke-opacity="0.22"/>`)
}
for (const g of chords.groups) parts.push(`<path d="${ring(g)}" fill="${INK}" fill-opacity="0.55"/>`)
parts.push('</g>')
// Names outside the ring at the middle of each arc: anchored away from the ring by side, and pushed a
// little further at the top and bottom, where the text runs along the ring instead of away from it
for (const g of chords.groups) {
const a = (g.startAngle + g.endAngle) / 2 // 0 at 12 o'clock, clockwise
const sx = Math.sin(a)
const cy = -Math.cos(a)
const r = R_OUT + 8
const x = CX + r * sx
const y = CY + r * cy + 3.5 + cy * 6
const anchor = Math.abs(sx) < 0.2 ? 'middle' : sx > 0 ? 'start' : 'end'
const sent = MATRIX[g.index].reduce((s, v) => s + v, 0)
parts.push(`<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="10" font-family="${FONT}" fill="${INK}">${esc(AGENTS[g.index])}<tspan font-size="9" fill="${MUTED}"> ${sent}</tspan></text>`)
}
// The Claude orange ribbon carries its own count, both ways together, inside its wide end at the first agent
// of the pair, where no other ribbon crosses it (a one-entry key below was dropped for this)
const [p, q] = PAIR
const hot = chords.find(isPair)
const end = hot.source.index === p ? hot.source : hot.target
const a = (end.startAngle + end.endAngle) / 2
const r = R_IN * 0.78
parts.push(text(CX + r * Math.sin(a), CY - r * Math.cos(a) + 3.5, MATRIX[p][q] + MATRIX[q][p], { size: 10, fill: INK, anchor: 'middle' }))
const total = MATRIX.flat().reduce((s, v) => s + v, 0)
parts.push(text(W - 12, H - 8, `TOTAL ${total}`, { anchor: 'end' }))
parts.push(text(12, H - 8, 'SAMPLE HANDOFFS IN A WEEK'))
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}
let source // drawn on first use and kept: the pane loads every design, so none pays at load time
export default {
id: 'chord',
title: 'Chord diagram',
category: 'flow',
description: 'Handoffs between agents around a ring: ribbon width by handoffs each way, with the busiest pair in Claude orange and its count.',
element: ui => ui.Svg({ source: (source ??= draw()), alt: `Chord diagram of handoffs between ${AGENTS.length} agents, with the ${AGENTS[PAIR[0]]} and ${AGENTS[PAIR[1]]} pair in Claude orange, sample data`, width: W, height: H }),
}
Imports d3-chord 3.0.1 (ISC), d3-shape 3.2.0 (ISC), bundled with its licence in look's hooks/vendor/.
To use it in a mod: copy the file to your mod's hooks/designs/ and return its element(ui) from a pane's ui.render. How a pane is built.
Code: PolyForm Noncommercial License 1.0.0: noncommercial use only. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).