{% extends "brickwork/shell/app.html" %} {% comment %} An analysis dashboard: one question answered at the top, the shape of the answer over time beneath it, then the breakdowns that explain it. COPY THIS FILE into your project and edit it. It is not on the template loader path, so you cannot extend it (ADR-056). What your view must supply: headline_tiles the weighted scorecard grid (see the shape note below) channel_rows ranked rows: label / amount / value dicts region_rows ranked rows: label / amount / value dicts nav_items / nav_active as in app/list.html Optional when you wire a charting library: revenue_mount a {% templatetag openblock %} bw_chart_mount {% templatetag closeblock %} result, marked safe The default copy below uses the chart card's empty state so the page still reads as designed before an engine is connected. Swap empty= for mount= when you have one. Every heading, caption and judgement below is typed into the template, which is where you change them. WHY THIS PAGE IS SHAPED THIS WAY, since it is the part worth copying: A dashboard is not a list of numbers. It is an argument, and an argument has an order. This page reads top to bottom as one: the headline says what happened, the chart shows the shape of it, the breakdowns say where it came from, and the gauge says whether we are on track. A reader who stops after the first band has still learned the answer. That ordering, not the choice of components, is what makes a dashboard readable. So the first band is NOT four equal tiles. Equal tiles say every number matters equally, which is never true, and a reader scanning them has to work out the ranking themselves. The scorecard's span= carries that ranking instead: the metric this dashboard exists to report gets span=2 and is read first, and the supporting metrics take one column each. Change which tile is wide and you have changed what the dashboard is about, which is the right amount of effort for that decision. THE SCORECARD'S SHAPE, and why it needs your view: _scorecard.html takes items as a list of dicts, each {content, span}, where content is already-rendered, already-safe markup. A Django template cannot build a list of dicts and cannot capture an {% templatetag openblock %} include {% templatetag closeblock %} into a variable, so this one grid genuinely has to come from Python. Build it in your view: from django.template.loader import render_to_string from django.utils.safestring import mark_safe headline_tiles = [ {"content": mark_safe(render_to_string( "brickwork/components/_stat_comparison.html", {"label": "Net revenue", "current": "GBP 1.24m", "previous": "GBP 1.08m", "period_label": "same quarter last year", "trend": "up", "trend_label": "15% higher", "size": "lg"})), "span": 2}, ... ] The grid is 1 column on a phone, 2 at sm and 4 at lg, so a span=2 tile is full width on a phone, full width at sm, and half the row at lg. Spans read as designed at lg and clamp gracefully below it; there is nothing to write for the narrow case. ON TREND, and this is the trap worth reading twice: trend states the DIRECTION THE NUMBER MOVED, never whether that is good news. _trend_indicator.html renders trend="down" as a down arrow plus the hidden word "decreased", and you cannot override that word. A cost that fell moved DOWN, so it takes trend="down" even though falling is what you wanted; the judgement goes in trend_label, in words ("18% lower, which is the target"). Get this backwards and a screen reader announces the opposite of what a sighted reader sees. No gate in this package catches it: not axe, not the render tests, not a visual check. Only reading it does. States: the scorecard has no states of its own, each tile carries its own (see _stat.html and _stat_comparison.html's own headers); the chart card's loading/error/empty/populated states, which are mutually exclusive and resolve in that order (see _chart_card.html's own header); each ranked list's own loading/empty states; the gauge's threshold bands. Accessibility: inherits shell/app.html's skip link, sidebar/drawer nav and page-header region. Every number is visible text: the chart is a mount whose accessible name is supplied at the call site, the sparkline and the ranked bars are aria-hidden decoration reinforcing figures stated in words beside them, and the gauge always renders its own numeric reading. No reading on this page rides on colour or on a shape alone. Covered by the archetype harness's full gate sweep (render, axe WCAG 2.2 AA, no horizontal overflow, light/dark distinctness, skip-link first-tab-stop with JS disabled) at every W0.1 breakpoint, both themes. Responsive: no breakpoint switch of its own; inherits shell/app.html's sidebar-to-drawer collapse at --bw-breakpoint-md (48rem). The scorecard steps 1 to 2 to 4 columns at its own sm and lg breakpoints and clamps wide spans automatically; the ranked lists and chart card reflow inside whatever column they land in. {% endcomment %} {% load brickwork_components brickwork_nav %} {% block page_title %}Revenue analysis - Northwind{% endblock %} {% block sidebar %}{% bw_nav nav_items nav_active %}{% endblock %} {% block sidebar_mobile %}{% bw_nav nav_items nav_active %}{% endblock %} {% block brand_wordmark %}Northwind{% endblock %} {% block page_header %} {% include "brickwork/components/_page_header.html" with title="Revenue analysis" description="Q3 2026 against the same quarter last year. Figures settle 48 hours after period close." %} {% endblock %} {% block page_actions %} {% bw_button "Export" variant="secondary" icon="download" href="/analysis/export/" %} {% endblock %} {% block content %}
{% comment %} BAND 1, the answer. One weighted grid, not a row of equal tiles. The wide tile is a comparison (this period against last), because the question this dashboard answers is "how are we doing against last year", and a bare number cannot answer a comparative question. The three supporting tiles are ordinary stats, deliberately quieter. The heading is visible via .bw-band-heading rather than hidden: _scorecard.html is view-only tiles with no title mechanism of its own (ADR-090 decision 4), so nothing else on the page carries "Headline figures" for a reader to see, and hiding it would leave the page's opening band unlabelled to anyone but a screen reader user. {% endcomment %}

