Stats — LEAPPs
Project Health

LEAPPs Stats

Live metrics across iLEAPP, ALEAPP, RLEAPP, VLEAPP, DLEAPP, GLEAPP, LAVA, and Batch LEAPP. Data pulled directly from GitHub.

Fetching data…
Total downloads
—
across all tools
Total stars
—
across all repos
Contributors
—
unique across repos
Total releases
—
across all tools
Total commits
—
across all repos
Unique visitors
—
GitHub repos, last 14 days
Unique cloners
—
GitHub repos, last 14 days

Download totals include checksum file downloads (under 1% for iLEAPP and ALEAPP). Unique visitors and cloners are GitHub’s 14-day figures added across the tool repositories, so a person who visits two repositories counts twice.

By Tool

Downloads by tool
Downloads
Stars by tool
Stars

Download History

Updated 4× daily — 9 PM, 3 AM, 9 AM, 3 PM ET

Loading download history…

Reach

Visits to and clones of the tools’ GitHub repositories, from GitHub’s traffic data. These are repository visits, not downloads. Clones include automated ones, such as CI systems and mirrors, so they are not a count of people.

Daily repository visits

Downloads by Platform

All-time release downloads by operating system, read from each release file’s name. Checksum files are left out. Older releases name no processor type, so the arm64 and x64 split is in each tool’s details below, not here.

Where Visits Come From

GitHub’s top 10 sites that sent people to the repository over the last 14 days, for all tools. These describe repository visits, not downloads. “Seen” counts how many of the recent daily snapshots listed the site.

Stars Over Time

Stars for all tools. Up to October 19, 2026 the line is built from the date each current star was given, so stars removed before then are not in it. From October 20 it is the star count recorded each day, which also falls when someone removes a star.

Per-Tool Metrics

Tool Downloads Stars Forks Open Issues Contributors Releases Commits

Guides

Loading…

Probes

The probes are small single-purpose readers that sit beside the LEAPPs. They are counted here on their own, on their own scale, and are not part of any total or chart above. See the probes

Probe downloads
—
all releases, five probes
Probe stars
—
across the probe repos
Probe releases
—
across the probe repos
Downloads by probe

Repository visits

Downloads by platform

Download history

