From 755a5fcb9ad1c1207272eb58f6acda2bdfc098dd Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 26 Feb 2026 17:49:12 +0100 Subject: [PATCH] dashboard: move Open Workstreams by Domain chart to top of overview page Co-Authored-By: Claude Sonnet 4.6 --- state-hub/dashboard/src/index.md | 142 +++++++++++++++---------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/state-hub/dashboard/src/index.md b/state-hub/dashboard/src/index.md index cb7698a..94c8f00 100644 --- a/state-hub/dashboard/src/index.md +++ b/state-hub/dashboard/src/index.md @@ -85,6 +85,77 @@ injectTocTop("live-indicator", _liveEl); if (summary.error) display(html`
⚠️ ${summary.error}
`); ``` +## Open Workstreams by Domain + +```js +import * as Plot from "npm:@observablehq/plot"; + +const topicById = Object.fromEntries((summary.topics ?? []).map(t => [t.id, t.domain])); + +const openWs = (summary.open_workstreams ?? []).map(w => ({ + title: w.title, + domain: topicById[w.topic_id] ?? "unknown", + done: w.tasks_done ?? 0, + in_progress: w.tasks_in_progress ?? 0, + blocked: w.tasks_blocked ?? 0, + todo: w.tasks_todo ?? 0, + total: w.tasks_total ?? 0, +})).sort((a, b) => a.domain.localeCompare(b.domain) || a.title.localeCompare(b.title)); + +const statusOrder = ["done", "in progress", "blocked", "todo"]; +const statusColors = ["#4caf50", "steelblue", "#ff7043", "#e0e0e0"]; + +const taskRows = openWs.flatMap(w => [ + {label: w.title, domain: w.domain, status: "done", count: w.done}, + {label: w.title, domain: w.domain, status: "in progress", count: w.in_progress}, + {label: w.title, domain: w.domain, status: "blocked", count: w.blocked}, + {label: w.title, domain: w.domain, status: "todo", count: w.todo}, +]).filter(d => d.count > 0); + +// y-axis shows domain (only for the first workstream in each domain group) +const yLabels = {}; +const _seenDomains = new Set(); +for (const w of openWs) { + yLabels[w.title] = _seenDomains.has(w.domain) ? "" : w.domain; + _seenDomains.add(w.domain); +} + +if (openWs.length === 0) { + display(html`

No open workstreams.

`); +} else { + display(Plot.plot({ + y: {label: null, tickSize: 0, domain: openWs.map(w => w.title), tickFormat: t => yLabels[t] ?? ""}, + x: {label: "Tasks", grid: true}, + color: {domain: statusOrder, range: statusColors, legend: true}, + marks: [ + Plot.barX(taskRows, {y: "label", x: "count", fill: "status", tip: true}), + // Workstream title inside the bar + Plot.text(openWs.filter(w => w.total > 0), { + y: "title", x: 0, dx: 6, + text: d => d.title.length > 36 ? d.title.slice(0, 34) + "…" : d.title, + textAnchor: "start", fontSize: 10, fill: "#333", + }), + Plot.text(openWs.filter(w => w.total === 0), { + y: "title", x: 0, dx: 6, + text: d => `${d.title.length > 24 ? d.title.slice(0, 22) + "…" : d.title} — no tasks yet`, + textAnchor: "start", fontSize: 10, fill: "#aaa", + }), + // "done / total" label after the bar + Plot.text(openWs.filter(w => w.total > 0), { + y: "title", x: "total", + text: d => ` ${d.done}/${d.total}`, + dx: 4, textAnchor: "start", fontSize: 11, fill: "gray", + }), + Plot.ruleX([0]), + ], + marginLeft: 160, + marginRight: 70, + height: Math.max(80, openWs.length * 44 + 50), + width: 700, + })); +} +``` + ## Status ```js @@ -194,77 +265,6 @@ if (regs.length === 0) { } ``` -## Open Workstreams by Domain - -```js -import * as Plot from "npm:@observablehq/plot"; - -const topicById = Object.fromEntries((summary.topics ?? []).map(t => [t.id, t.domain])); - -const openWs = (summary.open_workstreams ?? []).map(w => ({ - title: w.title, - domain: topicById[w.topic_id] ?? "unknown", - done: w.tasks_done ?? 0, - in_progress: w.tasks_in_progress ?? 0, - blocked: w.tasks_blocked ?? 0, - todo: w.tasks_todo ?? 0, - total: w.tasks_total ?? 0, -})).sort((a, b) => a.domain.localeCompare(b.domain) || a.title.localeCompare(b.title)); - -const statusOrder = ["done", "in progress", "blocked", "todo"]; -const statusColors = ["#4caf50", "steelblue", "#ff7043", "#e0e0e0"]; - -const taskRows = openWs.flatMap(w => [ - {label: w.title, domain: w.domain, status: "done", count: w.done}, - {label: w.title, domain: w.domain, status: "in progress", count: w.in_progress}, - {label: w.title, domain: w.domain, status: "blocked", count: w.blocked}, - {label: w.title, domain: w.domain, status: "todo", count: w.todo}, -]).filter(d => d.count > 0); - -// y-axis shows domain (only for the first workstream in each domain group) -const yLabels = {}; -const _seenDomains = new Set(); -for (const w of openWs) { - yLabels[w.title] = _seenDomains.has(w.domain) ? "" : w.domain; - _seenDomains.add(w.domain); -} - -if (openWs.length === 0) { - display(html`

No open workstreams.

`); -} else { - display(Plot.plot({ - y: {label: null, tickSize: 0, domain: openWs.map(w => w.title), tickFormat: t => yLabels[t] ?? ""}, - x: {label: "Tasks", grid: true}, - color: {domain: statusOrder, range: statusColors, legend: true}, - marks: [ - Plot.barX(taskRows, {y: "label", x: "count", fill: "status", tip: true}), - // Workstream title inside the bar - Plot.text(openWs.filter(w => w.total > 0), { - y: "title", x: 0, dx: 6, - text: d => d.title.length > 36 ? d.title.slice(0, 34) + "…" : d.title, - textAnchor: "start", fontSize: 10, fill: "#333", - }), - Plot.text(openWs.filter(w => w.total === 0), { - y: "title", x: 0, dx: 6, - text: d => `${d.title.length > 24 ? d.title.slice(0, 22) + "…" : d.title} — no tasks yet`, - textAnchor: "start", fontSize: 10, fill: "#aaa", - }), - // "done / total" label after the bar - Plot.text(openWs.filter(w => w.total > 0), { - y: "title", x: "total", - text: d => ` ${d.done}/${d.total}`, - dx: 4, textAnchor: "start", fontSize: 11, fill: "gray", - }), - Plot.ruleX([0]), - ], - marginLeft: 160, - marginRight: 70, - height: Math.max(80, openWs.length * 44 + 50), - width: 700, - })); -} -``` - ```js // Registered domains with no workstreams yet — show a getting-started hint const regs = regsState ?? [];