Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/printing.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,55 @@ after the tab's data has loaded; it excludes the initial dashboard load.
The timeout is a bounded wait with an explicit retry path, not a guarantee that
every device or connection finishes within 15 seconds. Safari native printing
remains unverified, and Firefox page numbering still needs its native print setting.

## Port to the interactive dashboard (27 September 2026)

The shadcn/ui dashboard (#490) replaced the PNG galleries with interactive charts
drawn from each variant's JSON dataset, so the print feature was carried over
onto the new components rather than merged line by line. Its behaviour is
unchanged: the live tree is printed under the same transient print context, with
selections stated as text, every table row up to the 500-row cap, and provenance
on every page. What changed is how each piece is implemented:

- **Readiness.** Charts are SVG drawn from JSON, so preparation no longer decodes
images. A chart still loading (its module or its dataset) carries
`data-print-pending`; a chart whose dataset failed carries `data-print-error`
and names itself ("Could not load chart data: <title> (<variant>)"). Print tab
waits, within the same 15-second bound, until nothing is pending, then refuses
to print if anything failed. Native printing before that point prints the named
loading or error notice, never a silent gap.
- **Collapsed content.** Section groups, cards and the tab explainer are Radix
collapsibles, which unmount closed content. Each holds its open state and is
forced open while printing; the reader's own collapse returns afterwards. The
native `<details>` that remain (chart methodology) are still opened and
restored by the print controller. Group names print as headings.
- **Selections.** Variant, role and period switches are toggle groups. While
printing they stay mounted but hidden, and a line such as "Time range: 1 month"
or "Pipeline view: By month" states the choice.
- **Controls on paper.** `print.css` selects data attributes and shadcn
`data-slot`s, never utility classes. It hides the header, sidebar, dialogs,
tooltips, inputs and every button except those marked `data-print-keep`: a
chart's legend doubles as its series switch, and some table rows name
themselves with a focus button, so those print as text.
- **Slideshows** keep every slide mounted (each keeps its chosen variant) and
print them all in order; an expanded chart prints in place of its dialog.
- **Light output.** Dark mode is one Tailwind `dark` variant, now scoped to
`screen`, so paper always gets the light palette.

The component tests were ported to the new roles (toggle items are radios) and
now cover chart readiness through the pending and error markers instead of image
decoding. The Playwright suite was ported to the new DOM with real chart datasets
in its fixtures; avatar requests to GitHub, which the dashboard makes by design
(see `web/README.md`), are answered locally so the suite stays offline.

Two behaviours needed new handling. An open Radix dialog (an expanded chart or a
figure's explanation) ignores Escape and outside clicks while printing, so the
key that dismisses the browser's print dialog cannot also close it; the dialog is
a scroll box and opts in to scroll restore. An expanded chart prints in place
while its dialog keeps its own copy mounted, so its open explanation and scroll
position survive.

Local verification after the port: 167 Vitest tests, and 21 Playwright tests in
each of Chromium and Firefox. Lint, Prettier and the typecheck/build pass; lint
keeps its five existing warnings and adds none. Printed output from real data was
not re-reviewed in this pass.
7 changes: 4 additions & 3 deletions src/hiero_analytics/analysis/scorecard_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ def scorecard_to_dataframe(scorecards: list[ScorecardRecord]) -> pd.DataFrame:
)


def scorecard_stacked_dataframe(scorecards: list[ScorecardRecord]) -> pd.DataFrame:
def scorecard_stacked_dataframe(scorecards: list[ScorecardRecord], missing: float | None = 0.0) -> pd.DataFrame:
"""
Convert ScorecardRecord list into a dataframe with checks as columns.

Missing checks are filled with 0.
Checks a scorecard does not report are filled with ``missing``: 0 for the
stacked chart, None where "not reported" must stay distinct from a zero.
"""
if not scorecards:
return pd.DataFrame(columns=["repo", "score", "date", *CHECK_COLUMNS])
Expand All @@ -52,7 +53,7 @@ def scorecard_stacked_dataframe(scorecards: list[ScorecardRecord]) -> pd.DataFra
"repo": s.repo,
"score": s.score,
"date": s.date,
**{check: checks.get(check, 0.0) for check in CHECK_COLUMNS},
**{check: checks.get(check, missing) for check in CHECK_COLUMNS},
}
)

