Skip to content

Commit c63ba65

Browse files
committed
feat: add repair attempt graph
1 parent 5b40782 commit c63ba65

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,
@@ -287,6 +290,116 @@ export class ReportViewer {
287290
];
288291
}
289292

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

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)