Skip to content

Commit fde865f

Browse files
authored
Merge pull request #42 from learningequality/claude/pr-stats-distributions-KokIK
Add distribution visualization to PR statistics
2 parents 3e6cce2 + 708f24e commit fde865f

File tree

1 file changed

+73
-0
lines changed

1 file changed

+73
-0
lines changed

scripts/pr-statistics.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,77 @@ const { PR_STATS_REPOS } = require('./constants');
33
const ORG = 'learningequality';
44
const ROLLING_WINDOW_DAYS = 30;
55

6+
/**
7+
* Unicode block characters for sparklines, from lowest to highest.
8+
*/
9+
const SPARKLINE_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
10+
11+
/**
12+
* Generate a sparkline string from an array of numeric values.
13+
* Maps each value to a Unicode block character based on its relative position
14+
* between the min and max values.
15+
*/
16+
function sparkline(values) {
17+
if (!values || values.length === 0) return '';
18+
19+
const min = Math.min(...values);
20+
const max = Math.max(...values);
21+
22+
// If all values are the same, return middle-height bars
23+
if (max === min) {
24+
return SPARKLINE_CHARS[3].repeat(values.length);
25+
}
26+
27+
return values
28+
.map(value => {
29+
// Normalize to 0-1 range
30+
const normalized = (value - min) / (max - min);
31+
// Map to character index (0-7)
32+
const index = Math.min(Math.floor(normalized * SPARKLINE_CHARS.length), SPARKLINE_CHARS.length - 1);
33+
return SPARKLINE_CHARS[index];
34+
})
35+
.join('');
36+
}
37+
38+
/**
39+
* Create a histogram from an array of values.
40+
* Returns an array of bin counts.
41+
*/
42+
function histogram(values, numBins = 10) {
43+
if (!values || values.length === 0) return [];
44+
45+
const min = Math.min(...values);
46+
const max = Math.max(...values);
47+
48+
// If all values are the same, put them all in one bin
49+
if (max === min) {
50+
const bins = new Array(numBins).fill(0);
51+
bins[Math.floor(numBins / 2)] = values.length;
52+
return bins;
53+
}
54+
55+
const binWidth = (max - min) / numBins;
56+
const bins = new Array(numBins).fill(0);
57+
58+
values.forEach(value => {
59+
let binIndex = Math.floor((value - min) / binWidth);
60+
// Handle edge case where value equals max
61+
if (binIndex >= numBins) binIndex = numBins - 1;
62+
bins[binIndex]++;
63+
});
64+
65+
return bins;
66+
}
67+
68+
/**
69+
* Generate a distribution sparkline from raw data values.
70+
* Creates a histogram and converts bin counts to a sparkline.
71+
*/
72+
function distributionSparkline(values, numBins = 10) {
73+
const bins = histogram(values, numBins);
74+
return sparkline(bins);
75+
}
76+
677
/**
778
* Calculate percentile value from a sorted array of numbers.
879
* Uses linear interpolation between closest ranks.
@@ -253,6 +324,7 @@ module.exports = async ({ github, core }) => {
253324
slackMessage += `*Time to First Review*\n`;
254325
if (timeToFirstReviewValues.length > 0) {
255326
slackMessage += `Median: ${formatDuration(timeToReviewMedian)} | 95th percentile: ${formatDuration(timeToReviewP95)}\n`;
327+
slackMessage += `Distribution: ${distributionSparkline(timeToFirstReviewValues)}\n`;
256328
slackMessage += `_Based on ${totalReviewedPRs} reviewed PRs_\n\n`;
257329
} else {
258330
slackMessage += `_No reviewed PRs in this period_\n\n`;
@@ -261,6 +333,7 @@ module.exports = async ({ github, core }) => {
261333
slackMessage += `*PR Lifespan (Open to Close/Merge)*\n`;
262334
if (lifespanValues.length > 0) {
263335
slackMessage += `Median: ${formatDuration(lifespanMedian)} | 95th percentile: ${formatDuration(lifespanP95)}\n`;
336+
slackMessage += `Distribution: ${distributionSparkline(lifespanValues)}\n`;
264337
slackMessage += `_Based on ${totalClosedPRs} closed/merged PRs_\n\n`;
265338
} else {
266339
slackMessage += `_No closed PRs in this period_\n\n`;

0 commit comments

Comments
 (0)