Expand Down
10 changes: 9 additions & 1 deletion src/hiero_analytics/dashboard_spec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"MACRO_GLOSSARIES",
"MACRO_GROUP_ORDER",
"MACRO_PARENTS",
"MACRO_SUMMARIES",
"METRIC_ANNOTATIONS",
"PROJECT_ISSUES_URL",
"TABLE_FAMILIES",
Expand All @@ -54,7 +55,9 @@
# The frontend implements exactly these (web/src/components/FormattedCell.tsx);
# an unlisted value would fall through to plain text, so a typo is caught by
# tests/dashboard_spec instead of shipping as a silently unformatted column.
COLUMN_FORMATS = frozenset({"hip", "date", "link", "evidence", "status", "flag", "presence", "number", "staleness"})
COLUMN_FORMATS = frozenset(
{"hip", "date", "link", "evidence", "status", "flag", "presence", "number", "percent", "staleness"}
)

# The families that carry table sections, keyed by their macro name — the
# dashboard pipeline reads SECTION_SPECS / SECTION_ORDER / SECTION_GROUP_OF
Expand Down Expand Up @@ -89,6 +92,11 @@
}


# The one-line purpose shown under each tab's title. Every family declares one,
# so a new tab cannot ship with the frontend's generic fallback by accident.
MACRO_SUMMARIES = {family.CHART_MACRO["name"]: family.SUMMARY for family in _FAMILIES}


