Skip to content

Commit 8946f79

Browse files
committed
feat(site): visualize dataset growth history
Refs #19
1 parent 0b0e944 commit 8946f79

3 files changed

Lines changed: 181 additions & 30 deletions

File tree

site/src/pages/index.astro

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ const endpoints = [
111111
<div class="sec-head">
112112
<div>
113113
<span class="kicker">00 - History</span>
114-
<h2 style="margin-top:14px">Dataset growth, synced live</h2>
115-
<p class="sec-sub">Counts come from the published static dump, while recent syncs come from repository activity when GitHub is reachable.</p>
114+
<h2 style="margin-top:14px">Dataset growth over time</h2>
115+
<p class="sec-sub">Recent dump commits are replayed into a small growth chart, showing how many records each sync added.</p>
116116
</div>
117117
<span class="num">v1/index.json</span>
118118
</div>
@@ -124,7 +124,10 @@ const endpoints = [
124124
<div class="history-grid" id="history-counts"></div>
125125
</div>
126126
<div class="history-panel">
127-
<div class="history-label">Recent syncs</div>
127+
<div class="history-label">Growth timeline</div>
128+
<div class="history-chart" id="history-chart" aria-label="Dataset record growth chart">
129+
<div class="history-empty">Loading growth chart...</div>
130+
</div>
128131
<ol class="history-list" id="history-list">
129132
<li><span class="history-dot"></span><span>Loading repository history...</span></li>
130133
</ol>

site/src/scripts/techapi.js

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -191,46 +191,129 @@ function countUp(node, target) {
191191
(function history() {
192192
const totalEl = document.getElementById("history-total");
193193
const countsEl = document.getElementById("history-counts");
194+
const chartEl = document.getElementById("history-chart");
194195
const listEl = document.getElementById("history-list");
195-
if (!totalEl || !countsEl || !listEl) return;
196+
if (!totalEl || !countsEl || !chartEl || !listEl) return;
196197

197198
const order = ["smartphones", "socs", "gpus", "cpus", "brands"];
198199
const label = { smartphones: "Phones", socs: "SoCs", gpus: "GPUs", cpus: "CPUs", brands: "Brands" };
200+
const shortLabel = { smartphones: "phones", socs: "socs", gpus: "gpus", cpus: "cpus", brands: "brands" };
201+
const dumpPath = "site/public/v1/index.json";
202+
const countRows = (manifest) => order
203+
.map((key) => ({ key, count: manifest.collections?.[key]?.count }))
204+
.filter((row) => row.count != null);
205+
const totalRecords = (manifest) => countRows(manifest).reduce((sum, row) => sum + row.count, 0);
206+
const sumByKey = (rows) => rows.reduce((out, row) => {
207+
out[row.key] = row.count;
208+
return out;
209+
}, {});
199210

200-
getJSON("v1/index.json").then((manifest) => {
201-
const rows = order
202-
.map((key) => ({ key, count: manifest.collections?.[key]?.count }))
203-
.filter((row) => row.count != null);
211+
function renderSnapshot(manifest) {
212+
const rows = countRows(manifest);
204213
const total = rows.reduce((sum, row) => sum + row.count, 0);
205214
totalEl.textContent = total.toLocaleString() + " records";
206215
countsEl.innerHTML = rows.map((row) =>
207216
`<div class="history-count"><span>${label[row.key]}</span><b>${row.count.toLocaleString()}</b></div>`
208217
).join("");
218+
}
219+
220+
function largestChanges(prevRows, nextRows) {
221+
if (!prevRows) return [];
222+
const prev = sumByKey(prevRows);
223+
return nextRows
224+
.map((row) => ({ key: row.key, delta: row.count - (prev[row.key] || 0) }))
225+
.filter((row) => row.delta > 0)
226+
.sort((a, b) => b.delta - a.delta)
227+
.slice(0, 2);
228+
}
229+
230+
function renderHistory(points) {
231+
if (!points.length) throw new Error("empty history");
232+
const maxTotal = Math.max(...points.map((point) => point.total));
233+
const minTotal = Math.min(...points.map((point) => point.total));
234+
const range = Math.max(1, maxTotal - minTotal);
235+
chartEl.innerHTML = points.map((point) => {
236+
const pct = 18 + ((point.total - minTotal) / range) * 82;
237+
const deltaText = point.delta > 0 ? "+" + point.delta.toLocaleString() : "baseline";
238+
return `<a class="history-bar" href="${esc(point.url)}" target="_blank" rel="noopener" style="--h:${pct.toFixed(1)}%" title="${esc(point.title)}">
239+
<span class="history-bar-fill"></span>
240+
<span class="history-bar-total">${point.total.toLocaleString()}</span>
241+
<span class="history-bar-delta">${esc(deltaText)}</span>
242+
</a>`;
243+
}).join("");
244+
245+
listEl.innerHTML = points.slice().reverse().map((point) => {
246+
const changes = point.changes.length
247+
? point.changes.map((row) => `${shortLabel[row.key]} +${row.delta.toLocaleString()}`).join(", ")
248+
: (point.delta > 0 ? `total +${point.delta.toLocaleString()}` : "baseline snapshot");
249+
return `<li><span class="history-dot"></span><span>
250+
<a href="${esc(point.url)}" target="_blank" rel="noopener">${esc(point.title)}</a>
251+
<small>${esc(point.when)} - ${esc(point.sha)} - ${esc(changes)}</small>
252+
</span></li>`;
253+
}).join("");
254+
}
255+
256+
async function loadCommitHistory(currentManifest) {
257+
const commitsUrl = `https://api.github.com/repos/GetTechAPI/TechAPI/commits?path=${encodeURIComponent(dumpPath)}&per_page=8`;
258+
const response = await fetch(commitsUrl);
259+
if (!response.ok) throw new Error(response.statusText);
260+
const commits = await response.json();
261+
const items = Array.isArray(commits) ? commits.slice(0, 7) : [];
262+
const snapshots = await Promise.all(items.map(async (item) => {
263+
const sha = String(item.sha || "");
264+
const rawUrl = `https://raw.githubusercontent.com/GetTechAPI/TechAPI/${sha}/${dumpPath}`;
265+
const raw = await fetch(rawUrl);
266+
if (!raw.ok) return null;
267+
const manifest = await raw.json();
268+
const date = item.commit?.committer?.date ? new Date(item.commit.committer.date) : null;
269+
return {
270+
sha: sha.slice(0, 7),
271+
dateValue: date ? date.getTime() : 0,
272+
when: date ? date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "recent",
273+
title: (item.commit?.message || "Dataset sync").split("\n")[0],
274+
url: item.html_url || "https://github.com/GetTechAPI/TechAPI",
275+
rows: countRows(manifest),
276+
total: totalRecords(manifest),
277+
};
278+
}));
279+
280+
const points = snapshots.filter(Boolean).sort((a, b) => a.dateValue - b.dateValue);
281+
if (!points.length) throw new Error("empty history");
282+
283+
const currentTotal = totalRecords(currentManifest);
284+
const latest = points[points.length - 1];
285+
if (latest.total !== currentTotal) {
286+
points.push({
287+
sha: "current",
288+
dateValue: Date.now(),
289+
when: "current",
290+
title: "Current published snapshot",
291+
url: base + "v1/index.json",
292+
rows: countRows(currentManifest),
293+
total: currentTotal,
294+
});
295+
}
296+
297+
for (let i = 0; i < points.length; i++) {
298+
const prev = points[i - 1];
299+
points[i].delta = prev ? points[i].total - prev.total : 0;
300+
points[i].changes = largestChanges(prev?.rows, points[i].rows);
301+
}
302+
renderHistory(points);
303+
}
304+
305+
getJSON("v1/index.json").then((manifest) => {
306+
renderSnapshot(manifest);
307+
return loadCommitHistory(manifest).catch(() => {
308+
chartEl.innerHTML = '<div class="history-empty">Growth chart unavailable</div>';
309+
listEl.innerHTML = '<li><span class="history-dot"></span><span>Current static dump is available; commit history could not be loaded.<small>GitHub API unavailable</small></span></li>';
310+
});
209311
}).catch(() => {
210312
totalEl.textContent = "sync unavailable";
211313
countsEl.innerHTML = '<div class="history-count"><span>Static dump</span><b>offline</b></div>';
314+
chartEl.innerHTML = '<div class="history-empty">Growth chart unavailable</div>';
315+
listEl.innerHTML = '<li><span class="history-dot"></span><span>Current static dump could not be loaded.<small>Build the public data first</small></span></li>';
212316
});
213-
214-
fetch("https://api.github.com/repos/GetTechAPI/TechAPI/commits?path=site/public/v1&per_page=5")
215-
.then((response) => {
216-
if (!response.ok) throw new Error(response.statusText);
217-
return response.json();
218-
})
219-
.then((commits) => {
220-
const items = Array.isArray(commits) ? commits.slice(0, 4) : [];
221-
if (!items.length) throw new Error("empty history");
222-
listEl.innerHTML = items.map((item) => {
223-
const message = (item.commit?.message || "Dataset sync").split("\n")[0];
224-
const date = item.commit?.committer?.date ? new Date(item.commit.committer.date) : null;
225-
const when = date ? date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "recent";
226-
const sha = String(item.sha || "").slice(0, 7);
227-
const url = item.html_url || "https://github.com/GetTechAPI/TechAPI";
228-
return `<li><span class="history-dot"></span><span><a href="${esc(url)}" target="_blank" rel="noopener">${esc(message)}</a><small>${esc(when)} · ${esc(sha)}</small></span></li>`;
229-
}).join("");
230-
})
231-
.catch(() => {
232-
listEl.innerHTML = '<li><span class="history-dot"></span><span>Current static dump is available; repository history could not be loaded.<small>GitHub API unavailable</small></span></li>';
233-
});
234317
})();
235318

236319
/* ============================================================

site/src/styles/techapi.css

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,11 +297,75 @@ code, .mono { font-family: var(--mono); }
297297
.history-count b { color: var(--fg); font-size: 13px; }
298298
.history-list {
299299
list-style: none;
300-
margin: 18px 0 0;
300+
margin: 20px 0 0;
301301
padding: 0;
302302
display: grid;
303303
gap: 14px;
304304
}
305+
.history-chart {
306+
min-height: 210px;
307+
margin-top: 18px;
308+
padding: 16px 12px 12px;
309+
display: grid;
310+
grid-auto-flow: column;
311+
grid-auto-columns: minmax(62px, 1fr);
312+
align-items: end;
313+
gap: 10px;
314+
overflow-x: auto;
315+
border: 1px solid var(--border);
316+
border-radius: var(--radius);
317+
background:
318+
linear-gradient(to top, var(--border) 1px, transparent 1px) 0 25% / 100% 25%,
319+
var(--surface-2);
320+
}
321+
.history-empty {
322+
align-self: center;
323+
justify-self: center;
324+
grid-column: 1 / -1;
325+
font-family: var(--mono);
326+
color: var(--muted);
327+
font-size: 12px;
328+
}
329+
.history-bar {
330+
min-width: 0;
331+
height: 178px;
332+
display: grid;
333+
grid-template-rows: 1fr auto auto;
334+
gap: 6px;
335+
color: var(--fg);
336+
}
337+
.history-bar-fill {
338+
align-self: end;
339+
min-height: 10px;
340+
height: var(--h);
341+
border: 1px solid color-mix(in srgb, var(--accent) 60%, var(--border));
342+
border-radius: 4px 4px 2px 2px;
343+
background:
344+
linear-gradient(180deg, color-mix(in srgb, var(--accent) 92%, white 8%), var(--accent-deep));
345+
box-shadow: 0 0 26px -12px var(--accent);
346+
transition: filter .16s, transform .16s;
347+
}
348+
.history-bar:hover .history-bar-fill {
349+
filter: brightness(1.08);
350+
transform: translateY(-2px);
351+
}
352+
.history-bar-total,
353+
.history-bar-delta {
354+
min-width: 0;
355+
overflow: hidden;
356+
text-overflow: ellipsis;
357+
white-space: nowrap;
358+
font-family: var(--mono);
359+
text-align: center;
360+
}
361+
.history-bar-total {
362+
font-size: 11px;
363+
font-weight: 700;
364+
}
365+
.history-bar-delta {
366+
font-size: 10px;
367+
color: var(--accent-text);
368+
}
305369
.history-list li {
306370
display: grid;
307371
grid-template-columns: 12px 1fr;
@@ -331,6 +395,7 @@ code, .mono { font-family: var(--mono); }
331395
@media (max-width: 760px) {
332396
.history { grid-template-columns: 1fr; }
333397
.history-grid { grid-template-columns: 1fr; }
398+
.history-chart { grid-auto-columns: 68px; }
334399
}
335400

336401
/* ============================================================

0 commit comments

Comments
 (0)