feat: health score report chart type redesign IN-1304 - #2204
Conversation
Renames the report to "Open Source Health Score" (page title, meta
description, and view heading), updates the two section headers with
eyebrow tags per design feedback ("How healthy are open source
projects?" / health score distribution, "Which signals are available?"
/ health score availability), and fixes the lifecycle widget's stage
order to follow the fixed active/stable/declining/inert/abandoned/
archived/unavailable sequence instead of sorting by project count,
which caused stages to jump around as counts changed.
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Converts five report widgets to the chart types from the design feedback, adding a new donut chart config builder alongside the existing bar/heatmap/gauge builders: - Health score bands: horizontal bars -> vertical grouped bars - How projects are maintained: horizontal bars -> donut, colored with the same lifecycle-stage hexes already used on the collection page (collection-lifecycle-badge.vue), rendered in the fixed stage order - Where the strongest repositories pull ahead: three grouped bar charts -> a single heatmap by percentile (Top 20% vs median per signal), replacing the previous per-category/gray bar coloring with one consistent value-based color scale - Repositories we can score, by category: horizontal bars -> donut, percentage-primary label with raw count in the tooltip - Security coverage on GitHub-hosted repositories: this widget turned out to be a table, not a horizontal bar chart as the design feedback assumed. Converted it to a vertical grouped bar chart (stage on the x-axis, Linux Foundation vs other projects as two series), which satisfies the same "more chart variance" intent as the other conversions. Also includes the axis-label and grid adjustments needed to keep the new heatmap's row labels and the new grouped-bar chart's x-axis labels legible, since both are new chart configs rather than edits to existing ones. Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Redesigns five Health Score report visualizations and adds a reusable donut-chart configuration.
Changes:
- Converts distribution widgets to grouped bars and donut charts.
- Replaces signal-score charts with a consolidated heatmap.
- Converts the security table to a grouped bar chart.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
band-distribution.vue |
Uses vertical grouped bars. |
lifecycle-distribution.vue |
Uses a lifecycle donut chart. |
signal-scores.vue |
Consolidates signals into a heatmap. |
category-coverage.vue |
Uses a category coverage donut. |
github-security-funnel.vue |
Replaces the table with grouped bars. |
pie.chart.ts |
Adds the shared donut configuration. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const donutData: DonutChartData[] = rows.map((row) => ({ | ||
| name: CATEGORY_LABELS[row.categoryKey] ?? row.categoryKey, | ||
| value: row.scored, | ||
| color: CATEGORY_COLORS[row.categoryKey] ?? lfxColors.brand[500], |
| <client-only> | ||
| <lfx-chart | ||
| :config="chartConfig" | ||
| :animation="true" | ||
| /> |
| left: 320, | ||
| right: '5%', | ||
| top: 4, | ||
| bottom: 4, | ||
| top: 24, | ||
| bottom: 56, | ||
| containLabel: false, |
| inRange: { | ||
| color: [lfxColors.neutral[200], lfxColors.brand[200], lfxColors.brand[500], lfxColors.brand[700]], | ||
| }, | ||
| text: ['Distinguishes strongly', 'Barely distinguishes'], |
| * Get donut chart config. Renders a single-series donut (pie with an inner radius) with each | ||
| * slice colored per `DonutChartData.color`. Tooltip and label show name, value and percent, per | ||
| * ECharts' default pie behavior. |
The comment claimed this widget's category colors matched signal-scores.vue and signal-availability-lf.vue, but both were changed to a heatmap value-scale and an LF/non-LF blue-purple convention respectively. Update the comment to reflect that this widget's category coloring now stands on its own. Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (4)
frontend/app/components/modules/report/health-score-coverage/components/category-coverage.vue:114
- These three counts overlap and each uses
reposTrackedas its denominator, so they cannot be slices of one pie. ECharts instead computes{d}/percentagainst the sum of all three counts: with the existing fixture, Maintainer Health is 20,665 / 31,593 = 65.4%, but this donut reports 20,665 / 52,857 = 39.1%, producing the contradictory tooltip39.1% (20,665 of 31,593). Please represent each category independently againstreposTracked(for example, separate rings or bars) rather than as mutually exclusive slices.
const donutData: DonutChartData[] = rows.map((row) => ({
name: CATEGORY_LABELS[row.categoryKey] ?? row.categoryKey,
value: row.scored,
color: CATEGORY_COLORS[row.categoryKey] ?? lfxColors.brand[500],
}));
return getDonutChartConfig(donutData, {
frontend/app/components/modules/report/health-score-coverage/components/signal-scores.vue:137
- The plot reserves a fixed 320 px left margin, but this full-width card has no minimum width or horizontal overflow. On common 320–390 px mobile viewports, the card's inner width leaves the two-column heatmap only a few pixels (or a negative grid width after the 5% right margin), so the cells and column labels cannot render legibly. Please add a scrollable minimum-width chart wrapper or responsive grid/axis-label settings.
left: 320,
frontend/app/components/modules/report/health-score-coverage/components/github-security-funnel.vue:50
- Replacing the table with
LfxChartremoves the only programmatic access to each stage's counts and percentages.frontend/app/components/uikit/chart/chart.vue:7-15renders only the ECharts view, and this config provides no semantic table/ARIA data, so keyboard and screen-reader users can no longer retrieve the values exposed by the former table. Keep an accessible data table or equivalent semantic summary alongside the visual chart.
<client-only>
<lfx-chart
:config="chartConfig"
:animation="true"
/>
frontend/app/components/modules/report/health-score-coverage/components/signal-scores.vue:184
- Color intensity represents each cell's absolute score percentage, not its discriminating power; only the difference between the two cells in a row indicates discrimination. Labeling the high end
Distinguishes stronglytherefore misstates what the scale encodes (for example, two equally high cells barely distinguish). Label the endpoints as higher/lower score percentages instead.
text: ['Distinguishes strongly', 'Barely distinguishes'],
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (4)
frontend/app/components/modules/report/health-score-coverage/components/category-coverage.vue:114
- These category counts overlap: a repository can be scored in multiple categories, so they are each percentages of
reposTracked, not parts of one whole. ECharts computes donut percentages against the sum ofrow.scoredvalues (for the existing test fixture this produces 39.1%/37.7%/23.2% instead of 65.4%/63.0%/38.9%), making both slice sizes and the displayed “of repos tracked” percentages incorrect. Keep independent bars/progress indicators or use another visualization that preservesscored / reposTracked.
const donutData: DonutChartData[] = rows.map((row) => ({
name: CATEGORY_LABELS[row.categoryKey] ?? row.categoryKey,
value: row.scored,
color: CATEGORY_COLORS[row.categoryKey] ?? lfxColors.brand[500],
}));
return getDonutChartConfig(donutData, {
frontend/app/components/modules/report/health-score-coverage/components/github-security-funnel.vue:50
- This removes the widget’s semantic table and replaces it with a canvas chart.
LfxChartrenders onlyVChartand does not provide a data table or equivalent summary, so screen-reader users lose access to every stage, count, and percentage. Retain an accessible table (it may be visually hidden) or provide an equivalent structured summary alongside the chart.
<client-only>
<lfx-chart
:config="chartConfig"
:animation="true"
/>
frontend/app/components/modules/report/health-score-coverage/components/signal-scores.vue:137
- A fixed 320 px left grid leaves no usable plot area on supported narrow layouts: this full-width card can be about 288–343 px wide on a phone, before the 5% right margin is applied. The heatmap cells therefore collapse or disappear. Make the label/grid width responsive, abbreviate labels on small screens, or place the chart in a horizontally scrollable minimum-width container.
left: 320,
frontend/app/components/modules/report/health-score-coverage/components/signal-scores.vue:184
- The visual map encodes the absolute score percentage, not how strongly a signal distinguishes repositories. A high median and nearly identical p80 value is colored toward “Distinguishes strongly” even though their gap is small, contradicting the explanatory copy. Label this scale in terms of score magnitude; distinction strength is communicated only by comparing the two cells.
text: ['Distinguishes strongly', 'Barely distinguishes'],
Summary
Converts five report widgets to new chart types per design feedback, and adds a reusable donut chart config builder (
app/components/uikit/chart/configs/pie.chart.ts) alongside the existing bar/heatmap/gauge builders.band-distribution.vue): horizontal bars → vertical grouped bars. Scope tabs (All/Linux Foundation/Other) still work unchanged.lifecycle-distribution.vue): horizontal bars → donut chart, colored with the lifecycle-stage hexes already used on the collection page (collection-lifecycle-badge.vue), rendered in the fixed stage order from PR fix: health score report copy, titles, and lifecycle ordering IN-1304 #2203.unavailable(not in that badge's map) falls back to a neutral gray since the donut needs to render all 7 stages.signal-scores.vue): three separate grouped bar charts (one per category) → a single heatmap by percentile. Rows are all 11 signals grouped by category (fixed order), columns are "Top 20% of repositories" and "Typical repository, the median", color intensity encodes the percentage on one shared blue scale. This also resolves the original inconsistent coloring (only the Top-20% series carried category color, median was always gray) — color now consistently encodes value, not category.category-coverage.vue): horizontal bars → donut chart, reusing the same category color scheme as the signal-scores/signal-availability widgets. Label shows percentage (donuts read most naturally as proportions); raw count stays in the tooltip.github-security-funnel.vue): this widget turned out to be a table (<lfx-table>), not a horizontal bar chart as the design feedback assumed — verified directly against the file before converting. Converted to a vertical grouped bar chart (stage on the x-axis, Linux Foundation vs other projects as two series), which satisfies the same intent (more chart variance, easier to scan) as the other conversions.Known/accepted limitation
"Signal availability inside and outside the Linux Foundation" (
signal-availability-lf.vue) is not part of this PR — no chart-type change there per the design feedback. It still shows a binary Linux-Foundation-vs-Other comparison rather than a full distribution, because the underlying Tinybird pipe only returns one aggregate percentage per scope. A true distribution chart would need a backend/pipe change, out of scope here. See PR #2204 for that widget's color update.Test plan
pnpm tsc-check— passespnpm lint:fix— 0 errors (pre-existing unrelated warnings only)pnpm test— all assertions passattachments/IN-1304/Deploy order
N/A — frontend-only change, no backend/pipe dependency for the chart-type conversions themselves.
Migration checklist
N/A — no database changes.
Notes
Stacked on #2203 (
fix/IN-1304-copy-and-structure) because the lifecycle donut's slice order depends on that PR's lifecycle stage sort-order fix — the donut rendersrowsin whatever order the server mapper returns, so it needs the fixed-order fix to render correctly. Targets that branch, notrelease/IN-1304-health-score-report-feedback, directly — merge order matters.release/IN-1304-health-score-report-feedback-