def _group_order(family) -> list[str]:
"""The family's section-group display order, chart-only groups included.

Expand Down
6 changes: 6 additions & 0 deletions src/hiero_analytics/dashboard_spec/community.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

from __future__ import annotations

from hiero_analytics.dashboard_spec.interactive import DISCORD_SOURCES

# One line under the tab's title saying what the tab is for.
SUMMARY = "Discover how the Hiero community connects and grows."

# Shown when the selected org has no content for this tab.
ABSENT_NOTE = (
"Discord serves the whole Hiero ecosystem, so community activity is published "
Expand All @@ -17,6 +22,7 @@
"hiero-ledger": [
{
"id": "discord",
"interactive_sources": DISCORD_SOURCES,
"group": "Discord",
"title": "Discord activity",
"description": "Discord channel categories, monthly traffic, and recent activity.",
Expand Down
34 changes: 26 additions & 8 deletions src/hiero_analytics/dashboard_spec/contributors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
from __future__ import annotations

from hiero_analytics.dashboard_spec.glossary import glossary_of
from hiero_analytics.dashboard_spec.interactive import (
ACTIVITY_HEATMAP_SOURCES,
CONTRIBUTOR_HEATMAP,
CONTRIBUTOR_NETWORK,
OVERVIEW_SOURCES,
REPO_GROWTH_SOURCES,
)

# One line under the tab's title saying what the tab is for.
SUMMARY = "Explore the people behind Hiero. Follow contribution activity, collaboration, and community growth."

CHART_MACRO = {
"name": "Contributors",
Expand All @@ -27,14 +37,15 @@
"(maintainers, committers, triage) live in the Governance tab. Click to enlarge."
),
"files": [("Repositories linked by shared contributors", "all_network.png")],
"interactive_sources": {"all_network.png": CONTRIBUTOR_NETWORK},
},
{
"id": "activity-heatmap",
"title": "Activity heatmaps",
"group": "Activity & networks",
"slideshow": True,
"description": (
"Weighted monthly activity over the last six months (greener = more active). Slide "
"Weighted monthly activity over the last six months (stronger colour = more active). Slide "
"through the same activity zooming steadily out — from each individual contributor, "
"to their governance team, to their employer, and finally to the repositories the "
"work lands in. For employer concentration and authority risk, see the Governance tab."
Expand All @@ -45,6 +56,7 @@
("By organisation", "org_activity_heatmap.png"),
("By repository", "repo_activity_heatmap.png"),
],
"interactive_sources": ACTIVITY_HEATMAP_SOURCES,
},
{
"id": "repo-growth",
Expand All @@ -59,11 +71,13 @@
("New repos per month", "repos_created_per_month.png"),
("Cumulative repo count", "cumulative_repo_count.png"),
],
"interactive_sources": REPO_GROWTH_SOURCES,
},
],
"hiero-hackers": [
{
"id": "org-overview",
"interactive_sources": OVERVIEW_SOURCES,
"title": "Organization overview (org-wide)",
"group": "Org overview",
"description": (
Expand All @@ -85,6 +99,7 @@
"linked when they share contributors. Colour = repository type. Click to enlarge."
),
"files": [("Repositories linked by shared contributors", "all_network.png")],
"interactive_sources": {"all_network.png": CONTRIBUTOR_NETWORK},
},
{
"id": "activity-heatmap",
Expand All @@ -95,6 +110,7 @@
"months (greener = more active that month)."
),
"files": [("Activity heatmap", "contributor_activity_heatmap.png")],
"interactive_sources": {"contributor_activity_heatmap.png": CONTRIBUTOR_HEATMAP},
},
{
"id": "repo-growth",
Expand All @@ -109,6 +125,7 @@
("New repos per month", "repos_created_per_month.png"),
("Cumulative repo count", "cumulative_repo_count.png"),
],
"interactive_sources": REPO_GROWTH_SOURCES,
},
],
},
Expand Down Expand Up @@ -178,21 +195,22 @@
"share contributors. Bubble colour is the repo's category; the link threshold scales with org size.",
"contributor_activity_heatmap.png": "Rows are the 25 busiest contributors over the last six months; columns are those months. The "
"colour and number in each cell are a weighted activity score (issues ×2, reviews ×3, PRs opened "
"×3, merges ×2) for that month — greener = more active, redder = less. Bots are excluded.",
"×3, merges ×2) for that month — darker blue = more active. Bots are excluded.",
"org_activity_heatmap.png": "Rows are organisations, columns are the last six months; each cell is that org's people's "
"weighted activity that month (issues ×2, reviews ×3, PRs opened ×3, merges ×2) — greener = more "
"active. It shows which employers carry the work over time, the activity counterpart to the "
"weighted activity that month (issues ×2, reviews ×3, PRs opened ×3, merges ×2) — darker blue = "
"more active. It shows which employers carry the work over time, the activity counterpart to the "
"head-count chart. Bots and contributors not mapped to an organisation are excluded.",
"team_activity_heatmap.png": "Rows are governance teams, columns are the last six months; each cell is the weighted activity of "
"the team's members that month (same weights as the other heatmaps). A contributor on several teams "
"counts toward each, so team totals overlap — this measures each team's activity, not a partition. "
"The 25 busiest teams are shown; bots are excluded.",
"repo_activity_heatmap.png": "Rows are repositories, columns are the last six months; each cell is the repo's weighted activity "
"that month (issues ×2, reviews ×3, PRs opened ×3, merges ×2) — greener = more active. Aggregated "
"that month (issues ×2, reviews ×3, PRs opened ×3, merges ×2) — darker blue = more active. Aggregated "
"straight from the events, so each counts once. The 25 busiest repositories are shown; bots are "
"excluded.",
"contributor_counts.png": "The 20 repositories with the most distinct contributors over the last six months; bar height is "
"the number of unique contributors.",
"contributor_counts.png": "Repositories ranked by distinct contributors over the last six months; bar height is the "
"number of unique contributors. The static image shows the top 20; the interactive view and CSV hold every "
"repository.",
"language_distribution.png": "How many repositories use each primary language (current snapshot). Repositories with no "
"detected language are grouped as 'Unknown'.",
"push_activity.png": "The share of repositories that received a push in the last 30 days (active) versus those that "
Expand Down Expand Up @@ -227,7 +245,7 @@
"contributor_counts.png": [
"Take the org-wide contributor activity records for the Hiero Hackers organisation.",
"Count the distinct contributors active in each repository.",
"Keep the top 20 repositories by that count.",
"Rank the repositories by that count; the static image keeps the top 20, the CSV keeps them all.",
],
"language_distribution.png": [
"List the organisation's repositories and read GitHub's primary-language field for each.",
Expand Down
Loading
Loading