Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions api/routers/insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ class DashboardResponse(BaseModel):
total_lines_of_code: int
avg_quality_score: float | None
coverage_percent: float
# Number of supported libraries — the denominator behind `coverage_percent`
# and the per-spec coverage cells. Served so clients never hardcode it.
total_libraries: int

library_stats: list[LibraryDashboardStats]
coverage_matrix: list[CoverageRow]
Expand Down Expand Up @@ -386,6 +389,7 @@ async def _build_dashboard(repo: SpecRepository, impl_repo: ImplRepository) -> D
total_lines_of_code=total_loc,
avg_quality_score=round(sum(all_scores) / len(all_scores), 1) if all_scores else None,
coverage_percent=round(coverage, 1),
total_libraries=len(SUPPORTED_LIBRARIES),
library_stats=lib_stats,
coverage_matrix=coverage_rows,
top_implementations=top_impls,
Expand Down
95 changes: 95 additions & 0 deletions app/src/pages/StatsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,35 @@ vi.mock('src/hooks', () => ({
}),
}));

// Mirrors core.constants.SUPPORTED_LIBRARIES (15 entries).
const ALL_LIBRARY_IDS = [
'altair',
'bokeh',
'chartjs',
'd3',
'echarts',
'ggplot2',
'highcharts',
'letsplot',
'makie',
'matplotlib',
'muix',
'plotly',
'plotnine',
'pygal',
'seaborn',
];