Headline figures

{% include "brickwork/components/_scorecard.html" with items=headline_tiles %}
{% comment %} BAND 2, the shape of the answer over time. This leads the body of the page rather than sitting under a wall of counts, because a trend line answers "is this getting better" faster than any number does. The default below uses empty=True so the card's designed empty state shows before an engine is wired (icvoss/django-brickwork#509). When you have a library, drop empty= and pass mount=revenue_mount instead: revenue_mount = render_to_string(...) or capture the tag: {% templatetag openblock %} bw_chart_mount aria_label="Net revenue by month, Q3 2026" as revenue_mount {% templatetag closeblock %} The accessible name is REQUIRED unless you pass decorative=True, and a chart carrying the page's main finding is never decorative. NOTE the card title, which used to be a trap and no longer is. _chart_card.html EXTENDS _card.html and does not redeclare the title block, so it inherits _card.html's dual consumption on that one region (icvoss/django-brickwork#398, ADR-090 decision 3): title="..." on the include below now renders the same

a caller filling {% templatetag openblock %} block title {% templatetag closeblock %} by hand would write. A caller who instead extends the template and fills the block still wins, since block override takes precedence by construction; nothing here changes that path. The band's own

stays visually hidden here even so, because this band's title is genuinely carried by the card rather than by the band: one card, one chart, one title, and duplicating it as a second visible heading immediately above the card would read as two headings for one thing. The band-level

exists only to give the
its accessible name via aria-labelledby; give the CARD the visible title with title="Net revenue by month" below, and it, not the hidden band heading, is what a sighted reader sees. {% endcomment %}

Net revenue by month

{% include "brickwork/components/_chart_card.html" with empty=True empty_heading="No chart yet" empty_body="Connect a charting library to plot net revenue by month, then pass its mount markup as mount=." empty_action_href="/docs/" empty_action_label="Read the chart contract" title="Net revenue by month" %}
{% comment %} BAND 3, where the answer came from. Two ranked lists side by side IS the natural arrangement here, and .bw-band-grid--2 is the shipped route to it (ADR-090 decision 1): the app family's own N-column primitive, content-driven rather than breakpoint-driven, so the pair reflows to one column on its own once a list no longer fits at 24rem rather than waiting for a viewport breakpoint. Do not reach for bw-feature-grid--2 or any other class marketing.css defines instead: it works mechanically in the same compiled bundle, but it is marketing-family, styled for marketing rhythm, and tests/test_family_boundary.py fails a build that uses it on an app-family shell (ADR-090 decision 2). The band gets a real, visible heading via .bw-band-heading rather than the hidden-

-plus-aria-labelledby pattern the other bands use: unlike the scorecard or the chart card, no single component here carries a title of its own for "Breakdowns" to duplicate, so hiding the heading would leave the band genuinely unlabelled to a sighted reader. .bw-band-heading is sized at the heading-lg role with no inline-size rule of its own, so it reads at a proper heading size without narrowing the band the way wrapping it in bw-prose would (bw-prose's first rule is a 65ch content measure, not a heading utility). The id/aria-labelledby wiring is unchanged: a visible heading still needs both, exactly as a hidden one does. Each list states its own figures as text beside the bar; the bars are aria-hidden decoration, so the ranking is readable without seeing them. {% endcomment %}

Breakdowns

{% bw_ranked_list channel_rows basis="total" label="Revenue by channel" empty_heading="No channel data" empty_body="Revenue attributed to a channel will appear here once the quarter opens." %} {% bw_ranked_list region_rows basis="total" label="Revenue by region" empty_heading="No regional data" empty_body="Revenue attributed to a region will appear here once the quarter opens." %}
{% comment %} BAND 4, are we on track. The gauge closes the argument the headline opened: the first band said what happened, this one says what it means against a target. Threshold bands tint the arc, but the percentage always renders as visible text underneath, so the reading never depends on seeing the colour. The heading is visible via .bw-band-heading, matching bands 1 and 3: the gauge's own label= only sets the SVG's aria-label, so nothing on this band carries a visible caption for a reader who is not using a screen reader. {% endcomment %}

Progress against target

{% bw_gauge value=78 min=0 max=100 label="Progress to annual revenue target" size="lg" threshold_bands=target_bands %}

{% endblock %}