Open code for public health in Scotland
Reproducible analysis, open-source tools, and data science resources from Public Health Scotland.
githubData = FileAttachment("github-data.json").json()
orgData = githubData.org
allRepos = githubData.repos
langColour = ({
R: "#2166ac",
Python: "#3572A5",
Vue: "#42b883",
JavaScript: "#f1e05a",
TypeScript: "#3178c6",
Dockerfile: "#384d54"
})
// Returns #1b1b1b or #ffffff depending on which achieves better contrast
// against the given hex background colour.
badgeTextColor = (hex) => {
const r = parseInt(hex.slice(1,3), 16) / 255;
const g = parseInt(hex.slice(3,5), 16) / 255;
const b = parseInt(hex.slice(5,7), 16) / 255;
const lin = c => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
return L > 0.179 ? "#1b1b1b" : "#ffffff";
}
// Repository fields are interpolated into innerHTML, so escape anything that
// comes from GitHub before it is written into the page.
escapeHtml = (value) => String(value ?? "").replace(
/[&<>"']/g,
ch => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch])
)
totalStars = allRepos.reduce((n, r) => n + (r.stargazers_count || 0), 0)
{
const pairs = [
["stat-repos", orgData.public_repos ?? allRepos.length],
["stat-stars", totalStars],
["stat-followers", orgData.followers ?? 0]
];
pairs.forEach(([id, val]) => {
const el = document.querySelector(`#${id} .phs-stat-value`);
if (el) el.textContent = val.toLocaleString();
});
}
—
Public repositories
—
Total stars
—
Followers
Featured projects
featuredList = FileAttachment("featured.json").json()
// Featured entries are matched against the snapshot; an entry naming a repo
// that no longer exists still renders, without metadata.
featuredData = featuredList.map(item => ({
...(allRepos.find(r => r.name === item.repo) || { name: item.repo }),
customDescription: item.description
}))
{
const grid = document.getElementById("featured-grid");
if (!grid) return;
grid.innerHTML = featuredData.map(repo => {
const lang = repo.language || "Other";
const colour = langColour[lang] || "#666666";
const textCol = badgeTextColor(colour);
const name = escapeHtml(repo.name);
const href = escapeHtml(repo.html_url ||
`https://github.com/Public-Health-Scotland/${repo.name}`);
const stars = (repo.stargazers_count || 0).toLocaleString();
return `
<a href="${href}" target="_blank" rel="noopener"
class="phs-featured-card"
aria-label="View ${name} on GitHub">
<div class="phs-featured-card-top">
<span class="phs-lang-badge" style="background:${colour};color:${textCol}">${escapeHtml(lang)}</span>
<span class="phs-star-count" aria-label="${stars} stars">★ ${stars}</span>
</div>
<h3 class="phs-featured-card-name">${name}</h3>
<p class="phs-featured-card-desc">${escapeHtml(repo.customDescription)}</p>
<span class="phs-featured-card-cta">View on GitHub →</span>
</a>`;
}).join("");
}Packages
packageRepos = allRepos
.filter(repo =>
!repo.archived &&
Array.isArray(repo.topics) &&
repo.topics.includes("r-package")
)
.sort((a, b) => a.name.localeCompare(b.name))
{
const grid = document.getElementById("packages-grid");
if (!grid) return;
if (!packageRepos.length) {
grid.innerHTML =
`<p class="phs-repo-empty">No repositories are tagged with the
<code>r-package</code> topic yet.
<a href="https://github.com/Public-Health-Scotland">View all on GitHub →</a></p>`;
return;
}
grid.innerHTML = packageRepos.map(repo => {
const lang = repo.language || "Other";
const colour = langColour[lang] || "#2166ac";
const textCol = badgeTextColor(colour);
const name = escapeHtml(repo.name);
const stars = (repo.stargazers_count || 0).toLocaleString();
const docsUrl = escapeHtml((repo.homepage || "").trim());
const docsLink = docsUrl
? `<a href="${docsUrl}" target="_blank" rel="noopener"
class="phs-package-link phs-package-link--docs"
aria-label="Read ${name} documentation">Documentation</a>`
: "";
return `
<div class="phs-package-card" aria-labelledby="pkg-${name}">
<div class="phs-package-card-top">
<span class="phs-lang-badge" style="background:${colour};color:${textCol}">${escapeHtml(lang)}</span>
<span class="phs-star-count" aria-label="${stars} stars">★ ${stars}</span>
</div>
<h3 class="phs-package-name" id="pkg-${name}">${name}</h3>
<p class="phs-package-desc">${escapeHtml(repo.description)}</p>
<div class="phs-package-links">
<a href="${escapeHtml(repo.html_url)}" target="_blank" rel="noopener"
class="phs-package-link phs-package-link--github"
aria-label="View ${name} source on GitHub">GitHub source</a>
${docsLink}
</div>
</div>`;
}).join("");
}All repositories
langFilter = Generators.observe(notify => {
const options = document.getElementById("repo-lang");
const read = () => options.querySelector("input:checked")?.value || "All";
const onChange = () => notify(read());
options.addEventListener("change", onChange);
notify(read());
return () => options.removeEventListener("change", onChange);
})
searchQuery = Generators.input(document.getElementById("repo-search"))
sortOrder = Generators.input(document.getElementById("repo-sort"))
// The current page has no control of its own, so it is held in a detached
// input that the pagination buttons write to.
_pageStore = {
const store = document.createElement("input");
store.type = "hidden";
store.value = "0";
store.set = (value) => {
store.value = value;
store.dispatchEvent(new Event("input"));
};
return store;
}
pageIndex = Generators.input(_pageStore)
// Languages offered as filters, derived from the repositories themselves so
// the options stay in step with what the organisation actually publishes.
// Everything below the threshold is reachable through "Other".
filterLanguages = {
const counts = new Map();
for (const repo of allRepos) {
if (repo.language) counts.set(repo.language, (counts.get(repo.language) || 0) + 1);
}
return [...counts]
.filter(([, n]) => n >= 3)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.map(([lang]) => lang);
}
// Render filter controls and repo grid as one reactive section
{
const PAGE_SIZE = 24;
// Filter and sort allRepos
const q = searchQuery.trim().toLowerCase();
const filtered = allRepos
.filter(r => {
const rl = r.language || "Other";
const matchLang =
langFilter === "All" ||
(langFilter === "Other"
? !filterLanguages.includes(rl)
: rl === langFilter);
const matchSearch =
!q ||
r.name.toLowerCase().includes(q) ||
(r.description || "").toLowerCase().includes(q);
return matchLang && matchSearch;
})
.sort((a, b) => {
if (sortOrder === "Most stars")
return (b.stargazers_count || 0) - (a.stargazers_count || 0);
if (sortOrder === "Alphabetical")
return a.name.localeCompare(b.name);
return new Date(b.pushed_at) - new Date(a.pushed_at);
});
const pageCount = Math.ceil(filtered.length / PAGE_SIZE);
const page = Math.min(Number(pageIndex), Math.max(0, pageCount - 1));
const paged = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
// Card HTML
const cardHtml = paged.map(repo => {
const lang = repo.language || "Other";
const colour = langColour[lang] || "#666666";
const textCol = badgeTextColor(colour);
const name = escapeHtml(repo.name);
const full = repo.description || "";
const desc = full.length > 120
? `${full.slice(0, 120).replace(/\s+\S*$/, "")}…`
: full;
const pushed = new Date(repo.pushed_at).toLocaleDateString("en-GB", {
day: "numeric", month: "short", year: "numeric"
});
const stars = (repo.stargazers_count || 0).toLocaleString();
return `
<a href="${escapeHtml(repo.html_url)}" target="_blank" rel="noopener"
class="phs-repo-card" aria-label="View ${name} on GitHub">
<div class="phs-repo-card-top">
<span class="phs-repo-name">${name}</span>
<span class="phs-lang-badge" style="background:${colour};color:${textCol}">${escapeHtml(lang)}</span>
</div>
<p class="phs-repo-desc">${escapeHtml(desc) || "<em>No description</em>"}</p>
<div class="phs-repo-meta">
<span aria-label="${stars} stars">★ ${stars}</span>
<span>Updated ${pushed}</span>
</div>
</a>`;
}).join("");
// Pagination HTML
const prevDisabled = page === 0 ? "disabled" : "";
const nextDisabled = page >= pageCount - 1 ? "disabled" : "";
const paginationHtml = pageCount > 1 ? `
<div class="phs-pagination">
<button class="btn btn-outline-primary btn-sm phs-page-btn" ${prevDisabled}
data-page="${page - 1}">← Previous</button>
<span class="phs-page-info">Page ${page + 1} of ${pageCount}</span>
<button class="btn btn-outline-primary btn-sm phs-page-btn" ${nextDisabled}
data-page="${page + 1}">Next →</button>
</div>` : "";
// Error / empty state
const emptyHtml = filtered.length === 0
? `<p class="phs-repo-empty">No repositories match your filters.
<a href="https://github.com/Public-Health-Scotland">View all on GitHub →</a></p>`
: "";
// Inject
const container = document.getElementById("repo-section-body");
const summary = document.getElementById("repo-count");
if (!container) return;
summary.textContent = filtered.length === 0
? ""
: `Showing ${paged.length} of ${filtered.length} repositories`;
container.innerHTML = `
<div class="phs-repo-grid">${emptyHtml || cardHtml}</div>
${paginationHtml}`;
// Wire pagination buttons
container.querySelectorAll(".phs-page-btn:not([disabled])").forEach(btn => {
btn.addEventListener("click", () => {
const target = +btn.dataset.page;
const forwards = target > page;
_pageStore.set(target);
// This button is destroyed by the re-render, which would drop keyboard
// focus back to the top of the document — move it to the equivalent
// control, or to the results summary once the button is disabled.
requestAnimationFrame(() => requestAnimationFrame(() => {
const buttons = container.querySelectorAll(".phs-page-btn");
const same = buttons[forwards ? 1 : 0];
if (same && !same.disabled) same.focus();
else summary.focus();
}));
});
});
}