Loading…
${r.platform}
${fmt(r.downloads)}
${fmt(r.stars)}
${fmt(r.forks)} ${fmt(r.openIssues)} ${r.openSource ? fmt(r.contributors) + '+' : '—'} ${fmt(r.releases)} ${r.openSource ? fmt(r.commits) : '—'} `).join(''); } catch(e) { document.getElementById('lp-updated').textContent = 'Could not load data. Please try again later.'; document.getElementById('stats-tbody').innerHTML = 'Could not load GitHub data. View on GitHub →'; } } function copyMetric(btn, label, valueId) { const val = document.getElementById(valueId).textContent.trim(); if (!val || val === '—') return; navigator.clipboard.writeText(`LEAPPs ${label}: ${val}`).then(() => { btn.classList.add('copied'); const icon = btn.querySelector('i'); icon.className = 'ti ti-check'; setTimeout(() => { btn.classList.remove('copied'); icon.className = 'ti ti-copy'; }, 1500); }); } fetchStats(); // Guides download counts const GUIDE_LABELS = { 'ileapp-module-contributor-guide.pdf': 'iLEAPP Module Contributor Guide', 'apple-unified-logs-ileapp-field-guide.pdf': 'Apple Unified Logs: iLEAPP Field Guide', 'apple-unified-logs-predicate-reference.pdf': 'Apple Unified Log Predicates: The Reference', 'telegram-system-events-reference.pdf': 'Telegram System Events: The Cross-Platform Reference', }; async function fetchGuideCounts() { const el = document.getElementById('guides-content'); try { const res = await fetch('https://leapps-api.4n6-198.workers.dev/downloads/counts'); if (!res.ok) throw new Error(); const counts = await res.json(); const rows = Object.entries(GUIDE_LABELS).map(([file, label]) => { const isTracked = Object.prototype.hasOwnProperty.call(counts, file); const count = Number(counts[file] || 0); // Use tracked delivery when the API knows the file. A direct static // fallback keeps newly published guides available before API deploy. const dlUrl = isTracked ? `https://leapps-api.4n6-198.workers.dev/downloads/${encodeURIComponent(file)}` : `/downloads/${encodeURIComponent(file)}`; return `
${label} ${count.toLocaleString()} downloads
`; }).join(''); el.innerHTML = rows || '

No data yet.

'; } catch (_) { el.innerHTML = '

—

'; } } fetchGuideCounts(); // Download history section const TOOL_COLORS = { ileapp: '#F5161F', aleapp: '#6FD127', rleapp: '#FDEC30', vleapp: '#27CFFC', dleapp: '#CF7BFB', gleapp: '#FD9B41', lava: '#0F6CA4', batchleapp: '#1FB8A6' }; const TOOL_LABELS = { ileapp: 'iLEAPP', aleapp: 'ALEAPP', rleapp: 'RLEAPP', vleapp: 'VLEAPP', dleapp: 'DLEAPP', gleapp: 'GLEAPP', lava: 'LAVA', batchleapp: 'Batch LEAPP' }; let dlHistoryChart = null; let dlSnapshots = []; let dlActivePeriod = 'today'; function toolCountsForPeriod(period) { const tools = Object.keys(TOOL_LABELS); const latest = dlSnapshots[dlSnapshots.length - 1]; const prev = dlSnapshots[dlSnapshots.length - 2]; if (period === 'today') { return tools.reduce((m, k) => { m[k] = Math.max(0, (latest.totals[k]||0) - (prev.totals[k]||0)); return m; }, {}); } const n = period === '7d' ? 7 : period === '30d' ? 30 : 90; const slice = dlSnapshots.length >= n + 1 ? dlSnapshots.slice(-(n + 1)) : dlSnapshots; return tools.reduce((m, k) => { m[k] = slice.slice(1).reduce((s, snap, i) => s + Math.max(0, (snap.totals[k]||0) - (slice[i].totals[k]||0)), 0); return m; }, {}); } function renderToolRows(period) { const tools = Object.keys(TOOL_LABELS); const counts = toolCountsForPeriod(period); const maxVal = Math.max(...Object.values(counts), 1); const prefix = '+'; const suffix = { today: ' today', '7d': ' / 7d', '30d': ' / 30d', '90d': ' / 90d' }[period]; document.getElementById('dl-tool-rows').innerHTML = tools.map(k => { const val = counts[k] || 0; const pct = Math.round(val / maxVal * 100); return `
${TOOL_LABELS[k]}
${val > 0 ? prefix + fmt(val) : '—'}
${val > 0 ? suffix.trim() : ''}
`; }).join(''); document.querySelectorAll('.lp-dl-summary-cell').forEach(el => { el.classList.toggle('active', el.dataset.period === period); }); } function renderDlHistory(days) { const container = document.getElementById('dl-history-content'); if (dlSnapshots.length < 2) { container.innerHTML = '
Daily delta available after two consecutive snapshots. Check back tomorrow.
'; return; } const latest = dlSnapshots[dlSnapshots.length - 1]; const prev = dlSnapshots[dlSnapshots.length - 2]; const tools = Object.keys(TOOL_LABELS); const todayDelta = tools.reduce((s, k) => s + Math.max(0, (latest.totals[k]||0) - (prev.totals[k]||0)), 0); const total = tools.reduce((s, k) => s + (latest.totals[k]||0), 0); const slice = dlSnapshots.slice(-(days + 1)); const deltas = slice.slice(1).map((s, i) => ({ date: s.date, total: tools.reduce((sum, k) => sum + Math.max(0, (s.totals[k]||0) - (slice[i].totals[k]||0)), 0) })); const last7 = dlSnapshots.length >= 8 ? dlSnapshots.slice(-8) : dlSnapshots; const sum7 = last7.slice(1).reduce((s, snap, i) => s + tools.reduce((ts, k) => ts + Math.max(0, (snap.totals[k]||0) - (last7[i].totals[k]||0)), 0), 0); const last30 = dlSnapshots.length >= 31 ? dlSnapshots.slice(-31) : dlSnapshots; const sum30 = last30.slice(1).reduce((s, snap, i) => s + tools.reduce((ts, k) => ts + Math.max(0, (snap.totals[k]||0) - (last30[i].totals[k]||0)), 0), 0); const last90 = dlSnapshots.length >= 91 ? dlSnapshots.slice(-91) : dlSnapshots; const sum90 = last90.slice(1).reduce((s, snap, i) => s + tools.reduce((ts, k) => ts + Math.max(0, (snap.totals[k]||0) - (last90[i].totals[k]||0)), 0), 0); container.innerHTML = `
${todayDelta > 0 ? '+' + fmt(todayDelta) : '—'}
Today
${dlSnapshots.length >= 8 ? fmt(sum7) : '—'}
Last 7 days
${dlSnapshots.length >= 31 ? fmt(sum30) : '—'}
Last 30 days
${dlSnapshots.length >= 91 ? fmt(sum90) : '—'}
Last 90 days
Daily downloads
`; renderToolRows(dlActivePeriod); if (dlHistoryChart) { dlHistoryChart.destroy(); dlHistoryChart = null; } const c = getChartColors(); dlHistoryChart = new Chart(document.getElementById('dlHistoryChart'), { type: 'bar', data: { labels: deltas.map(d => { const dt = new Date(d.date + 'T00:00:00Z'); return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }); }), datasets: [{ data: deltas.map(d => d.total), backgroundColor: deltas.map((_, i) => i === deltas.length - 1 ? '#F5C020' : 'rgba(245,192,32,.35)'), borderRadius: 3, maxBarThickness: 40 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { color: c.tick, font: { size: 10 }, maxRotation: 0 } }, y: { grid: { color: c.grid }, ticks: { color: c.tick, font: { size: 10 }, callback: v => fmt(v) } } } } }); } function switchDlChart(days) { renderDlHistory(days); } async function fetchDlHistory() { try { const res = await fetch('https://leapps-api.4n6-198.workers.dev/downloads/daily'); if (!res.ok) throw new Error(); const data = await res.json(); if (!data.available) { document.getElementById('dl-history-content').innerHTML = '
Daily delta available after two consecutive snapshots. Check back tomorrow.
'; return; } const snapshotRes = await fetch('https://raw.githubusercontent.com/abrignoni/leapps-website/main/data/downloads.json'); const snapshotData = await snapshotRes.json(); dlSnapshots = snapshotData.snapshots || []; renderDlHistory(7); } catch(e) { document.getElementById('dl-history-content').innerHTML = '
Could not load download history.
'; } } fetchDlHistory(); // Probes: a separate list, separate totals, separate chart. Nothing here feeds the LEAPP figures. const PROBES = [ { key: 'qnxprobe', name: 'qnxprobe', what: 'Filesystems in raw images', repo: 'abrignoni/qnxprobe' }, { key: 'ewfprobe', name: 'ewfprobe', what: 'Forensic and disk images', repo: 'abrignoni/ewfprobe' }, { key: 'exoprobe', name: 'exoprobe', what: 'ExoPlayer cached media', repo: 'abrignoni/exoprobe' }, { key: 'chromiumcacheprobe', name: 'chromiumcacheprobe', what: 'Chrome cache on Android', repo: 'abrignoni/chromiumcacheprobe' }, { key: 'mediacarve', name: 'mediacarve', what: 'Signature carving', repo: 'abrignoni/mediacarve' }, ]; let probeChart = null; function probeColor() { return getComputedStyle(document.documentElement).getPropertyValue('--color-probes').trim() || '#8FA9BF'; } async function fetchProbeStats() { const tbody = document.getElementById('probes-tbody'); try { const rows = await Promise.all(PROBES.map(async p => { const [repoRes, relRes] = await Promise.all([ fetch(`https://leapps-api.4n6-198.workers.dev/repos/${p.repo}`), fetch(`https://leapps-api.4n6-198.workers.dev/repos/${p.repo}/releases?per_page=100`), ]); if (!repoRes.ok || !relRes.ok) throw new Error('HTTP'); const repo = await repoRes.json(); const releases = (await relRes.json()).filter(r => !r.draft); const downloads = releases.reduce((sum, r) => sum + (r.assets || []).reduce((s, a) => s + (a.download_count || 0), 0), 0); const latest = releases.find(r => !r.prerelease) || releases[0] || null; return { ...p, downloads, stars: repo.stargazers_count || 0, releases: releases.length, latest: latest ? latest.tag_name : '', released: latest ? latest.published_at : '' }; })); const set = (id, v) => { const el = document.getElementById(id); el.textContent = fmt(v); el.classList.remove('loading'); }; set('pm-downloads', rows.reduce((s, r) => s + r.downloads, 0)); set('pm-stars', rows.reduce((s, r) => s + r.stars, 0)); set('pm-releases', rows.reduce((s, r) => s + r.releases, 0)); const color = probeColor(); if (probeChart) probeChart.destroy(); probeChart = new Chart(document.getElementById('probeDlChart'), { type: 'bar', data: { labels: rows.map(r => r.name), datasets: [{ data: rows.map(r => r.downloads), backgroundColor: color, borderRadius: 3 }] }, options: buildChartOptions() }); const maxDl = Math.max(...rows.map(r => r.downloads), 1); tbody.innerHTML = rows.map(r => `
${r.name}
${r.what}
${fmt(r.downloads)}
${fmt(r.stars)} ${r.latest || '—'} ${r.released ? new Date(r.released).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '—'} ${fmt(r.releases)} `).join(''); } catch (e) { tbody.innerHTML = 'Could not load the probe figures. See the probes'; } } // The daily snapshot records the probes under their own "probes" key, beside the LEAPP "totals". async function fetchProbeHistory() { const el = document.getElementById('probes-history'); try { const res = await fetch('https://raw.githubusercontent.com/abrignoni/leapps-website/main/data/downloads.json'); if (!res.ok) throw new Error('HTTP'); const snaps = ((await res.json()).snapshots || []).filter(s => s.probes); if (snaps.length < 2) { el.innerHTML = '
' + (snaps.length ? `Probe downloads are recorded with each daily snapshot, starting ${snaps[0].date}. The change since then appears after the next one.` : 'Probe downloads are recorded with each daily snapshot, starting with the next one. The change over time appears once there are two.') + '
'; return; } const first = snaps[0], last = snaps[snaps.length - 1]; const color = probeColor(); const gains = PROBES.map(p => ({ name: p.name, gain: Math.max(0, (last.probes[p.key] || 0) - (first.probes[p.key] || 0)) })); const maxGain = Math.max(...gains.map(g => g.gain), 1); el.innerHTML = `