const mockDashboard = {
total_specs: 142,
total_implementations: 987,
total_interactive: 53,
total_lines_of_code: 245_600,
avg_quality_score: 82.5,
coverage_percent: 73,
// Denominator of the coverage matrix — served by the API, never hardcoded
// in the page (it used to be a stale literal 9).
total_libraries: 15,
library_stats: [
{
id: 'matplotlib',
Expand Down Expand Up @@ -79,6 +101,12 @@ const mockDashboard = {
plotly: { score: null, has_impl: false },
},
},
// A fully covered spec — 15/15, the state the matrix renders as solid green.
{
spec_id: 'line-basic',
title: 'Basic Line Plot',
libraries: Object.fromEntries(ALL_LIBRARY_IDS.map(id => [id, { score: 90, has_impl: true }])),
},
],
top_implementations: [
{
Expand Down Expand Up @@ -229,6 +257,73 @@ describe('StatsPage', () => {
expect(screen.getByText('73%')).toBeInTheDocument();
});

it('scales the coverage matrix by total_libraries, not a hardcoded count', async () => {
mockFetchSuccess();

render(<StatsPage />);

await waitFor(() => {
expect(screen.getByText('specifications')).toBeInTheDocument();
});

// total_specs (142) * total_libraries (15) = 2130 possible implementations,
// and one of the two matrix rows is short of full coverage.
expect(screen.getByText(/987 of 2,?130 possible/)).toBeInTheDocument();
expect(screen.getByText(/1 below 15\/15/)).toBeInTheDocument();
expect(screen.getByText('complete (15/15)')).toBeInTheDocument();
});

it('labels each coverage cell with its count out of the full library set', async () => {
mockFetchSuccess();

render(<StatsPage />);

await waitFor(() => {
expect(screen.getByLabelText('Basic Scatter Plot: 1/15')).toBeInTheDocument();
});

expect(screen.getByLabelText('Basic Line Plot: 15/15')).toBeInTheDocument();
});

it('falls back to library_stats when an older payload omits total_libraries', async () => {
// Frontend-before-API deploy: the cached/older dashboard has no
// total_libraries, so the denominator has to come from library_stats.
const legacyDashboard: Record<string, unknown> = {
...mockDashboard,
library_stats: ALL_LIBRARY_IDS.map(id => ({
id,
name: id,
impl_count: 10,
avg_score: 85,
min_score: 60,
max_score: 98,
score_buckets: { '85-90': 5 },
loc_buckets: { '40-60': 5 },
avg_loc: 78,
})),
};
delete legacyDashboard.total_libraries;
vi.stubGlobal(
'fetch',
vi.fn().mockImplementation((url: string) => {
if (url.includes('/insights/visitors')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({ points: [] }) });
}
return Promise.resolve({ ok: true, json: () => Promise.resolve(legacyDashboard) });
})
);

render(<StatsPage />);

await waitFor(() => {
expect(screen.getByLabelText('Basic Scatter Plot: 1/15')).toBeInTheDocument();
});

expect(screen.getByLabelText('Basic Line Plot: 15/15')).toBeInTheDocument();
expect(screen.getByText(/987 of 2,?130 possible/)).toBeInTheDocument();
expect(screen.getByText(/1 below 15\/15/)).toBeInTheDocument();
});

it('renders top implementation cards', async () => {
mockFetchSuccess();

Expand Down
158 changes: 102 additions & 56 deletions app/src/pages/StatsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ interface DashboardData {
total_lines_of_code: number;
avg_quality_score: number | null;
coverage_percent: number;
total_libraries: number;
library_stats: LibraryStats[];
coverage_matrix: CoverageRow[];
top_implementations: TopImpl[];
Expand All @@ -99,6 +100,33 @@ function formatNum(n: number): string {
return n.toLocaleString();
}

// Coverage matrix geometry: a 16px visual mark centred in a 24px hit area, so
// every cell is a WCAG 2.2 SC 2.5.8 sized target (24x24, non-overlapping)
// while the marks themselves stay a dense strip rather than a chunky grid.
const COVERAGE_MARK_PX = 16;
const COVERAGE_TARGET_PX = 24;

/**
* Per-cell styling of the coverage matrix: complete specs read as a solid
* brand-green block, anything short of full coverage carries an ink outline
* (dashed when nothing is implemented yet) so the (few) incomplete specs are
* the ones that stand out. The stroke is ink rather than amber because amber
* clears neither WCAG 1.4.11 on the cream background nor the style guide's
* light-bg caveat (`docs/reference/style-guide.md`, "Light-bg WCAG caveat"),
* and here the stroke is the only thing marking a state.
*/
function coverageCellStyle(count: number, total: number): Record<string, string> {
if (count >= total) return { backgroundColor: colors.success, border: '1px solid transparent' };
if (count === 0)
return { backgroundColor: 'var(--bg-elevated)', border: '1px dashed var(--ink)' };
// brand green (#009E73) scaled by how much of the library set is covered
const intensity = total > 0 ? count / total : 0;
return {
backgroundColor: `rgba(0, 158, 115, ${0.2 + intensity * 0.6})`,
border: '1px solid var(--ink)',
};
}

// Shorter labels for the fixed-width (80px) library column so every row stays
// on a single line. The API's canonical names ("Apache ECharts", "MUI X
// Charts") wrap to two lines here; we keep the full names elsewhere (Libraries
Expand Down Expand Up @@ -160,6 +188,16 @@ export function StatsPage() {
</Box>
);

// Coverage denominator comes from the API (the canonical supported-library
// count); library_stats is the fallback for a stale/cached payload so the
// matrix never silently reverts to a hardcoded number.
const libCount = data.total_libraries || data.library_stats.length || 1;
Comment thread
MarkusNeusinger marked this conversation as resolved.
const coverageCells = (data.coverage_matrix ?? []).map(row => ({
row,
count: Object.values(row.libraries).filter(c => c.has_impl).length,
}));
const incompleteSpecs = coverageCells.filter(c => c.count < libCount).length;

const dailyImpls = data.daily_impls ?? [];
const maxDaily = Math.max(...dailyImpls.map(d => d.count), 1);
const visitorPoints = visitors ?? [];
Expand Down Expand Up @@ -510,65 +548,73 @@ export function StatsPage() {
mb: 1,
}}
>
{data.coverage_percent}% · {data.total_implementations} of {data.total_specs * 9} possible
{data.coverage_percent}% · {data.total_implementations} of {data.total_specs * libCount}{' '}
possible
{incompleteSpecs > 0 && ` · ${incompleteSpecs} below ${libCount}/${libCount}`}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '2px' }}>
{data.coverage_matrix.map(row => {
const count = Object.values(row.libraries).filter(c => c.has_impl).length;
const intensity = count / 9;
return (
<Tooltip key={row.spec_id} title={`${row.title}: ${count}/9`} arrow>
<Link
component={RouterLink}
to={specPath(row.spec_id)}
sx={{
display: 'block',
width: 10,
height: 10,
borderRadius: '2px',
bgcolor:
count === 0
? 'var(--bg-elevated)'
: // brand green (#009E73) — was an off-palette Tailwind green
`rgba(0, 158, 115, ${0.15 + intensity * 0.7})`,
textDecoration: 'none',
'&:hover': { outline: `1px solid ${colors.success}` },
}}
/>
</Tooltip>
);
})}
{/* Each link fills a 24px square (SC 2.5.8) and paints the 16px mark
with its own background, so the visible strip stays dense while the
tap target is the full square. */}
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
{coverageCells.map(({ row, count }) => (
<Tooltip key={row.spec_id} title={`${row.title}: ${count}/${libCount}`} arrow>
<Link
component={RouterLink}
to={specPath(row.spec_id)}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: COVERAGE_TARGET_PX,
height: COVERAGE_TARGET_PX,
textDecoration: 'none',
'& > span': {
width: COVERAGE_MARK_PX,
height: COVERAGE_MARK_PX,
borderRadius: '3px',
boxSizing: 'border-box',
...coverageCellStyle(count, libCount),
},
'&:hover > span': {
outline: `2px solid ${colors.success}`,
outlineOffset: '1px',
},
}}
>
<span />
</Link>
</Tooltip>
))}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.micro,
color: semanticColors.mutedText,
}}
>
less
</Typography>
{[0, 0.25, 0.5, 0.75, 1].map(v => (
<Box
key={v}
sx={{
width: 8,
height: 8,
borderRadius: '1px',
bgcolor: `rgba(0, 158, 115, ${0.15 + v * 0.7})`,
}}
/>
{/* Legend — three states, not a gradient: the interesting signal is
"which specs are NOT complete", and those are the minority. */}
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 1.5, mt: 0.75 }}>
{[
{ label: `complete (${libCount}/${libCount})`, count: libCount },
{ label: 'partial', count: Math.max(libCount - 1, 1) },
{ label: 'none', count: 0 },
].map(({ label, count }) => (
<Box key={label} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Box
sx={{
width: COVERAGE_MARK_PX,
height: COVERAGE_MARK_PX,
borderRadius: '3px',
boxSizing: 'border-box',
...coverageCellStyle(count, libCount),
}}
/>
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.micro,
color: semanticColors.mutedText,
}}
>
{label}
</Typography>
</Box>
))}
<Typography
sx={{
fontFamily: typography.fontFamily,
fontSize: fontSize.micro,
color: semanticColors.mutedText,
}}
>
more
</Typography>
</Box>

