-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
211 lines (184 loc) · 7.16 KB
/
Copy pathscript.js
File metadata and controls
211 lines (184 loc) · 7.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// OrbitOrg — GitHub Organization Explorer
// Vanilla JS, GitHub REST API, no backend, no auth required (subject to
// GitHub's unauthenticated rate limit of 60 requests/hour per IP).
const LANG_COLORS = {
JavaScript: '#e8b34c',
TypeScript: '#7d9ce8',
Python: '#7cc48f',
HTML: '#e8815f',
CSS: '#c98ae8',
Go: '#5fd0d8',
Rust: '#e0725f',
Java: '#e8a15f',
Ruby: '#e85f80',
Shell: '#9ad46a',
'C++': '#f06e9e',
C: '#a4a7c9',
default: '#9a9bc0',
};
const svgEl = document.getElementById('sky');
const input = document.getElementById('org-input');
const btn = document.getElementById('chart-btn');
const status = document.getElementById('status-line');
const summaryEl = document.getElementById('org-summary');
const legend = document.getElementById('legend');
const legendKeys = document.getElementById('legend-keys');
const detailPanel = document.getElementById('detail-panel');
const SVG_NS = 'http://www.w3.org/2000/svg';
const CENTER = { x: 450, y: 320 };
const MAX_RADIUS = 260;
btn.addEventListener('click', () => loadOrg(input.value.trim()));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') loadOrg(input.value.trim());
});
async function loadOrg(org) {
if (!org) {
setStatus('Type an organization name to begin.', false);
return;
}
setBusy(true);
setStatus(`Charting ${org}…`, false);
clearSky();
detailPanel.classList.add('hidden');
try {
const orgRes = await fetch(`https://api.github.com/orgs/${encodeURIComponent(org)}`);
if (!orgRes.ok) {
if (orgRes.status === 404) throw new Error(`No organization named "${org}" was found.`);
if (orgRes.status === 403) throw new Error('GitHub API rate limit reached. Try again in a few minutes.');
throw new Error('GitHub API returned an unexpected error.');
}
const orgData = await orgRes.json();
const reposRes = await fetch(`https://api.github.com/orgs/${encodeURIComponent(org)}/repos?per_page=100&sort=pushed`);
if (!reposRes.ok) throw new Error('Could not load repositories for this organization.');
const repos = await reposRes.json();
if (!Array.isArray(repos) || repos.length === 0) {
setStatus(`${org} has no public repositories to chart.`, false);
setBusy(false);
return;
}
renderOrg(orgData, repos);
setStatus(`Showing ${repos.length} repositories for ${org}. Click any point for detail.`, false);
} catch (err) {
setStatus(err.message || 'Something went wrong.', true);
} finally {
setBusy(false);
}
}
function setBusy(busy) {
btn.disabled = busy;
input.disabled = busy;
}
function setStatus(msg, isError) {
status.textContent = msg;
status.classList.toggle('error', !!isError);
}
function clearSky() {
while (svgEl.firstChild) svgEl.removeChild(svgEl.firstChild);
summaryEl.classList.add('hidden');
legend.classList.add('hidden');
}
function renderOrg(org, repos) {
summaryEl.innerHTML = `<strong>${escapeHtml(org.login)}</strong> · ${repos.length} public repos · ${org.followers ?? 0} followers`;
summaryEl.classList.remove('hidden');
const now = Date.now();
const ages = repos.map((r) => now - new Date(r.pushed_at).getTime());
const maxAge = Math.max(...ages, 1);
const stars = repos.map((r) => r.stargazers_count || 0);
const maxStars = Math.max(...stars, 1);
// background rings
[0.35, 0.65, 1].forEach((f) => {
const ring = document.createElementNS(SVG_NS, 'ellipse');
ring.setAttribute('cx', CENTER.x);
ring.setAttribute('cy', CENTER.y);
ring.setAttribute('rx', MAX_RADIUS * f);
ring.setAttribute('ry', MAX_RADIUS * f * 0.62);
ring.setAttribute('class', 'orbit-ring');
svgEl.appendChild(ring);
});
// center node = the org
const centerRing = document.createElementNS(SVG_NS, 'circle');
centerRing.setAttribute('cx', CENTER.x);
centerRing.setAttribute('cy', CENTER.y);
centerRing.setAttribute('r', 18);
centerRing.setAttribute('class', 'center-ring');
svgEl.appendChild(centerRing);
const centerNode = document.createElementNS(SVG_NS, 'circle');
centerNode.setAttribute('cx', CENTER.x);
centerNode.setAttribute('cy', CENTER.y);
centerNode.setAttribute('r', 9);
centerNode.setAttribute('class', 'center-node');
svgEl.appendChild(centerNode);
const languagesSeen = new Set();
const group = document.createElementNS(SVG_NS, 'g');
group.setAttribute('class', 'spin-slow');
svgEl.appendChild(group);
repos.forEach((repo, i) => {
const age = now - new Date(repo.pushed_at).getTime();
const distFrac = 0.18 + 0.82 * (age / maxAge); // recently pushed => closer
const radius = MAX_RADIUS * distFrac;
const angle = (i / repos.length) * Math.PI * 2 + (i % 3) * 0.35;
const x = CENTER.x + radius * Math.cos(angle);
const y = CENTER.y + radius * 0.62 * Math.sin(angle);
const starFrac = (repo.stargazers_count || 0) / maxStars;
const nodeR = 4 + Math.sqrt(starFrac) * 14;
const lang = repo.language || 'default';
languagesSeen.add(lang);
const color = LANG_COLORS[lang] || LANG_COLORS.default;
const node = document.createElementNS(SVG_NS, 'circle');
node.setAttribute('cx', x);
node.setAttribute('cy', y);
node.setAttribute('r', nodeR);
node.setAttribute('fill', color);
node.setAttribute('class', 'repo-node');
node.setAttribute('tabindex', '0');
node.setAttribute('role', 'button');
node.setAttribute('aria-label', `${repo.name}, ${repo.stargazers_count} stars, ${lang}`);
node.addEventListener('click', () => showDetail(repo));
node.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); showDetail(repo); }
});
group.appendChild(node);
if (nodeR > 9) {
const label = document.createElementNS(SVG_NS, 'text');
label.setAttribute('x', x);
label.setAttribute('y', y - nodeR - 4);
label.setAttribute('text-anchor', 'middle');
label.setAttribute('class', 'repo-label');
label.textContent = repo.name;
group.appendChild(label);
}
});
renderLegend(languagesSeen);
}
function renderLegend(languages) {
legendKeys.innerHTML = '';
Array.from(languages).sort().forEach((lang) => {
const item = document.createElement('span');
item.className = 'legend-key';
const swatch = document.createElement('span');
swatch.className = 'legend-swatch';
swatch.style.background = LANG_COLORS[lang] || LANG_COLORS.default;
item.appendChild(swatch);
item.appendChild(document.createTextNode(lang));
legendKeys.appendChild(item);
});
legend.classList.remove('hidden');
}
function showDetail(repo) {
detailPanel.innerHTML = `
<h3><a href="${repo.html_url}" target="_blank" rel="noopener">${escapeHtml(repo.name)}</a></h3>
<p>${escapeHtml(repo.description || 'No description provided.')}</p>
<div class="detail-meta">
<span>★ ${repo.stargazers_count}</span>
<span>${repo.language || 'Unknown language'}</span>
<span>Updated ${new Date(repo.pushed_at).toLocaleDateString()}</span>
</div>
`;
detailPanel.classList.remove('hidden');
detailPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}