demo D278
How work flows through the board
A cumulative-flow diagram of what sits in each column day by day, with cycle time, lead time and throughput read from the board’s own movements, so a slowdown shows before it becomes a surprise
board.flow · cfd() · cycleTime()
Loading a live grid…
The configuration
'kanban-flow-metrics': () => ({
rows: [],
config: {},
foot: [
'the cumulative-flow diagram stacks how many cards sit in each column day by day, so a widening band is work piling up and a flattening top is delivery slowing',
'cycle time is how long a card takes once work starts, lead time is the whole wait from arrival, both read from the board’s own movements',
'throughput counts cards finished per week, the rate the board is actually clearing work at',
'every figure comes from the moves the board records, so it stays right as cards are dragged',
],
mount: (el: HTMLElement, LG: any) => {
const { rows, history } = flowSeed();
let board: any;
let chart: any;
let chartGrid: any;
let disposed = false;
const grid = document.createElement('div');
grid.style.cssText = 'display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);gap:16px;align-items:start';
const boardCol = document.createElement('div');
boardCol.style.minWidth = '0';
const boardEl = document.createElement('div');
boardEl.style.cssText = 'height:520px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);overflow:hidden';
boardCol.append(gridTitle('The board'), boardEl);
const rightCol = document.createElement('div');
rightCol.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:14px';
const tiles = document.createElement('div');
tiles.style.cssText = 'display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px';
const cfdEl = document.createElement('div');
cfdEl.style.cssText = 'height:300px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
rightCol.append(tiles, gridTitle('Cumulative flow'), cfdEl);
grid.append(boardCol, rightCol);
el.append(grid);
const tile = (label: string, value: string, sub: string) => {
const t = document.createElement('div');
t.style.cssText = 'border:1px solid var(--rule);border-radius:9px;background:var(--paper);padding:12px 14px';
const v = document.createElement('div');
v.style.cssText = 'font-size:1.5rem;font-weight:650;color:var(--ink);line-height:1.1';
v.textContent = value;
const l = document.createElement('div');
l.style.cssText = 'font-size:.75rem;color:var(--ink-2);margin-top:4px';
l.textContent = label;
const s = document.createElement('div');
s.style.cssText = 'font-size:.7rem;color:var(--ink-3,var(--ink-2));margin-top:2px';
s.textContent = sub;
t.append(v, l, s);
return t;
};
const DAY = 86_400_000;
const days = (ms: number | null) => (ms == null ? 'n/a' : (ms / DAY).toFixed(1));
loadKanban()
.then((KB: any) => {
if (disposed) return;
board = KB.createKanban(boardEl, {
rows,
rowKey: 'id',
columnProperty: 'status',
columns: [
{ id: 'backlog', title: 'Backlog' },
{ id: 'todo', title: 'To do' },
{ id: 'doing', title: 'In progress' },
{ id: 'review', title: 'In review' },
{ id: 'done', title: 'Done', color: '#2e7d32' },
],
pointsProperty: 'points',
showPoints: true,
card: { title: { field: 'title' }, subtitle: 'assignee', badges: 'epic' },
doneColumns: ['done'],
ariaLabel: 'Delivery board',
flow: { history, doneColumns: ['done'], startColumns: ['doing'] },
});
board.flow.seed();
const cycle = board.flow.cycleTime();
const lead = board.flow.leadTime();
const tp = board.flow.throughput({ bucket: 'week' });
const perWeek = tp.length ? (tp.reduce((n: number, b: any) => n + b.count, 0) / tp.length) : 0;
tiles.append(
tile('Cycle time, median', `${days(cycle.median)}d`, `85th percentile ${days(cycle.p85)}d`),
tile('Lead time, median', `${days(lead.median)}d`, `${lead.count} cards delivered`),
tile('Throughput', `${perWeek.toFixed(1)}/wk`, `over ${tp.length} weeks`),
);
const cd = board.flow.chartData('cfd', { bucket: 'day' });
chartGrid = LG.createHeadlessGrid({
rowKey: 't',
columns: cd.columns.map((c: any) => ({
field: c.field,
title: c.title,
...(c.field === 't' ? {} : { type: 'number' }),
})),
rows: cd.rows,
});
loadCharts()
.then(({ createChart }: any) => {
if (disposed) return;
try {
chart = createChart({ grid: chartGrid, container: cfdEl, ...cd.spec, title: undefined });
} catch (err) {
cfdEl.textContent = `chart failed: ${(err as Error)?.message ?? err}`;
cfdEl.style.cssText += ';font:12px system-ui;color:#c92a2a;padding:8px';
}
})
.catch((err) => console.error('[flow-cfd]', err));
})
.catch((err) => console.error('[kanban-flow]', err));
return () => {
disposed = true;
chart?.destroy?.();
chartGrid?.destroy?.();
board?.destroy?.();
};
},
})