Skip to content

Commit

Permalink
Draw bug age distributions as box & whisker plots.
Browse files Browse the repository at this point in the history
  • Loading branch information
jyasskin committed Oct 17, 2023
1 parent 953fc02 commit dc51758
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 53 deletions.
123 changes: 114 additions & 9 deletions frontend/src/components/BugStats.astro
Original file line number Diff line number Diff line change
@@ -1,21 +1,126 @@
---
import type { Temporal } from "@js-temporal/polyfill";
import { formatRoundAge } from "@lib/formatRoundAge";
import type { AgeStats } from "@lib/repo-summaries";
interface Props {
ageStats: AgeStats;
description: string;
/** The global maximum bug age, to render at the right side of the chart. */
maxAge: Temporal.Duration;
}
const { ageStats, description } = Astro.props;
const { ageStats, description, maxAge } = Astro.props;
const percentiles = [10, 25, 50, 75, 90];
const maxAgeS = maxAge.total("seconds");
const p10Frac =
Math.round((ageStats[10].total("seconds") / maxAgeS) * 1000) / 10;
const p25Frac =
Math.round((ageStats[25].total("seconds") / maxAgeS) * 1000) / 10;
const p50Frac =
Math.round((ageStats[50].total("seconds") / maxAgeS) * 1000) / 10;
const p75Frac =
Math.round((ageStats[75].total("seconds") / maxAgeS) * 1000) / 10;
const p90Frac =
Math.round((ageStats[90].total("seconds") / maxAgeS) * 1000) / 10;
const meanFrac =
Math.round((ageStats.mean.total("seconds") / maxAgeS) * 1000) / 10;
---

<div class="bugStats">
<p>{ageStats.count}{' '}{description}: mean age {formatRoundAge(ageStats.mean)}</p>
<table>
<tr>{percentiles.map((p) => <th><span class="ord">{p}th</span> %ile</th>)}</tr>
<tr>{percentiles.map((p) => <td>{formatRoundAge(ageStats[p])}</td>)}</tr
<figure class="bugStats" title={`${ageStats.count} ${description}`}>
<span class="whisker" style={`--start: ${p10Frac}%; --end: ${p90Frac}%`}
></span>
<span class="box" style={`--start: ${p25Frac}%; --end: ${p75Frac}%`}></span>
<ul>
<li
class="p"
style={`--frac: ${p10Frac}%`}
title=`10th percentile: ${formatRoundAge(ageStats[10])}`
>
</table>
</div>
</li>
<li
class="p"
style={`--frac: ${p25Frac}%`}
title=`25th percentile: ${formatRoundAge(ageStats[25])}`
>
</li>
<li
class="median"
style={`--frac: ${p50Frac}%`}
title=`median: ${formatRoundAge(ageStats[50])}`
>
</li>
<li
class="p"
style={`--frac: ${p75Frac}%`}
title=`75th percentile: ${formatRoundAge(ageStats[75])}`
>
</li>
<li
class="p"
style={`--frac: ${p90Frac}%`}
title=`90th percentile: ${formatRoundAge(ageStats[90])}`
>
</li>
<li
class="mean"
style={`--frac: ${meanFrac}%`}
title=`mean: ${formatRoundAge(ageStats.mean)}`
>
</li>
</ul>
</figure>

