diff --git a/components/DetailPanel.jsx b/components/DetailPanel.jsx
index 1d64c11..c33431d 100644
--- a/components/DetailPanel.jsx
+++ b/components/DetailPanel.jsx
@@ -2,7 +2,8 @@
import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
-import { formatNum } from '../lib/format.js';
+import { formatNum, formatRelativeTime, isStaleData } from '../lib/format.js';
+import { DIMENSIONS, SCORE_METHODOLOGY } from '../lib/scoring.js';
export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMount = false, claimSuccess = false }) {
const [fullData, setFullData] = useState(null);
@@ -79,7 +80,12 @@ export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMou
š {dev.location || 'Unknown location'}
- Score: {dev.score}/100
+
+ Score: {dev.score}/100
+
{dev.globalRank && (
)}
+
+ {SCORE_METHODOLOGY.short}
+
GitHub ā
{merged.soUserId && (
@@ -145,6 +154,7 @@ export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMou
@@ -190,6 +200,54 @@ export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMou
);
}
+function ScoreExplanation({ dev }) {
+ const dimensions = dev.scoreDimensions;
+ const weights = dev.scoreWeights;
+ if (!dimensions || !weights) return null;
+
+ const freshLabel = formatRelativeTime(dev.metricsUpdatedAt);
+ const stale = isStaleData(dev.metricsUpdatedAt);
+
+ return (
+
+
{SCORE_METHODOLOGY.short}
+ {dev.scoreHasSO === false && (
+
{SCORE_METHODOLOGY.noSO}
+ )}
+
+
+
+ {typeof dev.scorePercentile === 'number' && (
+
+ Higher than {dev.scorePercentile}% of developers currently indexed by DevGlobe.
+
+ )}
+
+
+ {freshLabel ? `Metrics last refreshed ${freshLabel}${stale ? ' ā may be out of date' : ''}.` : 'Metrics freshness unknown.'}
+
+
+ );
+}
+
function StatCard({ label, value, className = '' }) {
return (
@@ -514,4 +572,4 @@ function CardModal({ dev, claimSuccess, onClose }) {
);
-}
+}
\ No newline at end of file
diff --git a/components/Globe.jsx b/components/Globe.jsx
index 5ffaa9d..12c681b 100644
--- a/components/Globe.jsx
+++ b/components/Globe.jsx
@@ -17,12 +17,12 @@ const COUNTRY_GEOJSON_URLS = [
const POLYGON_ALTITUDE = 0.003;
const POLYGON_ALTITUDE_ACTIVE = 0.009;
-// Score-based color gradient: blue ā green ā gold ā red
+// Score-based color gradient (visual tiering only, not a judgment of skill)
function getScoreColor(score) {
- if (score >= 80) return '#fbbf24'; // gold ā elite
- if (score >= 60) return '#34d399'; // emerald ā strong
- if (score >= 40) return '#3b82f6'; // blue ā solid
- return '#6366f1'; // indigo ā emerging
+ if (score >= 80) return '#fbbf24'; // gold ā top of the indexed range
+ if (score >= 60) return '#34d399'; // emerald ā upper-mid range
+ if (score >= 40) return '#3b82f6'; // blue ā mid range
+ return '#6366f1'; // indigo ā lower range
}
function featureName(feat) {
diff --git a/components/Leaderboard.jsx b/components/Leaderboard.jsx
index 71cd4f5..17ade0d 100644
--- a/components/Leaderboard.jsx
+++ b/components/Leaderboard.jsx
@@ -3,6 +3,7 @@
import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
import { formatNum } from '../lib/format.js';
import { extractCountry, normalizeCountry, countryKey } from '../lib/country.js';
+import { SCORE_METHODOLOGY } from '../lib/scoring.js';
const ITEM_HEIGHT = 62;
const BUFFER = 10;
@@ -165,6 +166,11 @@ export default function Leaderboard({
+ {sortBy === 'score' && (
+
+ ā Score is relative to this dataset, not an absolute rating ā tap a profile for the full breakdown.
+
+ )}
@@ -218,7 +224,13 @@ export default function Leaderboard({
>
{isCompareSelected ? 'ā' : 'ā'}
- {dev.score}
+
+ {dev.score}
+
);
diff --git a/data/developers-sample.json b/data/developers-sample.json
index 203e9c5..dee01bf 100644
--- a/data/developers-sample.json
+++ b/data/developers-sample.json
@@ -13,21 +13,46 @@
"followers": 210000,
"topLanguage": "C",
"languages": [
- { "name": "C", "percent": 85 },
- { "name": "Assembly", "percent": 8 },
- { "name": "Shell", "percent": 5 },
- { "name": "Other", "percent": 2 }
+ {
+ "name": "C",
+ "percent": 85
+ },
+ {
+ "name": "Assembly",
+ "percent": 8
+ },
+ {
+ "name": "Shell",
+ "percent": 5
+ },
+ {
+ "name": "Other",
+ "percent": 2
+ }
],
"topRepos": [
- { "name": "linux", "stars": 175000, "forks": 52000 },
- { "name": "subsurface-for-dirk", "stars": 2200, "forks": 680 },
- { "name": "uemacs", "stars": 710, "forks": 120 }
+ {
+ "name": "linux",
+ "stars": 175000,
+ "forks": 52000
+ },
+ {
+ "name": "subsurface-for-dirk",
+ "stars": 2200,
+ "forks": 680
+ },
+ {
+ "name": "uemacs",
+ "stars": 710,
+ "forks": 120
+ }
],
"soUserId": null,
"soReputation": 0,
"soAnswers": 0,
"soAcceptRate": 0,
- "soBadges": 0
+ "soBadges": 0,
+ "metricsUpdatedAt": "2026-08-06T00:00:00Z"
},
{
"login": "gaearon",
@@ -43,21 +68,46 @@
"followers": 85000,
"topLanguage": "JavaScript",
"languages": [
- { "name": "JavaScript", "percent": 60 },
- { "name": "TypeScript", "percent": 25 },
- { "name": "CSS", "percent": 10 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "JavaScript",
+ "percent": 60
+ },
+ {
+ "name": "TypeScript",
+ "percent": 25
+ },
+ {
+ "name": "CSS",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "redux", "stars": 60700, "forks": 15700 },
- { "name": "react-hot-loader", "stars": 12300, "forks": 850 },
- { "name": "overreacted.io", "stars": 7100, "forks": 1300 }
+ {
+ "name": "redux",
+ "stars": 60700,
+ "forks": 15700
+ },
+ {
+ "name": "react-hot-loader",
+ "stars": 12300,
+ "forks": 850
+ },
+ {
+ "name": "overreacted.io",
+ "stars": 7100,
+ "forks": 1300
+ }
],
"soUserId": 458193,
"soReputation": 52000,
"soAnswers": 320,
"soAcceptRate": 45,
- "soBadges": 48
+ "soBadges": 48,
+ "metricsUpdatedAt": "2026-08-04T00:00:00Z"
},
{
"login": "sindresorhus",
@@ -73,21 +123,46 @@
"followers": 56000,
"topLanguage": "TypeScript",
"languages": [
- { "name": "TypeScript", "percent": 45 },
- { "name": "JavaScript", "percent": 35 },
- { "name": "Swift", "percent": 15 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "TypeScript",
+ "percent": 45
+ },
+ {
+ "name": "JavaScript",
+ "percent": 35
+ },
+ {
+ "name": "Swift",
+ "percent": 15
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "awesome", "stars": 310000, "forks": 27000 },
- { "name": "got", "stars": 14000, "forks": 940 },
- { "name": "execa", "stars": 6800, "forks": 220 }
+ {
+ "name": "awesome",
+ "stars": 310000,
+ "forks": 27000
+ },
+ {
+ "name": "got",
+ "stars": 14000,
+ "forks": 940
+ },
+ {
+ "name": "execa",
+ "stars": 6800,
+ "forks": 220
+ }
],
"soUserId": null,
"soReputation": 1200,
"soAnswers": 15,
"soAcceptRate": 60,
- "soBadges": 8
+ "soBadges": 8,
+ "metricsUpdatedAt": "2026-08-02T00:00:00Z"
},
{
"login": "tj",
@@ -103,21 +178,46 @@
"followers": 38000,
"topLanguage": "Go",
"languages": [
- { "name": "Go", "percent": 40 },
- { "name": "JavaScript", "percent": 35 },
- { "name": "TypeScript", "percent": 15 },
- { "name": "Other", "percent": 10 }
+ {
+ "name": "Go",
+ "percent": 40
+ },
+ {
+ "name": "JavaScript",
+ "percent": 35
+ },
+ {
+ "name": "TypeScript",
+ "percent": 15
+ },
+ {
+ "name": "Other",
+ "percent": 10
+ }
],
"topRepos": [
- { "name": "commander.js", "stars": 26500, "forks": 1700 },
- { "name": "co", "stars": 11900, "forks": 780 },
- { "name": "n", "stars": 18700, "forks": 750 }
+ {
+ "name": "commander.js",
+ "stars": 26500,
+ "forks": 1700
+ },
+ {
+ "name": "co",
+ "stars": 11900,
+ "forks": 780
+ },
+ {
+ "name": "n",
+ "stars": 18700,
+ "forks": 750
+ }
],
"soUserId": 68115,
"soReputation": 35000,
"soAnswers": 890,
"soAcceptRate": 52,
- "soBadges": 62
+ "soBadges": 62,
+ "metricsUpdatedAt": "2026-07-31T00:00:00Z"
},
{
"login": "yyx990803",
@@ -133,21 +233,46 @@
"followers": 95000,
"topLanguage": "TypeScript",
"languages": [
- { "name": "TypeScript", "percent": 55 },
- { "name": "JavaScript", "percent": 30 },
- { "name": "HTML", "percent": 10 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "TypeScript",
+ "percent": 55
+ },
+ {
+ "name": "JavaScript",
+ "percent": 30
+ },
+ {
+ "name": "HTML",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "vue", "stars": 207000, "forks": 33800 },
- { "name": "vite", "stars": 67000, "forks": 6000 },
- { "name": "vue-next", "stars": 45000, "forks": 8200 }
+ {
+ "name": "vue",
+ "stars": 207000,
+ "forks": 33800
+ },
+ {
+ "name": "vite",
+ "stars": 67000,
+ "forks": 6000
+ },
+ {
+ "name": "vue-next",
+ "stars": 45000,
+ "forks": 8200
+ }
],
"soUserId": 2437875,
"soReputation": 8400,
"soAnswers": 120,
"soAcceptRate": 38,
- "soBadges": 22
+ "soBadges": 22,
+ "metricsUpdatedAt": "2026-08-05T00:00:00Z"
},
{
"login": "antirez",
@@ -155,7 +280,7 @@
"avatarUrl": "https://avatars.githubusercontent.com/u/65601?v=4",
"location": "Catania, Italy",
"lat": 37.5079,
- "lng": 15.0830,
+ "lng": 15.083,
"totalStars": 66000,
"totalCommits": 9800,
"totalForks": 24000,
@@ -163,21 +288,46 @@
"followers": 22000,
"topLanguage": "C",
"languages": [
- { "name": "C", "percent": 70 },
- { "name": "Tcl", "percent": 15 },
- { "name": "Ruby", "percent": 10 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "C",
+ "percent": 70
+ },
+ {
+ "name": "Tcl",
+ "percent": 15
+ },
+ {
+ "name": "Ruby",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "redis", "stars": 65000, "forks": 23500 },
- { "name": "kilo", "stars": 6900, "forks": 960 },
- { "name": "smallchat", "stars": 7200, "forks": 870 }
+ {
+ "name": "redis",
+ "stars": 65000,
+ "forks": 23500
+ },
+ {
+ "name": "kilo",
+ "stars": 6900,
+ "forks": 960
+ },
+ {
+ "name": "smallchat",
+ "stars": 7200,
+ "forks": 870
+ }
],
"soUserId": null,
"soReputation": 0,
"soAnswers": 0,
"soAcceptRate": 0,
- "soBadges": 0
+ "soBadges": 0,
+ "metricsUpdatedAt": "2026-06-23T00:00:00Z"
},
{
"login": "mrdoob",
@@ -193,20 +343,41 @@
"followers": 28000,
"topLanguage": "JavaScript",
"languages": [
- { "name": "JavaScript", "percent": 80 },
- { "name": "GLSL", "percent": 10 },
- { "name": "HTML", "percent": 8 },
- { "name": "Other", "percent": 2 }
+ {
+ "name": "JavaScript",
+ "percent": 80
+ },
+ {
+ "name": "GLSL",
+ "percent": 10
+ },
+ {
+ "name": "HTML",
+ "percent": 8
+ },
+ {
+ "name": "Other",
+ "percent": 2
+ }
],
"topRepos": [
- { "name": "three.js", "stars": 101000, "forks": 27000 },
- { "name": "htmleditor", "stars": 980, "forks": 290 }
+ {
+ "name": "three.js",
+ "stars": 101000,
+ "forks": 27000
+ },
+ {
+ "name": "htmleditor",
+ "stars": 980,
+ "forks": 290
+ }
],
"soUserId": null,
"soReputation": 3200,
"soAnswers": 45,
"soAcceptRate": 30,
- "soBadges": 12
+ "soBadges": 12,
+ "metricsUpdatedAt": "2026-07-28T00:00:00Z"
},
{
"login": "JakeWharton",
@@ -222,21 +393,46 @@
"followers": 68000,
"topLanguage": "Kotlin",
"languages": [
- { "name": "Kotlin", "percent": 40 },
- { "name": "Java", "percent": 35 },
- { "name": "Groovy", "percent": 15 },
- { "name": "Other", "percent": 10 }
+ {
+ "name": "Kotlin",
+ "percent": 40
+ },
+ {
+ "name": "Java",
+ "percent": 35
+ },
+ {
+ "name": "Groovy",
+ "percent": 15
+ },
+ {
+ "name": "Other",
+ "percent": 10
+ }
],
"topRepos": [
- { "name": "butterknife", "stars": 25700, "forks": 4700 },
- { "name": "timber", "stars": 10400, "forks": 960 },
- { "name": "retrofit", "stars": 43000, "forks": 7400 }
+ {
+ "name": "butterknife",
+ "stars": 25700,
+ "forks": 4700
+ },
+ {
+ "name": "timber",
+ "stars": 10400,
+ "forks": 960
+ },
+ {
+ "name": "retrofit",
+ "stars": 43000,
+ "forks": 7400
+ }
],
"soUserId": 132047,
"soReputation": 128000,
"soAnswers": 2400,
"soAcceptRate": 68,
- "soBadges": 184
+ "soBadges": 184,
+ "metricsUpdatedAt": "2026-06-08T00:00:00Z"
},
{
"login": "getify",
@@ -252,21 +448,46 @@
"followers": 44000,
"topLanguage": "JavaScript",
"languages": [
- { "name": "JavaScript", "percent": 90 },
- { "name": "HTML", "percent": 5 },
- { "name": "CSS", "percent": 3 },
- { "name": "Other", "percent": 2 }
+ {
+ "name": "JavaScript",
+ "percent": 90
+ },
+ {
+ "name": "HTML",
+ "percent": 5
+ },
+ {
+ "name": "CSS",
+ "percent": 3
+ },
+ {
+ "name": "Other",
+ "percent": 2
+ }
],
"topRepos": [
- { "name": "You-Dont-Know-JS", "stars": 178000, "forks": 21000 },
- { "name": "Functional-Light-JS", "stars": 16500, "forks": 2100 },
- { "name": "LABjs", "stars": 2300, "forks": 340 }
+ {
+ "name": "You-Dont-Know-JS",
+ "stars": 178000,
+ "forks": 21000
+ },
+ {
+ "name": "Functional-Light-JS",
+ "stars": 16500,
+ "forks": 2100
+ },
+ {
+ "name": "LABjs",
+ "stars": 2300,
+ "forks": 340
+ }
],
"soUserId": 228852,
"soReputation": 67000,
"soAnswers": 1800,
"soAcceptRate": 55,
- "soBadges": 95
+ "soBadges": 95,
+ "metricsUpdatedAt": "2026-08-03T00:00:00Z"
},
{
"login": "addyosmani",
@@ -282,28 +503,53 @@
"followers": 42000,
"topLanguage": "JavaScript",
"languages": [
- { "name": "JavaScript", "percent": 65 },
- { "name": "HTML", "percent": 15 },
- { "name": "CSS", "percent": 10 },
- { "name": "Other", "percent": 10 }
+ {
+ "name": "JavaScript",
+ "percent": 65
+ },
+ {
+ "name": "HTML",
+ "percent": 15
+ },
+ {
+ "name": "CSS",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 10
+ }
],
"topRepos": [
- { "name": "learning-jsdp", "stars": 28000, "forks": 5100 },
- { "name": "es6-tools", "stars": 4000, "forks": 510 },
- { "name": "critical", "stars": 10000, "forks": 430 }
+ {
+ "name": "learning-jsdp",
+ "stars": 28000,
+ "forks": 5100
+ },
+ {
+ "name": "es6-tools",
+ "stars": 4000,
+ "forks": 510
+ },
+ {
+ "name": "critical",
+ "stars": 10000,
+ "forks": 430
+ }
],
"soUserId": 193238,
"soReputation": 48000,
"soAnswers": 620,
"soAcceptRate": 42,
- "soBadges": 55
+ "soBadges": 55,
+ "metricsUpdatedAt": "2026-08-01T00:00:00Z"
},
{
"login": "vczh",
"name": "Zhiyu Chen",
"avatarUrl": "https://avatars.githubusercontent.com/u/1320481?v=4",
"location": "Redmond, WA, USA",
- "lat": 47.6740,
+ "lat": 47.674,
"lng": -122.1215,
"totalStars": 25000,
"totalCommits": 22000,
@@ -312,21 +558,46 @@
"followers": 18000,
"topLanguage": "C++",
"languages": [
- { "name": "C++", "percent": 70 },
- { "name": "C#", "percent": 15 },
- { "name": "TypeScript", "percent": 10 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "C++",
+ "percent": 70
+ },
+ {
+ "name": "C#",
+ "percent": 15
+ },
+ {
+ "name": "TypeScript",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "GacUI", "stars": 3600, "forks": 480 },
- { "name": "vczh-libraries", "stars": 2800, "forks": 380 },
- { "name": "tinymoe", "stars": 1600, "forks": 220 }
+ {
+ "name": "GacUI",
+ "stars": 3600,
+ "forks": 480
+ },
+ {
+ "name": "vczh-libraries",
+ "stars": 2800,
+ "forks": 380
+ },
+ {
+ "name": "tinymoe",
+ "stars": 1600,
+ "forks": 220
+ }
],
"soUserId": null,
"soReputation": 15000,
"soAnswers": 450,
"soAcceptRate": 35,
- "soBadges": 32
+ "soBadges": 32,
+ "metricsUpdatedAt": "2026-07-30T00:00:00Z"
},
{
"login": "ThePrimeagen",
@@ -334,7 +605,7 @@
"avatarUrl": "https://avatars.githubusercontent.com/u/4458174?v=4",
"location": "Salt Lake City, UT, USA",
"lat": 40.7608,
- "lng": -111.8910,
+ "lng": -111.891,
"totalStars": 32000,
"totalCommits": 8900,
"totalForks": 4200,
@@ -342,22 +613,50 @@
"followers": 24000,
"topLanguage": "TypeScript",
"languages": [
- { "name": "TypeScript", "percent": 35 },
- { "name": "Go", "percent": 25 },
- { "name": "Rust", "percent": 20 },
- { "name": "Zig", "percent": 15 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "TypeScript",
+ "percent": 35
+ },
+ {
+ "name": "Go",
+ "percent": 25
+ },
+ {
+ "name": "Rust",
+ "percent": 20
+ },
+ {
+ "name": "Zig",
+ "percent": 15
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "harpoon", "stars": 6800, "forks": 420 },
- { "name": "vim-be-good", "stars": 2900, "forks": 180 },
- { "name": "kata-machine", "stars": 2100, "forks": 680 }
+ {
+ "name": "harpoon",
+ "stars": 6800,
+ "forks": 420
+ },
+ {
+ "name": "vim-be-good",
+ "stars": 2900,
+ "forks": 180
+ },
+ {
+ "name": "kata-machine",
+ "stars": 2100,
+ "forks": 680
+ }
],
"soUserId": null,
"soReputation": 2800,
"soAnswers": 35,
"soAcceptRate": 40,
- "soBadges": 8
+ "soBadges": 8,
+ "metricsUpdatedAt": "2026-05-09T00:00:00Z"
},
{
"login": "jonschlinkert",
@@ -365,7 +664,7 @@
"avatarUrl": "https://avatars.githubusercontent.com/u/383994?v=4",
"location": "Cincinnati, OH, USA",
"lat": 39.1031,
- "lng": -84.5120,
+ "lng": -84.512,
"totalStars": 68000,
"totalCommits": 45000,
"totalForks": 9800,
@@ -373,20 +672,42 @@
"followers": 6800,
"topLanguage": "JavaScript",
"languages": [
- { "name": "JavaScript", "percent": 85 },
- { "name": "TypeScript", "percent": 10 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "JavaScript",
+ "percent": 85
+ },
+ {
+ "name": "TypeScript",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "micromatch", "stars": 2200, "forks": 180 },
- { "name": "remarkable", "stars": 5700, "forks": 410 },
- { "name": "gray-matter", "stars": 3800, "forks": 210 }
+ {
+ "name": "micromatch",
+ "stars": 2200,
+ "forks": 180
+ },
+ {
+ "name": "remarkable",
+ "stars": 5700,
+ "forks": 410
+ },
+ {
+ "name": "gray-matter",
+ "stars": 3800,
+ "forks": 210
+ }
],
"soUserId": null,
"soReputation": 4200,
"soAnswers": 85,
"soAcceptRate": 50,
- "soBadges": 15
+ "soBadges": 15,
+ "metricsUpdatedAt": "2026-08-05T00:00:00Z"
},
{
"login": "brendangregg",
@@ -402,22 +723,50 @@
"followers": 16000,
"topLanguage": "C",
"languages": [
- { "name": "C", "percent": 40 },
- { "name": "DTrace", "percent": 25 },
- { "name": "Shell", "percent": 20 },
- { "name": "Perl", "percent": 10 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "C",
+ "percent": 40
+ },
+ {
+ "name": "DTrace",
+ "percent": 25
+ },
+ {
+ "name": "Shell",
+ "percent": 20
+ },
+ {
+ "name": "Perl",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "perf-tools", "stars": 9700, "forks": 1700 },
- { "name": "FlameGraph", "stars": 17000, "forks": 1900 },
- { "name": "bcc", "stars": 2100, "forks": 410 }
+ {
+ "name": "perf-tools",
+ "stars": 9700,
+ "forks": 1700
+ },
+ {
+ "name": "FlameGraph",
+ "stars": 17000,
+ "forks": 1900
+ },
+ {
+ "name": "bcc",
+ "stars": 2100,
+ "forks": 410
+ }
],
"soUserId": null,
"soReputation": 5800,
"soAnswers": 42,
"soAcceptRate": 65,
- "soBadges": 18
+ "soBadges": 18,
+ "metricsUpdatedAt": "2026-08-04T00:00:00Z"
},
{
"login": "mitchellh",
@@ -433,21 +782,46 @@
"followers": 32000,
"topLanguage": "Zig",
"languages": [
- { "name": "Zig", "percent": 35 },
- { "name": "Go", "percent": 30 },
- { "name": "Ruby", "percent": 20 },
- { "name": "Other", "percent": 15 }
+ {
+ "name": "Zig",
+ "percent": 35
+ },
+ {
+ "name": "Go",
+ "percent": 30
+ },
+ {
+ "name": "Ruby",
+ "percent": 20
+ },
+ {
+ "name": "Other",
+ "percent": 15
+ }
],
"topRepos": [
- { "name": "vagrant", "stars": 26000, "forks": 4500 },
- { "name": "ghostty", "stars": 22000, "forks": 680 },
- { "name": "packer", "stars": 15000, "forks": 3400 }
+ {
+ "name": "vagrant",
+ "stars": 26000,
+ "forks": 4500
+ },
+ {
+ "name": "ghostty",
+ "stars": 22000,
+ "forks": 680
+ },
+ {
+ "name": "packer",
+ "stars": 15000,
+ "forks": 3400
+ }
],
"soUserId": 84745,
"soReputation": 22000,
"soAnswers": 180,
"soAcceptRate": 48,
- "soBadges": 35
+ "soBadges": 35,
+ "metricsUpdatedAt": "2026-08-02T00:00:00Z"
},
{
"login": "fabpot",
@@ -463,21 +837,46 @@
"followers": 21000,
"topLanguage": "PHP",
"languages": [
- { "name": "PHP", "percent": 90 },
- { "name": "Twig", "percent": 5 },
- { "name": "JavaScript", "percent": 3 },
- { "name": "Other", "percent": 2 }
+ {
+ "name": "PHP",
+ "percent": 90
+ },
+ {
+ "name": "Twig",
+ "percent": 5
+ },
+ {
+ "name": "JavaScript",
+ "percent": 3
+ },
+ {
+ "name": "Other",
+ "percent": 2
+ }
],
"topRepos": [
- { "name": "symfony", "stars": 29500, "forks": 9500 },
- { "name": "Twig", "stars": 8100, "forks": 1200 },
- { "name": "Silex", "stars": 3600, "forks": 720 }
+ {
+ "name": "symfony",
+ "stars": 29500,
+ "forks": 9500
+ },
+ {
+ "name": "Twig",
+ "stars": 8100,
+ "forks": 1200
+ },
+ {
+ "name": "Silex",
+ "stars": 3600,
+ "forks": 720
+ }
],
"soUserId": 42983,
"soReputation": 31000,
"soAnswers": 520,
"soAcceptRate": 58,
- "soBadges": 72
+ "soBadges": 72,
+ "metricsUpdatedAt": "2026-07-23T00:00:00Z"
},
{
"login": "mattn",
@@ -493,22 +892,50 @@
"followers": 12000,
"topLanguage": "Go",
"languages": [
- { "name": "Go", "percent": 45 },
- { "name": "Vim Script", "percent": 20 },
- { "name": "C", "percent": 15 },
- { "name": "JavaScript", "percent": 10 },
- { "name": "Other", "percent": 10 }
+ {
+ "name": "Go",
+ "percent": 45
+ },
+ {
+ "name": "Vim Script",
+ "percent": 20
+ },
+ {
+ "name": "C",
+ "percent": 15
+ },
+ {
+ "name": "JavaScript",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 10
+ }
],
"topRepos": [
- { "name": "go-sqlite3", "stars": 7800, "forks": 1100 },
- { "name": "vim-gist", "stars": 1800, "forks": 210 },
- { "name": "goreman", "stars": 1400, "forks": 160 }
+ {
+ "name": "go-sqlite3",
+ "stars": 7800,
+ "forks": 1100
+ },
+ {
+ "name": "vim-gist",
+ "stars": 1800,
+ "forks": 210
+ },
+ {
+ "name": "goreman",
+ "stars": 1400,
+ "forks": 160
+ }
],
"soUserId": 340289,
"soReputation": 18000,
"soAnswers": 680,
"soAcceptRate": 42,
- "soBadges": 45
+ "soBadges": 45,
+ "metricsUpdatedAt": "2026-07-18T00:00:00Z"
},
{
"login": "kennethreitz",
@@ -524,21 +951,46 @@
"followers": 35000,
"topLanguage": "Python",
"languages": [
- { "name": "Python", "percent": 85 },
- { "name": "Shell", "percent": 8 },
- { "name": "Makefile", "percent": 4 },
- { "name": "Other", "percent": 3 }
+ {
+ "name": "Python",
+ "percent": 85
+ },
+ {
+ "name": "Shell",
+ "percent": 8
+ },
+ {
+ "name": "Makefile",
+ "percent": 4
+ },
+ {
+ "name": "Other",
+ "percent": 3
+ }
],
"topRepos": [
- { "name": "requests", "stars": 52000, "forks": 9300 },
- { "name": "httpbin", "stars": 12600, "forks": 2100 },
- { "name": "pipenv", "stars": 24800, "forks": 1900 }
+ {
+ "name": "requests",
+ "stars": 52000,
+ "forks": 9300
+ },
+ {
+ "name": "httpbin",
+ "stars": 12600,
+ "forks": 2100
+ },
+ {
+ "name": "pipenv",
+ "stars": 24800,
+ "forks": 1900
+ }
],
"soUserId": 356942,
"soReputation": 28000,
"soAnswers": 240,
"soAcceptRate": 50,
- "soBadges": 42
+ "soBadges": 42,
+ "metricsUpdatedAt": "2026-08-06T00:00:00Z"
},
{
"login": "substack",
@@ -554,21 +1006,46 @@
"followers": 11000,
"topLanguage": "JavaScript",
"languages": [
- { "name": "JavaScript", "percent": 70 },
- { "name": "Rust", "percent": 15 },
- { "name": "C", "percent": 10 },
- { "name": "Other", "percent": 5 }
+ {
+ "name": "JavaScript",
+ "percent": 70
+ },
+ {
+ "name": "Rust",
+ "percent": 15
+ },
+ {
+ "name": "C",
+ "percent": 10
+ },
+ {
+ "name": "Other",
+ "percent": 5
+ }
],
"topRepos": [
- { "name": "browserify", "stars": 14500, "forks": 1200 },
- { "name": "stream-handbook", "stars": 13600, "forks": 990 },
- { "name": "tape", "stars": 5800, "forks": 330 }
+ {
+ "name": "browserify",
+ "stars": 14500,
+ "forks": 1200
+ },
+ {
+ "name": "stream-handbook",
+ "stars": 13600,
+ "forks": 990
+ },
+ {
+ "name": "tape",
+ "stars": 5800,
+ "forks": 330
+ }
],
"soUserId": null,
"soReputation": 6200,
"soAnswers": 110,
"soAcceptRate": 45,
- "soBadges": 20
+ "soBadges": 20,
+ "metricsUpdatedAt": "2026-06-28T00:00:00Z"
},
{
"login": "jonsuh",
@@ -584,18 +1061,36 @@
"followers": 9200,
"topLanguage": "C#",
"languages": [
- { "name": "C#", "percent": 70 },
- { "name": "Java", "percent": 20 },
- { "name": "Other", "percent": 10 }
+ {
+ "name": "C#",
+ "percent": 70
+ },
+ {
+ "name": "Java",
+ "percent": 20
+ },
+ {
+ "name": "Other",
+ "percent": 10
+ }
],
"topRepos": [
- { "name": "nodatime", "stars": 2700, "forks": 380 },
- { "name": "google-cloud-dotnet", "stars": 920, "forks": 480 }
+ {
+ "name": "nodatime",
+ "stars": 2700,
+ "forks": 380
+ },
+ {
+ "name": "google-cloud-dotnet",
+ "stars": 920,
+ "forks": 480
+ }
],
"soUserId": 22656,
"soReputation": 1410000,
"soAnswers": 36000,
"soAcceptRate": 82,
- "soBadges": 890
+ "soBadges": 890,
+ "metricsUpdatedAt": null
}
-]
+]
\ No newline at end of file
diff --git a/lib/format.js b/lib/format.js
index cf1ffed..60682d7 100644
--- a/lib/format.js
+++ b/lib/format.js
@@ -4,3 +4,38 @@ export function formatNum(n) {
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return n.toString();
}
+
+// Days after which we consider source metrics "stale" for display purposes.
+export const STALE_DATA_DAYS = 30;
+
+/**
+ * Formats an ISO timestamp as a short relative string ("3 days ago"),
+ * for showing when a profile's underlying metrics were last refreshed.
+ * Returns null for missing/invalid input so callers can fall back to an
+ * explicit "freshness unknown" message instead of showing a wrong date.
+ */
+export function formatRelativeTime(isoString) {
+ if (!isoString) return null;
+ const date = new Date(isoString);
+ if (Number.isNaN(date.getTime())) return null;
+
+ const diffMs = Date.now() - date.getTime();
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffDays < 0) return 'just now';
+ if (diffDays === 0) return 'today';
+ if (diffDays === 1) return 'yesterday';
+ if (diffDays < 30) return `${diffDays} days ago`;
+ const diffMonths = Math.floor(diffDays / 30);
+ if (diffMonths < 12) return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
+ const diffYears = Math.floor(diffMonths / 12);
+ return `${diffYears} year${diffYears > 1 ? 's' : ''} ago`;
+}
+
+export function isStaleData(isoString, staleDays = STALE_DATA_DAYS) {
+ if (!isoString) return true;
+ const date = new Date(isoString);
+ if (Number.isNaN(date.getTime())) return true;
+ const diffDays = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24);
+ return diffDays > staleDays;
+}
\ No newline at end of file
diff --git a/lib/scoring.js b/lib/scoring.js
index cb65215..1d6edd8 100644
--- a/lib/scoring.js
+++ b/lib/scoring.js
@@ -1,3 +1,20 @@
+/**
+ * DevGlobe score
+ * -----------------
+ * The 0-100 score is a *relative ranking signal*, not an absolute measure of
+ * developer skill or worth. Every dimension is normalized against the
+ * maximum value seen in the current indexed dataset, so a score reflects
+ * "how this profile compares to the developers DevGlobe has indexed" ā
+ * it will shift as the dataset grows or changes, and it says nothing about
+ * developers who aren't indexed.
+ *
+ * When a profile has no linked Stack Overflow activity, the two SO-based
+ * dimensions (soReputation, soEngagement) contribute 0 and their combined
+ * 25% weight is redistributed proportionally across the three GitHub-only
+ * dimensions (stars, commits, repoReach) plus community, so the score stays
+ * comparable across profiles with and without a Stack Overflow presence.
+ */
+
// Weights for each dimension
const WEIGHTS = {
stars: 0.25,
@@ -8,6 +25,53 @@ const WEIGHTS = {
community: 0.05,
};
+// Human-readable metadata for each scoring dimension, used to render the
+// score breakdown in the UI (leaderboard tooltip + profile detail panel).
+export const DIMENSIONS = [
+ {
+ key: 'stars',
+ label: 'GitHub Stars',
+ description: 'Total stars across owned repositories, log-scaled against the dataset maximum.',
+ },
+ {
+ key: 'commits',
+ label: 'Commit Activity',
+ description: 'Total tracked commit contributions, log-scaled against the dataset maximum.',
+ },
+ {
+ key: 'repoReach',
+ label: 'Repo Reach',
+ description: 'Combined forks and watchers across owned repositories, log-scaled.',
+ },
+ {
+ key: 'soReputation',
+ label: 'Stack Overflow Reputation',
+ description: 'Stack Overflow reputation points, log-scaled. Zero if no SO profile is linked.',
+ },
+ {
+ key: 'soEngagement',
+ label: 'Stack Overflow Engagement',
+ description: 'Accepted-answer rate weighted by answer volume. Zero if no SO profile is linked.',
+ },
+ {
+ key: 'community',
+ label: 'Community',
+ description: 'Combined GitHub followers and Stack Overflow badges, log-scaled.',
+ },
+];
+
+// Neutral, non-evaluative copy for surfacing the score in the UI. Avoid
+// language that implies the score measures overall developer ability.
+export const SCORE_METHODOLOGY = {
+ short:
+ 'Relative ranking signal (0-100) combining GitHub and Stack Overflow activity, ' +
+ 'calibrated against developers currently indexed by DevGlobe ā not an absolute ' +
+ 'measure of skill.',
+ noSO:
+ 'This profile has no linked Stack Overflow activity, so that 25% weight was ' +
+ 'redistributed across GitHub-based dimensions.',
+};
+
function logNormalize(value, max) {
if (value <= 0 || max <= 0) return 0;
return Math.log(1 + value) / Math.log(1 + max);
@@ -52,7 +116,56 @@ function computeScore(dev, maxValues) {
score += dimensions[key] * weight;
}
- return { total: Math.round(score * 100), dimensions };
+ return { total: Math.round(score * 100), dimensions, weights, hasSO };
+}
+
+/**
+ * Builds a structured, UI-ready breakdown of how a score was produced:
+ * each dimension's weight, normalized value (0-1), and point contribution
+ * to the final 0-100 score. Intended for the profile detail panel and any
+ * "why this score" explanation surface.
+ *
+ * Works the same whether the profile has complete, missing, or partial
+ * source data ā dimensions with no underlying data simply normalize to 0
+ * and contribute 0 points (see `hasSO` for whether weight was redistributed).
+ */
+export function explainScore(dev, maxValues) {
+ const { total, dimensions, weights, hasSO } = computeScore(dev, maxValues);
+
+ const breakdown = DIMENSIONS.map(({ key, label, description }) => {
+ const normalized = dimensions[key] || 0;
+ const weight = weights[key] || 0;
+ return {
+ key,
+ label,
+ description,
+ weight,
+ weightPercent: Math.round(weight * 100),
+ normalized,
+ contributionPoints: Math.round(normalized * weight * 100),
+ };
+ });
+
+ return {
+ total,
+ hasSO,
+ redistributed: !hasSO,
+ breakdown,
+ methodology: hasSO ? SCORE_METHODOLOGY.short : `${SCORE_METHODOLOGY.short} ${SCORE_METHODOLOGY.noSO}`,
+ };
+}
+
+/**
+ * Returns what percentage of scores in `allScores` are strictly below
+ * `score` ā used to describe calibration ("higher than N% of indexed
+ * developers") without implying the score itself is an absolute measure.
+ * Returns null for an empty dataset, so callers can omit the line entirely
+ * rather than showing a misleading 0%/100%.
+ */
+export function getScorePercentile(score, allScores) {
+ if (!allScores || allScores.length === 0) return null;
+ const below = allScores.filter(s => s < score).length;
+ return Math.round((below / allScores.length) * 100);
}
export function scoreAll(developers) {
@@ -65,11 +178,15 @@ export function scoreAll(developers) {
community: Math.max(...developers.map(d => (d.followers || 0) + (d.soBadges || 0))),
};
- return developers
- .map(dev => {
- const { total, dimensions } = computeScore(dev, maxValues);
- return { ...dev, score: total, scoreDimensions: dimensions };
- })
+ const scored = developers.map(dev => {
+ const { total, dimensions, hasSO, weights } = computeScore(dev, maxValues);
+ return { ...dev, score: total, scoreDimensions: dimensions, scoreWeights: weights, scoreHasSO: hasSO };
+ });
+
+ const allScores = scored.map(d => d.score);
+
+ return scored
+ .map(dev => ({ ...dev, scorePercentile: getScorePercentile(dev.score, allScores) }))
.sort((a, b) => b.score - a.score);
}
@@ -81,4 +198,4 @@ export function getPlatformColor(dimensions) {
return '#3b82f6';
}
-export { WEIGHTS };
+export { WEIGHTS };
\ No newline at end of file
diff --git a/scripts/build-dataset.js b/scripts/build-dataset.js
index b337996..b246f9b 100644
--- a/scripts/build-dataset.js
+++ b/scripts/build-dataset.js
@@ -11,6 +11,8 @@
import 'dotenv/config';
import { execSync } from 'child_process';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
+import { scoreAll } from '../lib/scoring.js';
+import { addDeveloperRanks } from '../lib/ranking.js';
function run(script, description) {
console.log(`\n${'='.repeat(60)}`);
@@ -48,54 +50,38 @@ async function main() {
console.log(' STEP: Computing composite scores');
console.log(`${'='.repeat(60)}\n`);
- const developers = JSON.parse(readFileSync('data/github-so-geo.json', 'utf-8'));
-
- // Import scoring logic (reuse browser module logic)
- function logNormalize(value, max) {
- if (value <= 0 || max <= 0) return 0;
- return Math.log(1 + value) / Math.log(1 + max);
- }
-
- function linearNormalize(value, max) {
- if (max <= 0) return 0;
- return Math.min(value / max, 1);
- }
-
- const maxValues = {
- stars: Math.max(...developers.map(d => d.totalStars || 0)),
- commits: Math.max(...developers.map(d => d.totalCommits || 0)),
- repoReach: Math.max(...developers.map(d => (d.totalForks || 0) + (d.totalWatchers || 0))),
- soReputation: Math.max(...developers.map(d => d.soReputation || 0)),
- soEngagement: Math.max(...developers.map(d => ((d.soAcceptRate || 0) / 100) * (d.soAnswers || 0))),
- community: Math.max(...developers.map(d => (d.followers || 0) + (d.soBadges || 0)))
- };
-
- const WEIGHTS = { stars: 0.25, commits: 0.25, repoReach: 0.20, soReputation: 0.15, soEngagement: 0.10, community: 0.05 };
-
- const scored = developers.map(dev => {
- const dimensions = {
- stars: logNormalize(dev.totalStars || 0, maxValues.stars),
- commits: logNormalize(dev.totalCommits || 0, maxValues.commits),
- repoReach: logNormalize((dev.totalForks || 0) + (dev.totalWatchers || 0), maxValues.repoReach),
- soReputation: logNormalize(dev.soReputation || 0, maxValues.soReputation),
- soEngagement: linearNormalize(((dev.soAcceptRate || 0) / 100) * (dev.soAnswers || 0), maxValues.soEngagement),
- community: logNormalize((dev.followers || 0) + (dev.soBadges || 0), maxValues.community)
- };
-
- let score = 0;
- for (const [key, weight] of Object.entries(WEIGHTS)) {
- score += dimensions[key] * weight;
- }
-
- return { ...dev, score: Math.round(score * 100), scoreDimensions: dimensions };
- }).sort((a, b) => b.score - a.score);
+ const developers = JSON.parse(readFileSync('data/github-so-geo.json', 'utf-8'))
+ // Freshness: record when each profile's metrics were captured, so the UI
+ // can show "last refreshed" and flag stale profiles. githubFetchedAt is
+ // always present (every profile goes through fetch-github.js); soFetchedAt
+ // only exists when a Stack Overflow match was found.
+ .map(dev => ({
+ ...dev,
+ metricsUpdatedAt: dev.soFetchedAt && dev.soFetchedAt < dev.githubFetchedAt
+ ? dev.soFetchedAt
+ : dev.githubFetchedAt || dev.soFetchedAt || null,
+ }));
+
+ // Reuse the exact same scoring module the frontend uses, so the shipped
+ // dataset's scores are always consistent with what the app would compute
+ // client-side ā no risk of the two implementations drifting apart.
+ const scored = addDeveloperRanks(scoreAll(developers));
// Write final output
writeFileSync('data/developers.json', JSON.stringify(scored, null, 2));
+ // Score distribution summary, for reviewing calibration (spread, outliers,
+ // how many profiles land in sparse-data territory) each time the dataset
+ // is rebuilt.
+ const scores = scored.map(d => d.score).sort((a, b) => a - b);
+ const pct = p => scores[Math.min(scores.length - 1, Math.floor((p / 100) * scores.length))];
+ const sparseCount = scored.filter(d => !d.scoreHasSO).length;
+
console.log(`\nā
Pipeline complete!`);
console.log(` Total developers: ${scored.length}`);
console.log(` Top scorer: ${scored[0]?.login} (${scored[0]?.score}/100)`);
+ console.log(` Score distribution: min=${scores[0]} p25=${pct(25)} median=${pct(50)} p75=${pct(75)} max=${scores[scores.length - 1]}`);
+ console.log(` Profiles without Stack Overflow data (weight redistributed): ${sparseCount}/${scored.length}`);
console.log(` Output: data/developers.json`);
console.log(`\n To use in the app, update DATA_URL in src/app.js to 'data/developers.json'`);
}
@@ -103,4 +89,4 @@ async function main() {
main().catch(err => {
console.error('\nā Pipeline failed:', err.message);
process.exit(1);
-});
+});
\ No newline at end of file
diff --git a/scripts/fetch-github.js b/scripts/fetch-github.js
index 8dbba5e..d46b99a 100644
--- a/scripts/fetch-github.js
+++ b/scripts/fetch-github.js
@@ -204,7 +204,8 @@ function processUser(user) {
name: r.name,
stars: r.stargazerCount,
forks: r.forkCount
- }))
+ })),
+ githubFetchedAt: new Date().toISOString(),
};
}
@@ -266,4 +267,4 @@ async function main() {
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
-});
+});
\ No newline at end of file
diff --git a/scripts/fetch-stackoverflow.js b/scripts/fetch-stackoverflow.js
index cb87155..30c5c4f 100644
--- a/scripts/fetch-stackoverflow.js
+++ b/scripts/fetch-stackoverflow.js
@@ -141,7 +141,8 @@ async function main() {
soAcceptRate: match.accept_rate || 0,
soBadges: (match.badge_counts?.gold || 0) +
(match.badge_counts?.silver || 0) +
- (match.badge_counts?.bronze || 0)
+ (match.badge_counts?.bronze || 0),
+ soFetchedAt: new Date().toISOString(),
});
console.log(` ā Matched: ${match.display_name} (rep: ${match.reputation}, answers: ${answerCount})`);
} else {
@@ -179,4 +180,4 @@ async function main() {
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
-});
+});
\ No newline at end of file
diff --git a/styles/main.css b/styles/main.css
index ffcd59b..a6072f0 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -556,6 +556,14 @@ body {
text-overflow: ellipsis;
}
+.sidebar__score-note {
+ margin: 6px 0 0;
+ font-size: 10.5px;
+ line-height: 1.4;
+ color: var(--text-muted);
+ cursor: help;
+}
+
.sidebar__list {
list-style: none;
overflow-y: auto;
@@ -747,6 +755,15 @@ body {
border-radius: 12px;
font-size: 12px;
font-weight: 600;
+ cursor: help;
+}
+
+.detail-header__score-note {
+ font-size: 11px;
+ line-height: 1.4;
+ color: var(--text-muted);
+ margin-top: 6px;
+ max-width: 46ch;
}
.rank-badge {
@@ -812,6 +829,86 @@ body {
margin-bottom: 12px;
}
+/* Score explanation (dimension/weight breakdown + calibration + freshness) */
+.score-explain {
+ margin-top: 10px;
+ font-size: 11px;
+ color: var(--text-secondary);
+}
+
+.score-explain__methodology {
+ line-height: 1.4;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+}
+
+.score-explain__redistribute {
+ line-height: 1.4;
+ color: var(--accent-blue);
+ margin-bottom: 8px;
+}
+
+.score-explain__list {
+ list-style: none;
+ padding: 0;
+ margin: 8px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.score-explain__row-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 8px;
+}
+
+.score-explain__label {
+ font-weight: 600;
+ color: var(--text-primary);
+ cursor: help;
+}
+
+.score-explain__points {
+ font-variant-numeric: tabular-nums;
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+
+.score-explain__track {
+ height: 4px;
+ border-radius: 2px;
+ background: var(--bg-card);
+ overflow: hidden;
+ margin: 4px 0 2px;
+}
+
+.score-explain__fill {
+ height: 100%;
+ background: var(--accent-blue);
+ border-radius: 2px;
+}
+
+.score-explain__weight {
+ font-size: 10px;
+ color: var(--text-muted);
+}
+
+.score-explain__percentile {
+ margin-top: 8px;
+ color: var(--text-secondary);
+}
+
+.score-explain__freshness {
+ margin-top: 4px;
+ color: var(--text-muted);
+}
+
+.score-explain__freshness--stale {
+ color: #fbbf24;
+}
+
/* Radar chart */
#chart-radar svg {
width: 100%;