Downloads since tracking began on ${first.date}, as of ${last.date}.

` + '
' + gains.map(g => `
${g.name}
${g.gain > 0 ? '+' + fmt(g.gain) : '—'}
`).join('') + '
'; } catch (e) { el.innerHTML = '
Could not load the probe download history.
'; } } fetchProbeStats(); fetchProbeHistory(); new MutationObserver(() => { if (!probeChart) return; const c = getChartColors(); probeChart.data.datasets[0].backgroundColor = probeColor(); probeChart.options.scales.x.ticks.color = c.tick; probeChart.options.scales.y.ticks.color = c.tick; probeChart.options.scales.y.grid.color = c.grid; probeChart.update(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); // --------------------------------------------------------------------------------------- // GitHub statistics collected daily by scripts/collect_github_stats.py (traffic, platforms, // stars, commit activity). Repository visits, clones and downloads are kept apart on purpose: // nothing here turns a visit into a download. // --------------------------------------------------------------------------------------- const STATS_BASE = /^(localhost|127\.0\.0\.1)$/.test(location.hostname) ? '/data/' : 'https://raw.githubusercontent.com/abrignoni/leapps-website/main/data/'; const OS_LABELS = { windows: 'Windows', macos: 'macOS', linux: 'Linux', script: 'Python script', other: 'Other' }; const OS_COLORS = { windows: '#378ADD', macos: '#B4B2A9', linux: '#F5C020', script: '#8FA9BF', other: '#555555' }; const GH = { index: null, months: {}, assets: null, community: null }; let ghTool = 'all', ghKind = 'views', ghRange = '90', ghProbe = 'all'; let reachChart = null, platformChart = null, starHistoryChart = null, probeReachChart = null, probePlatformChart = null; const drillCharts = {}; function esc(v) { return String(v == null ? '' : v).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function toolKey(name) { return Object.keys(TOOL_LABELS).find(k => TOOL_LABELS[k] === name) || name.toLowerCase().replace(/\s+/g, ''); } function labelFor(key) { if (TOOL_LABELS[key]) return TOOL_LABELS[key]; const probe = PROBES.find(p => p.key === key); return probe ? probe.name : key; } function groupKeys(group) { return group === 'probe' ? PROBES.map(p => p.key) : Object.keys(TOOL_LABELS); } function fmtDate(day) { return new Date(day + 'T00:00:00Z').toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }); } function fmtStamp(iso) { if (!iso) return 'never'; const d = new Date(iso); return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' }) + ' ' + d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'UTC' }) + ' UTC'; } async function getJson(name) { const res = await fetch(STATS_BASE + name, { cache: 'no-cache' }); if (!res.ok) throw new Error(name + ' ' + res.status); return res.json(); } async function loadGitHubStats() { const results = await Promise.allSettled([getJson('traffic/index.json'), getJson('assets.json'), getJson('community.json')]); GH.index = results[0].status === 'fulfilled' ? results[0].value : null; GH.assets = results[1].status === 'fulfilled' ? results[1].value : null; GH.community = results[2].status === 'fulfilled' ? results[2].value : null; if (GH.index) { const months = await Promise.allSettled((GH.index.months || []).map(m => getJson(`traffic/${m}.json`))); months.forEach(r => { if (r.status === 'fulfilled') GH.months[r.value.month] = r.value; }); } renderKpiTraffic(); buildPicker('tool-picker', ['all', ...groupKeys('tool')], () => ghTool, k => { ghTool = k; renderToolPanels(); }); buildPicker('probe-picker', ['all', ...groupKeys('probe')], () => ghProbe, k => { ghProbe = k; renderProbeReach(); }); wireToggle('reach-kind', 'kind', v => { ghKind = v; renderReach(); }); wireToggle('reach-range', 'range', v => { ghRange = v; renderReach(); }); renderToolPanels(); renderPlatforms(); renderProbeReach(); } function buildPicker(id, keys, current, onPick) { const el = document.getElementById(id); el.innerHTML = keys.map(k => ``).join(''); el.querySelectorAll('button').forEach(b => b.addEventListener('click', () => { el.querySelectorAll('button').forEach(x => x.classList.toggle('active', x === b)); onPick(b.dataset.key); })); } function wireToggle(id, attr, onPick) { const el = document.getElementById(id); el.querySelectorAll('button').forEach(b => b.addEventListener('click', () => { el.querySelectorAll('button').forEach(x => x.classList.toggle('active', x === b)); onPick(b.dataset[attr]); })); } function trafficStatusLine(keys) { if (!GH.index) return 'Repository traffic is not collected yet.'; const repos = GH.index.repos || {}; const configured = keys.filter(k => repos[k] && Object.values(repos[k].status || {}).some(s => s !== 'not_configured')); if (!configured.length) return 'Repository traffic starts once the stats GitHub App is installed.'; const missing = keys.filter(k => !repos[k] || Object.values(repos[k].status || {}).some(s => s !== 'ok')) .map(k => `${labelFor(k)} (${esc(Object.values((repos[k] || {}).status || {}).find(s => s !== 'ok') || 'missing')})`); return `Traffic collected ${fmtStamp(GH.index.updated_at)}.` + (missing.length ? ` Not collected on the last run: ${missing.join(', ')}.` : ''); } function dailySeries(keys, kind) { const field = `${kind}_daily`, days = {}; Object.values(GH.months).forEach(m => keys.forEach(k => { const points = ((m.repos || {})[k] || {})[field] || {}; Object.entries(points).forEach(([day, v]) => { if (!days[day]) days[day] = { count: 0, uniques: 0, repos: 0 }; days[day].count += v.count || 0; days[day].uniques += v.uniques || 0; days[day].repos += 1; }); })); return Object.keys(days).sort().map(day => ({ day, ...days[day] })); } function renderKpiTraffic() { const set = (id, v) => { const el = document.getElementById(id); el.textContent = v == null ? '—' : fmt(v); el.classList.remove('loading'); }; if (!GH.index) { set('m-visitors', null); set('m-cloners', null); return; } const keys = groupKeys('tool'); const sum = kind => { const vals = keys.map(k => (((GH.index.repos || {})[k] || {}).window || {})[kind]).filter(w => w && Number.isFinite(w.uniques)); return vals.length ? vals.reduce((s, w) => s + w.uniques, 0) : null; }; set('m-visitors', sum('views')); set('m-cloners', sum('clones')); } function lineChart(canvasId, existing, labels, datasets) { if (existing) existing.destroy(); const c = getChartColors(); return new Chart(document.getElementById(canvasId), { type: 'line', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: datasets.length > 1, labels: { color: c.tick, boxWidth: 10, font: { size: 11 } } } }, elements: { point: { radius: 0, hitRadius: 6 }, line: { tension: 0.2, borderWidth: 2 } }, scales: { x: { grid: { display: false }, ticks: { color: c.tick, font: { size: 10 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 8 } }, y: { beginAtZero: true, grid: { color: c.grid }, ticks: { color: c.tick, font: { size: 10 }, callback: v => fmt(v) } } } } }); } function renderReach() { const keys = ghTool === 'all' ? groupKeys('tool') : [ghTool]; let series = dailySeries(keys, ghKind); if (ghRange !== 'all') series = series.slice(-Number(ghRange)); const word = ghKind === 'views' ? 'visits' : 'clones'; document.getElementById('reach-title').textContent = `Daily repository ${word}` + (ghTool === 'all' ? ', all tools' : `, ${labelFor(ghTool)}`); const empty = document.getElementById('reach-empty'); const canvas = document.getElementById('reachChart'); if (!series.length) { canvas.style.display = 'none'; empty.style.display = 'block'; empty.textContent = 'No repository traffic recorded yet.'; } else { canvas.style.display = ''; empty.style.display = 'none'; const color = ghTool === 'all' ? '#F5C020' : (TOOL_COLORS[ghTool] || '#F5C020'); reachChart = lineChart('reachChart', reachChart, series.map(p => fmtDate(p.day)), [ { label: ghKind === 'views' ? 'Visits' : 'Clones', data: series.map(p => p.count), borderColor: color, backgroundColor: color }, { label: ghTool === 'all' ? 'Unique, summed per repo' : 'Unique', data: series.map(p => p.uniques), borderColor: '#888', backgroundColor: '#888', borderDash: [4, 3] }, ]); } const first = series.length ? fmtDate(series[0].day) : null; document.getElementById('reach-fresh').textContent = trafficStatusLine(keys) + (first ? ` Shown from ${first}; GitHub keeps 14 days, so the history starts when collection began.` : ''); } function stackedShareChart(canvasId, existing, keys) { if (existing) existing.destroy(); const repos = (GH.assets && GH.assets.repos) || {}; const rows = keys.filter(k => repos[k] && repos[k].by_os); const oses = Object.keys(OS_LABELS).filter(os => rows.some(k => repos[k].by_os[os])); const totals = rows.map(k => Object.values(repos[k].by_os).reduce((s, v) => s + v, 0) || 1); const c = getChartColors(); return new Chart(document.getElementById(canvasId), { type: 'bar', data: { labels: rows.map(labelFor), datasets: oses.map(os => ({ label: OS_LABELS[os], backgroundColor: OS_COLORS[os], borderWidth: 0, data: rows.map((k, i) => Math.round(((repos[k].by_os[os] || 0) / totals[i]) * 1000) / 10), counts: rows.map(k => repos[k].by_os[os] || 0), })), }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: c.tick, boxWidth: 10, font: { size: 11 } } }, tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${ctx.raw}% (${ctx.dataset.counts[ctx.dataIndex].toLocaleString()})` } } }, scales: { x: { stacked: true, max: 100, grid: { color: c.grid }, ticks: { color: c.tick, font: { size: 10 }, callback: v => v + '%' } }, y: { stacked: true, grid: { display: false }, ticks: { color: c.tick, font: { size: 11 } } } } } }); } function renderPlatforms() { platformChart = stackedShareChart('platformChart', platformChart, groupKeys('tool')); probePlatformChart = stackedShareChart('probePlatformChart', probePlatformChart, groupKeys('probe')); document.getElementById('platform-fresh').textContent = GH.assets ? `Release files read ${fmtStamp(GH.assets.updated_at)}.` : 'Downloads by platform are not collected yet.'; } function latestSnapshot(key, field) { const all = {}; Object.values(GH.months).forEach(m => Object.assign(all, (((m.repos || {})[key] || {})[field]) || {})); const days = Object.keys(all).sort(); return { days, all, latest: days.length ? all[days[days.length - 1]] : null }; } function renderReferrers() { const keys = ghTool === 'all' ? groupKeys('tool') : [ghTool]; document.getElementById('ref-tool').textContent = ghTool === 'all' ? 'all tools' : labelFor(ghTool); const merged = {}, seen = {}; let snapshotsCounted = 0; keys.forEach(k => { const snap = latestSnapshot(k, 'referrers'); (snap.latest || []).forEach(item => { const m = merged[item.referrer] || (merged[item.referrer] = { count: 0, uniques: 0 }); m.count += item.count || 0; m.uniques += item.uniques || 0; }); const recent = snap.days.slice(-30); snapshotsCounted = Math.max(snapshotsCounted, recent.length); recent.forEach(day => (snap.all[day] || []).forEach(item => { seen[item.referrer] = seen[item.referrer] || new Set(); seen[item.referrer].add(day); })); }); const rows = Object.entries(merged).sort((a, b) => b[1].count - a[1].count).slice(0, 10); const tbody = document.getElementById('ref-tbody'); tbody.innerHTML = rows.length ? rows.map(([site, v]) => ` ${esc(site)}${fmt(v.count)}${fmt(v.uniques)} ${seen[site] ? seen[site].size : 0} of ${snapshotsCounted}`).join('') : 'No referring sites recorded yet.'; document.getElementById('ref-fresh').textContent = trafficStatusLine(keys) + (ghTool === 'all' && rows.length ? ' Unique visitors are summed per repository.' : ''); } function renderStars() { const keys = ghTool === 'all' ? groupKeys('tool') : [ghTool]; document.getElementById('star-tool').textContent = ghTool === 'all' ? 'all tools' : labelFor(ghTool); const stars = (GH.community && GH.community.stars) || {}; // Each repo is a step line: its saved history (cumulative per change day), then the daily // counts recorded after the history's last day. Repos are added up day by day, each // carrying its last known value forward. const perRepo = keys.map(k => { const entry = stars[k] || {}; const points = (entry.series || []).map(([day, total]) => [day, total]); const lastSaved = points.length ? points[points.length - 1][0] : ''; Object.entries(entry.daily || {}).forEach(([day, total]) => { if (day > lastSaved) points.push([day, total]); }); return points.sort((a, b) => a[0] < b[0] ? -1 : 1); }); const days = [...new Set(perRepo.flatMap(p => p.map(x => x[0])))].sort(); const cursor = perRepo.map(() => ({ i: 0, value: 0 })); const series = days.map(day => { let total = 0; perRepo.forEach((points, r) => { const c = cursor[r]; while (c.i < points.length && points[c.i][0] <= day) { c.value = points[c.i][1]; c.i += 1; } total += c.value; }); return { day, total }; }); const color = ghTool === 'all' ? '#F5C020' : (TOOL_COLORS[ghTool] || '#F5C020'); const labels = series.map(p => new Date(p.day + 'T00:00:00Z').toLocaleDateString('en-US', { year: 'numeric', month: 'short', timeZone: 'UTC' })); starHistoryChart = lineChart('starHistoryChart', starHistoryChart, labels, [{ label: 'Stars', data: series.map(p => p.total), borderColor: color, backgroundColor: color, stepped: true }]); document.getElementById('star-fresh').textContent = GH.community ? `Stars read ${fmtStamp(GH.community.updated_at)}.` : 'Stars over time are not collected yet.'; } function renderToolPanels() { renderReach(); renderReferrers(); renderStars(); } function renderProbeReach() { const keys = ghProbe === 'all' ? groupKeys('probe') : [ghProbe]; const series = dailySeries(keys, 'views').slice(-90); const empty = document.getElementById('probe-reach-empty'); const canvas = document.getElementById('probeReachChart'); if (!series.length) { canvas.style.display = 'none'; empty.style.display = 'block'; empty.textContent = 'No repository traffic recorded yet.'; } else { canvas.style.display = ''; empty.style.display = 'none'; const color = probeColor(); probeReachChart = lineChart('probeReachChart', probeReachChart, series.map(p => fmtDate(p.day)), [ { label: 'Visits', data: series.map(p => p.count), borderColor: color, backgroundColor: color }, { label: ghProbe === 'all' ? 'Unique, summed per repo' : 'Unique', data: series.map(p => p.uniques), borderColor: '#888', backgroundColor: '#888', borderDash: [4, 3] }, ]); } document.getElementById('probe-reach-fresh').textContent = trafficStatusLine(keys); } function toggleDrill(key, btn) { const row = document.getElementById('drill-' + key); if (!row) return; const open = row.hidden; row.hidden = !open; btn.setAttribute('aria-expanded', String(open)); btn.innerHTML = open ? 'Details ▴' : 'Details ▾'; if (open && !row.dataset.rendered) { renderDrill(key, row.firstElementChild); if (GH.assets || GH.index) row.dataset.rendered = '1'; // render again once the data has loaded } } function renderDrill(key, cell) { const repo = ((GH.assets && GH.assets.repos) || {})[key] || {}; const arch = Object.entries(repo.by_os_arch || {}).sort((a, b) => b[1] - a[1]); const releases = (repo.releases || []).slice(0, 10); const paths = latestSnapshot(key, 'paths').latest || []; const activity = ((GH.community && GH.community.commit_activity) || {})[key] || {}; const list = items => items.length ? `
    ${items.join('')}
` : '

Not recorded yet.

'; cell.innerHTML = `

Downloads by platform and processor

${list(arch.map(([k, v]) => { const [os, a] = k.split('/'); return `
  • ${esc(OS_LABELS[os] || os)}${a ? ' · ' + esc(a) : ''}${fmt(v)}
  • `; }))}

    Downloads by release (latest 10)

    ${list(releases.map(r => `
  • ${esc(r.tag)}${r.prerelease ? ' (pre)' : ''}${fmt(r.downloads)}
  • `))}

    Most visited pages, last 14 days

    ${list(paths.map(p => `
  • ${esc(p.title || p.path)}${fmt(p.count || 0)}
  • `))}

    Commits per week, last year

    ${activity.weeks && activity.weeks.length ? `
    ` : `

    ${activity.status === 'computing' ? 'GitHub is still computing this; it appears after the next daily run.' : 'Not recorded yet.'}

    `}
    ${esc(repo.checksum_downloads_excluded ? `Checksum file downloads left out: ${repo.checksum_downloads_excluded.toLocaleString()}.` : '')}
    `; if (activity.weeks && activity.weeks.length) { const c = getChartColors(); drillCharts[key] = new Chart(document.getElementById('drill-activity-' + key), { type: 'bar', data: { labels: activity.weeks.map(w => fmtDate(w[0])), datasets: [{ data: activity.weeks.map(w => w[1]), backgroundColor: TOOL_COLORS[key] || '#F5C020', borderRadius: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { beginAtZero: true, grid: { color: c.grid }, ticks: { color: c.tick, font: { size: 9 }, maxTicksLimit: 3 } } } } }); } } loadGitHubStats(); new MutationObserver(() => { if (!GH.index && !GH.assets) return; renderToolPanels(); renderPlatforms(); renderProbeReach(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); // Mobile menu const hamburger = document.getElementById('lp-hamburger'); const mobileMenu = document.getElementById('lp-mobile-menu'); hamburger.addEventListener('click', function() { hamburger.classList.toggle('open'); mobileMenu.classList.toggle('open'); }); function closeMobileMenu() { hamburger.classList.remove('open'); mobileMenu.classList.remove('open'); } document.addEventListener('click', function(e) { if (!hamburger.contains(e.target) && !mobileMenu.contains(e.target)) closeMobileMenu(); });