{/* Timeline — daily implementation updates over the last 28 days.
Expand Down
21 changes: 21 additions & 0 deletions changelog.d/stats-coverage-denominator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
### Fixed

- **Coverage matrix on the stats page counted against 9 libraries instead of
15.** Every cell tooltip read `15/9` for a fully covered spec, and the
"possible implementations" total was short by the same factor, because the
page carried a hardcoded library count from back when nine were supported.
`/insights/dashboard` now serves `total_libraries` (the canonical
`SUPPORTED_LIBRARIES` size that already backs `coverage_percent`) and the page
renders against it, so the denominator follows the library set instead of
drifting from it.

### Changed

- **Coverage cells read as three states, not a gradient.** The mark grew from
10 px to 16 px inside a 24 px hit area (WCAG 2.2 SC 2.5.8), and full coverage
now renders as solid brand green while anything short of it carries an ink
outline (dashed when a spec has no implementation at all), with a labelled
legend replacing the less/more ramp.
The interesting signal is which specs are *not* complete, and those are the
minority — the old opacity ramp made them the hardest cells to pick out. The
summary line also names how many specs are below full coverage.
3 changes: 3 additions & 0 deletions tests/unit/api/test_routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2135,6 +2135,9 @@ def test_dashboard_with_db(self, client: TestClient, mock_spec) -> None:
assert data["total_lines_of_code"] == 500
assert data["total_interactive"] == 0
assert len(data["library_stats"]) == len(SUPPORTED_LIBRARIES)
# Denominator behind coverage_percent — the stats page renders the
# matrix against this instead of hardcoding a library count.
assert data["total_libraries"] == len(SUPPORTED_LIBRARIES)
assert isinstance(data["coverage_matrix"], list)
assert isinstance(data["score_distribution"], dict)
assert isinstance(data["tag_distribution"], dict)
Expand Down
Loading