4.6 Velocity + burndown charts
The two charts that make iteration measurable — the analytics half of Jira's Scrum feature set: an in-sprint burndown (how fast the committed work is being completed) and a cross-sprint velocity (how much a team reliably completes per sprint, the planning forecast). Both are pure read surfaces over data Stories 4.1 / 4.3 / 4.4 already ship — this story adds NO new write model and NO migration (the stub: "Reads the sprint + points data; no new write model"). It wires the in-sprint burndown into the chart seam Story 4.5 left in the scrum header and the completed-sprint burndown + velocity into the chart seam Story 4.4.6 left in the sprint report.
Burndown (in-sprint). Remaining work on the Y axis (the configured estimation statistic — story points by default, falling back to issue count when unestimated, exactly as 4.3 parameterises its roll-ups), sprint days on the X axis. Two series, the Jira-faithful pair (decision-ladder rung 1): the guideline — an ideal straight line from the committed baseline at sprint start down to 0 at sprint end — and the actual remaining — a step line that DROPS as issues reach a done-category status and RISES when scope is added mid-sprint. A live (active) sprint draws the actual line up to "today"; a completed sprint draws it to completedAt. The load-bearing piece is the data, not the pixels: the actual line is a function of time, and the only record of when each point burned down (or scope changed) is the 1.4.6 work_item_revision trail — the status transitions into/out of a done-category status and the sprint-association add/remove events. 4.6 reads that trail, bucketed by day, to reconstruct "remaining at the end of each day".
Velocity (cross-sprint). A grouped bar chart over the last N completed sprints (N default 7, the Jira default): per sprint a committed bar (the locked committedPoints baseline from 4.4.2) and a completed bar (rollupForSprint().completed from 4.3.3 — the same done-category aggregate the scrum header uses), plus an average completed readout (the planning forecast: "your average velocity is 24"). It enumerates the completed-sprint history completeSprint (4.4.3) produces.
Bounded, never load-all (finding #57 — the real-product scale axis). Neither chart loads a row set whose size grows with the team's data. Velocity reads the last N completed sprints (a LIMIT N over the sprint table) and, per sprint, ONE stored baseline figure + ONE bounded rollupForSprint aggregate — it never iterates every issue of every sprint. Burndown's day series is bounded by sprint LENGTH (~10–14 days), and the underlying "what happened each day" is a grouped aggregate over the revision rows scoped to the sprint window and the sprint's issues (a $queryRaw that GROUPs revision events by calendar day server-side), NOT a load of every revision row into Node followed by a client reduce. A burndown that fetched all revisions and summed them in JS would be prototype-thinking; the day buckets come from the database. This is the same discipline 4.3 / 4.4 / 4.5 applied to their roll-ups and projections.
The reusable chart primitive (the "viz from Epic 4" Story 6.3 reuses). The repo has no charting library and the design system routes every colour through --el-* and every shape through element-semantic tokens (CLAUDE.md). A third-party chart lib bypasses that swap layer and ships disproportionate weight for two charts, so — per the no-shortcut / justified-deviation rule — 4.6.2 builds a small token-aware SVG chart primitive in components/ui/ (a line/area chart, a grouped bar chart, and the shared axis / gridline / legend scaffolding). It is a11y-first: a visible text legend, axis labels, and a <table> data fallback / aria-describedby summary so the chart is conveyed as TEXT and number, never colour or shape alone (finding #35). Epic 6.3 (dashboards & reports — "Charts reuse the viz from Epic 4") consumes this primitive, so it is planned as a reusable building block, not a one-off.
Where each chart mounts (no new navigation surface invented). The in-sprint burndown mounts in the scrum header chart seam (Story 4.5 reserved it — the header shows numeric remaining + an empty chart slot). The completed-sprint burndown + the velocity chart mount in the sprint report chart seam (Story 4.4.6 reserved it). 4.6 does NOT build a new "Reports" nav area, a dashboard, or a board-level reports tab — that is Epic 6.3, which reuses this story's primitive. 4.6 fills the two seams its sibling stories already left, and the design (4.6.1) specifies the exact placement + sizing in each.
Completeness — the real-product states (planned whole, not just the happy path). Unestimated sprint/issues: by-points the chart degrades to the issue-count statistic (Jira does the same) or shows the "—/no point data" state, never NaN or a broken axis. Too little history for velocity (0–1 completed sprints): a "not enough history yet" state, not an axis-of-one. Active vs. completed sprint: the burndown draws the actual line to "now" vs. to completedAt. Loading / error: the chart slot reuses the surrounding surface's skeleton + ErrorState. Empty sprint (no issues): a flat guideline at 0 / empty state. Each is drawn by 4.6.1 and asserted in 4.6.7.
Out of scope (siblings / Epic 6 — kept deliberately narrow): the numeric points summary + per-column totals (Stories 4.3 / 4.5 — the charts sit BESIDE them, do not replace them); the sprint-report lists / carry-over / scope-change line (Story 4.4.6 — 4.6 only fills its chart seam); a standalone Reports / dashboards navigation surface, configurable widgets, and the created-vs-resolved / status-distribution reports (Story 6.3, which reuses 4.6.2's primitive); a cumulative-flow diagram and other Jira reports beyond velocity + burndown (no use case yet — no complexity for nothing); working-days / non-working-day shading on the guideline (the project has no working-days calendar config yet — the guideline uses calendar days, noted as a future refinement when a calendar lands); per-user or epic burndown variants; exporting a chart to image/CSV. The at-scale combined Scrum journey that also exercises these charts on a large sprint is Story 4.7 (the Scrum analogue of 3.5), not duplicated here.
Verification
- Pull the Story branch,
pnpm install,pnpm prisma migrate dev(no 4.6 migration — the charts are read-only over the Story 4.1 sprint schema + 4.3 story points + the 1.4.6 revision trail;migrate devreports "No difference detected"),pnpm db:seed,pnpm dev. (Requires the sibling chart seams — Story 4.5.3 scrum header + Story 4.4.6 sprint report — merged so there is a slot to mount into, and at least one completed sprint with estimated, completed issues to chart.) pnpm test:coverage— Vitest (real Postgres, no mocks exceptgetSession) over the burndown series derivation + the velocity aggregate stays ≥90% per-file branch/fn/line on the newreportsService/ revision-aggregate repo files (the CI coverage gate,motir-core-coverage-gate); any new repo method has a direct empty-input-guard test.- Burndown data check:
getBurndownSeries(sprintId)returns the guideline (committed → 0 across the sprint window) and the actual stepped remaining series reconstructed from the 1.4.6 revision trail — the actual line drops on the day an issue reached adone-category status and rises on the day scope was added; the figures match the 4.3.3rollupForSprintremaining at "now"; an unestimated sprint degrades to issue count (or the "no point data" state), neverNaN. - Velocity data check:
getVelocityreturns the last N completed sprints with committed (the locked 4.4.2 baseline) vs completed (4.3.3rollupForSprint) and the average; a project with 0–1 completed sprints returns the low-history state; the read is a boundedLIMIT N, not all sprints. - Chart render check: sign in as
zhuyue@motir.co/!QAZ1qaz, open a project with an active scrum sprint →/boards: the scrum header shows the in-sprint burndown in the reserved chart slot (guideline + actual line) beside the numeric remaining; complete the sprint (or open a completed sprint's report) → the sprint report shows the completed-sprint burndown AND the velocity bar chart (committed vs completed per sprint + average). The layout matchesdesign/reports/charts.mock.html. - Bounded-scale check (finding #57):
pnpm db:seed:large(a long sprint with many issues + many completed sprints) → the burndown comes from a grouped day-aggregate over the revision rows (not an all-revisions load), the velocity is aLIMIT Nread (not all sprints), and both render in bounded time with a bounded DOM. pnpm test:e2e --grep charts(or--grep burndown) — Playwright over the real stack: an active sprint shows the live burndown in the scrum header; completing a sprint shows the completed-sprint burndown + the velocity chart in the report; the low-history + unestimated states render without errors.- a11y check: each chart exposes a visible text legend + axis labels and a data-table (or
aria-describedby) fallback, so the series are read by assistive tech as text+number — never colour or shape alone (finding #35); colour via--el-*, shape via element shape tokens (no Tier-0--color-*/ rawrounded-*).