Skip to content

Commit 564d1f3

Browse files
amishneAndrewKushnir
authored andcommitted
feat: add repair attempt graph
1 parent 448e7f3 commit 564d1f3

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,
@@ -283,6 +286,116 @@ export class ReportViewer {
283286
];
284287
}
285288

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

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)