<style is:global>
.bugStats {
display: inline-block;
vertical-align: middle;
position: relative;
margin: 0;
padding: 0;
height: 1lh;
width: clamp(50px, 40vw, 500px);
}
.bugStats :is(ul, li) {
display: inline;
margin: 0;
padding: 0;
}
.bugStats :is(.box, .whisker) {
position: absolute;
left: var(--start);
right: calc(100% - var(--end));
border: thin solid black;
}
.bugStats .box {
top: 0;
bottom: 0;
background-color: var(--bg);
}
.bugStats .whisker {
top: 50%;
bottom: 50%;
}
.bugStats :is(.p, .median, .mean) {
position: absolute;
left: calc(var(--frac) - 0.5em);
right: calc(100% - var(--frac) - 0.5em);
top: 0;
bottom: 0;
}
.bugStats .median:after {
position: absolute;
left: 50%;
right: 50%;
top:0;
bottom:0;
border: thin solid black;
content: " ";
}
.bugStats .mean:after {
content: "★";
position: absolute;
font-size: 50%;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
}
</style>
10 changes: 7 additions & 3 deletions frontend/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ const { title } = Astro.props;
</body>
</html>
<style is:global>
body {
font-family: system-ui;
}
:root {
--bg: white;
--fg: black;
}
body {
font-family: system-ui;
}
.ord {
font-variant-numeric: ordinal;
}
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/formatRoundAge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import type { Temporal } from '@js-temporal/polyfill';
export function formatRoundAge(age: Temporal.Duration): string {
const totalDays = age.total('day');
if (totalDays > 336) {
const roundYears = Math.round(totalDays / 365.24);
const roundYears = Math.round(totalDays / 365.24 * 10) / 10;
return `${roundYears} year${roundYears === 1 ? '' : 's'}`;
} else if (totalDays > 28) {
const roundMonths = Math.round(totalDays / 30.4);
const roundMonths = Math.round(totalDays / 30.4 * 10) / 10;
return `${roundMonths} month${roundMonths === 1 ? '' : 's'}`;
} else if (totalDays > 6) {
const roundWeeks = Math.round(totalDays / 7);
const roundWeeks = Math.round(totalDays / 7 * 10) / 10;
return `${roundWeeks} week${roundWeeks === 1 ? '' : 's'}`;
} else if (totalDays > 23/24) {
} else if (totalDays > 23 / 24) {
const roundDays = Math.round(totalDays);
return `${roundDays} day${roundDays === 1 ? '' : 's'}`;
} else if (totalDays > 59 / 60 / 24) {
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/lib/repo-summaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ export const IssueSummary = z.object({
labels: z.array(z.string()),
ageAtCloseMs: z.number().optional(),
})

export type IssueSummary = z.infer<typeof IssueSummary>;

const durationInMs = z.number().transform(val => Temporal.Duration.from({ milliseconds: Math.round(val) }));

export const AgeStats = z.object({
count: z.number(),
mean: z.number().transform(val => Temporal.Duration.from({ milliseconds: Math.round(val) })),
}).catchall(/*percentiles*/ z.number().transform(val => Temporal.Duration.from({ milliseconds: val })));
mean: durationInMs,
10: durationInMs,
25: durationInMs,
50: durationInMs,
75: durationInMs,
90: durationInMs,
});
export type AgeStats = z.infer<typeof AgeStats>;

export const RepoSummary = z.object({
Expand Down
83 changes: 49 additions & 34 deletions frontend/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import BugStats from "@components/BugStats.astro";
import { Temporal } from "@js-temporal/polyfill";
import { AgeStats, RepoSummary } from "@lib/repo-summaries";
import { z } from "zod";
import Layout from "../layouts/Layout.astro";
Expand All @@ -14,48 +15,62 @@ const GlobalStats = z.object({
const globalStats = GlobalStats.parse(
(await Astro.glob("../../../scanner/summaries/global.json"))[0]
);
const maxAge = Temporal.Duration.from({
// Use the open age because it's bigger in practice.
seconds: Math.round(globalStats.openAgeMs[90].total("seconds") * 1.2),
});
---

<Layout title="Browser Spec Maintenance Status">
<main>
<h1>Browser Spec Maintenance Status</h1>
<section id="global-stats">
<BugStats
ageStats={globalStats.openAgeMs}
description="open bugs"
/>
<BugStats
ageStats={globalStats.ageAtCloseMs}
description="closed bugs"
/>
</section>

<section>
<ul>
<table>
<tbody>
<tr
><td>{globalStats.openAgeMs.count} total open bugs</td><td>
<BugStats
ageStats={globalStats.openAgeMs}
description="open bugs"
{maxAge}
/></td
></tr
>
<tr
><td>{globalStats.ageAtCloseMs.count} total closed bugs</td>
<td
><BugStats
ageStats={globalStats.ageAtCloseMs}
description="closed bugs"
{maxAge}
/>
</td></tr
>
</tbody>
<tbody>
<tr><td></td><th>Open Bugs</th></tr>
{
repos.map((repo) => (
<li>
<a href={`${repo.org}/${repo.repo}`}>
{repo.org}/{repo.repo}
</a>
{repo.openAgeMs ? (
<BugStats
ageStats={repo.openAgeMs}
description="open bugs"
/>
) : (
": 0 open bugs"
)}
</li>
<tr>
<td>
<a href={`${repo.org}/${repo.repo}`}>
{repo.org}/{repo.repo}
</a>
</td>
<td>
{repo.openAgeMs ? (
<BugStats
ageStats={repo.openAgeMs}
description="open bugs"
{maxAge}
/>
) : (
"No open bugs"
)}
</td>
</tr>
))
}
</ul>
</section>
</tbody>
</table>
</main>
</Layout>

<style>
#global-stats {
display: flex;
}
</style>

0 comments on commit dc51758

Please sign in to comment.