Skip to content

Commit cfc2219

Browse files
committed
feat: add repair attempt graph
1 parent 2cded3f commit cfc2219

File tree

6 files changed

+167
-3
lines changed

6 files changed

+167
-3
lines changed

report-app/src/app/pages/report-viewer/report-viewer.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,32 @@ <h3 class="chart-title">
7373
<stacked-bar-chart [data]="buildsAsGraphData(overview.stats.builds)" [compact]="true" />
7474
</div>
7575
</div>
76+
@if (hasSuccessfulResultWithMoreThanOneBuildAttempt()) {
77+
<div class="chart-container repair-attempts">
78+
<h3>
79+
<span class="material-symbols-outlined">build_circle</span>
80+
<span>Repair attempts</span>
81+
<span
82+
class="material-symbols-outlined has-tooltip multiline-tooltip chart-title-tooltip-icon"
83+
data-tooltip="For applications that required repairs to be built, this displays the distribution of how many repair attempts were required."
84+
>info</span
85+
>
86+
@if (averageRepairAttempts() !== null) {
87+
<span class="chart-title-right-label"
88+
>Avg: {{ averageRepairAttempts() | number: '1.2-2' }}</span
89+
>
90+
<span
91+
class="material-symbols-outlined has-tooltip multiline-tooltip chart-title-tooltip-icon"
92+
data-tooltip="Average repair count among applications that were successfully built after repairs."
93+
>info</span
94+
>
95+
}
96+
</h3>
97+
<div class="summary-card-item">
98+
<stacked-bar-chart [data]="repairAttemptsAsGraphData()" [compact]="true" />
99+
</div>
100+
</div>
101+
}
76102
@if (overview.stats.tests) {
77103
<div class="chart-container test-results-details">
78104
<h3 class="chart-title">

report-app/src/app/pages/report-viewer/report-viewer.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,17 @@ lighthouse-category + lighthouse-category {
190190
align-items: center;
191191
}
192192

193+
.chart-title-tooltip-icon {
194+
font-size: 18px;
195+
cursor: help;
196+
}
197+
198+
.chart-title-right-label {
199+
margin-left: auto;
200+
font-size: 0.9rem;
201+
font-weight: 500;
202+
}
203+
193204
.axe-violations ul {
194205
padding: 0px 20px;
195206
}

report-app/src/app/pages/report-viewer/report-viewer.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
viewChild,
1414
} from '@angular/core';
1515
import {NgxJsonViewerModule} from 'ngx-json-viewer';
16-
import {BuildErrorType} from '../../../../../runner/workers/builder/builder-types';
16+
import {
17+
BuildErrorType,
18+
BuildResultStatus,
19+
} from '../../../../../runner/workers/builder/builder-types';
1720
import {
1821
AssessmentResult,
1922
AssessmentResultFromReportServer,
@@ -281,6 +284,116 @@ export class ReportViewer {
281284
];
282285
}
283286

287+
protected hasSuccessfulResultWithMoreThanOneBuildAttempt = computed(() => {
288+
if (!this.selectedReport.hasValue()) {
289+
return false;
290+
}
291+
for (const result of this.selectedReport.value().results) {
292+
if (
293+
result.finalAttempt.buildResult.status === BuildResultStatus.SUCCESS &&
294+
result.repairAttempts > 1
295+
) {
296+
return true;
297+
}
298+
}
299+
return false;
300+
});
301+
302+
protected averageRepairAttempts = computed<number | null>(() => {
303+
const report = this.selectedReportWithSortedResults();
304+
if (!report) {
305+
return null;
306+
}
307+
308+
let totalRepairs = 0;
309+
let count = 0;
310+
311+
for (const result of report.results) {
312+
// Only consider successful builds that required repairs.
313+
if (
314+
result.finalAttempt.buildResult.status === BuildResultStatus.SUCCESS &&
315+
result.repairAttempts > 0
316+
) {
317+
totalRepairs += result.repairAttempts;
318+
count++;
319+
}
320+
}
321+
322+
return count > 0 ? totalRepairs / count : null;
323+
});
324+
325+
protected repairAttemptsAsGraphData = computed<StackedBarChartData>(() => {
326+
const report = this.selectedReportWithSortedResults();
327+
if (!report) {
328+
return [];
329+
}
330+
331+
const repairsToAppCount = new Map<number | 'failed', number>();
332+
333+
// Map repair count to how many applications shared that count.
334+
let maxRepairCount = 0;
335+
for (const result of report.results) {
336+
if (result.finalAttempt.buildResult.status === BuildResultStatus.ERROR) {
337+
repairsToAppCount.set('failed', (repairsToAppCount.get('failed') || 0) + 1);
338+
} else {
339+
const repairs = result.repairAttempts;
340+
// For this graph, we ignore applications that required no repair.
341+
if (repairs > 0) {
342+
repairsToAppCount.set(repairs, (repairsToAppCount.get(repairs) || 0) + 1);
343+
maxRepairCount = Math.max(maxRepairCount, repairs);
344+
}
345+
}
346+
}
347+
348+
const data: StackedBarChartData = [];
349+
350+
// All the numeric keys, sorted by value.
351+
const intermediateRepairKeys = Array.from(repairsToAppCount.keys())
352+
.filter((k): k is number => typeof k === 'number')
353+
.sort((a, b) => a - b);
354+
355+
// This graph might involve a bunch of sections. We want to scale them among all the possible color "grades".
356+
357+
const minGrade = 1;
358+
const maxGrade = 8;
359+
const failureGrade = 9;
360+
361+
for (let repairCount = 1; repairCount <= maxRepairCount; repairCount++) {
362+
const applicationCount = repairsToAppCount.get(repairCount);
363+
if (!applicationCount) continue;
364+
const label = `${repairCount} repair${repairCount > 1 ? 's' : ''}`;
365+
366+
// Normalize the repair count to the range [0, 1].
367+
const normalizedRepairCount = (repairCount - 1) / (maxRepairCount - 1);
368+
369+
let gradeIndex: number;
370+
if (intermediateRepairKeys.length === 1) {
371+
// If there's only one intermediate repair count, map it to a middle grade (e.g., --chart-grade-5)
372+
gradeIndex = Math.floor(maxGrade / 2) + minGrade;
373+
} else {
374+
// Distribute multiple intermediate repair counts evenly across available grades
375+
gradeIndex = minGrade + Math.round(normalizedRepairCount * (maxGrade - minGrade));
376+
}
377+
378+
data.push({
379+
label,
380+
color: `var(--chart-grade-${gradeIndex})`,
381+
value: applicationCount,
382+
});
383+
}
384+
385+
// Handle 'Build failed even after all retries' - always maps to the "failure" grade.
386+
const failedCount = repairsToAppCount.get('failed') || 0;
387+
if (failedCount > 0) {
388+
data.push({
389+
label: 'Build failed even after all retries',
390+
color: `var(--chart-grade-${failureGrade})`,
391+
value: failedCount,
392+
});
393+
}
394+
return data;
395+
});
396+
284397
protected testsAsGraphData(tests: RunSummaryTests): StackedBarChartData {
285398
return [
286399
{

report-app/src/app/shared/styles/tooltip.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
&.multiline-tooltip::before {
3030
white-space: normal;
31+
width: max-content;
3132
max-width: 400px;
3233
}
3334

report-app/src/app/shared/visualization/stacked-bar-chart/stacked-bar-chart.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@
5656

5757
.legend {
5858
display: flex;
59-
justify-content: center;
60-
gap: 1.5rem;
59+
flex-wrap: wrap;
60+
justify-content: flex-start;
61+
column-gap: 1.5rem;
6162
}
6263

6364
.legend-item {
@@ -66,6 +67,7 @@
6667
font-size: 14px;
6768
color: var(--text-secondary);
6869
white-space: nowrap;
70+
margin-top: 0.5rem;
6971
}
7072

7173
.legend-color {

report-app/src/styles.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@
3838
--status-text-poor: #eb1515;
3939
--status-text-neutral: #64748b;
4040

41+
/* 10-step Green-to-Red Quality Gradient */
42+
--chart-grade-1: #10b981; /* Emerald 500 (Excellent) */
43+
--chart-grade-2: #22c55e; /* Green 500 */
44+
--chart-grade-3: #4ade80; /* Green 400 */
45+
--chart-grade-4: #84cc16; /* Lime 500 (Great) */
46+
--chart-grade-5: #a3e635; /* Lime 400 */
47+
--chart-grade-6: #facc15; /* Yellow 400 */
48+
--chart-grade-7: #f59e0b; /* Amber 500 (Good) */
49+
--chart-grade-8: #f97316; /* Orange 500 */
50+
--chart-grade-9: #ef4444; /* Red 500 (Poor) */
51+
4152
--tooltip-background-color: light-dark(#111827, #f1f4f9);
4253
--tooltip-text-color: light-dark(#f9fafb, #1e293b);
4354

0 commit comments

Comments
 (0)