There are
354 repositories
including
316 from ropensci
10 from ropensci-review-tools
6 from ropenscilabs
This dashboard presents metrics and models for each repository within the rOpenSci’s package registry. The dashboard is intended to demonstrate the kinds of analyses and insights that are possible, and is expected to change and develop a lot. Please provide feedback by clicking on the symbol and selecting “Feedback”.
There are
354 repositories
including
316 from ropensci
10 from ropensci-review-tools
6 from ropenscilabs
Total commits:
180,792
between 2009 and 2025
(Plot below shows time series)
GitHub Activity
133,785
Total issues, comments, and pull requests
The dashboard currently has four main pages:
This dashboard currently only considers repositories which are R packages, and thus every repository is also a package. Some aspects measured here, like the following dependency network, rely on structures specific to R packages, while other aspects are more related to the structure of repositories on GitHub. The terms “repository” and “package” may nevertheless be considered interchangeable.
This graph shows dependencies between all packages which use any other organization package.
The diagram can be zoomed, moved, and dragged to dynamically explore how packages inter-related. The initial “Repository” control highlights the selected repository in red.
nodeIds = pkg_nodes.map(item => item.id);
initialId = localStorage.getItem("orgmetricsRepo") ||
nodeIds [Math.floor(Math.random() * nodeIds.length)];
viewof selectedNode = Inputs.select(
nodeIds,
{
multiple: false,
value: initialId,
label: htl.html`<b>Repository:</b>`
}
)
s = localStorage.setItem("orgmetricsRepo", selectedNode.toString());width = 928;
height = 600;
chart = {
const x0 = -width / 2;
const y0 = -height / 2;
const strength = 50;
const types = Array.from(new Set(pkg_nodes.map(d => d.group)));
const color = d3.scaleOrdinal(types, d3.schemeCategory10);
const simulation = d3.forceSimulation(pkg_nodes)
.force("link", d3.forceLink(pkg_edges).id(d => d.id))
.force("charge", d3.forceManyBody().strength(-strength))
.force("x", d3.forceX())
.force("y", d3.forceY());
const svg = d3.create("svg")
.attr("viewBox", [x0, y0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; font: 14px sans-serif;");
const zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on("zoom", (event) => {
// Update the viewBox for panning
const transform = event.transform;
const newX0 = -transform.x;
const newY0 = -transform.y;
const newWidth = width / transform.k;
const newHeight = height / transform.k;
svg.attr("viewBox", [newX0, newY0, newWidth, newHeight]);
// Update strength based on zoom level (inverse relationship)
const newStrength = Math.max(10, Math.min(800, strength * transform.k));
// Update simulation with new strength
simulation.force("charge", d3.forceManyBody().strength(-newStrength));
simulation.alpha(0.3).restart();
});
// apply zoom behaviour to svg:
svg.call(zoom);
// create container for all context that will be transformed:
const container = svg.append("g");
const link = container.append("g")
.attr("fill", "none")
.attr("stroke-width", 1.5)
.selectAll("path")
.data(pkg_edges)
.enter().append("path")
.attr("stroke", "gray")
.attr("stroke-width", d => 5 * Math.log10(d.value));
const node = container.append("g")
.selectAll("g")
.data(pkg_nodes)
.join("g")
.call(drag(simulation));
node.append("circle")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.data(pkg_nodes)
.join("circle")
.attr("fill", d => color(d.group))
.attr("r", d => 5 * Math.log10(d.size));
node.append("text")
.attr("x", 8)
.attr("y", "0.31em")
.style("font-size", "14px")
.attr("fill", d => (d.id === selectedNode ? "red" : ""))
.text(d => d.id)
.html(d =>
`<a href="/orgmetrics-ropensci/repo.html"
onclick="localStorage.setItem('orgmetricsRepo', '${d.id}')">${d.id}
</a>`)
.clone(true).lower()
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 3);
simulation.on("tick", () => {
link.attr("d", linkArc);
node.attr("transform", d => `translate(${d.x},${d.y})`);
});
// set initial zoom transform to match current x0, y0:
const initialTransform = d3.zoomIdentity.translate(-x0, -y0);
svg.call(zoom.transform, initialTransform);
invalidation.then(() => simulation.stop());
return Object.assign(svg.node(), {scales: {color}});
}drag = simulation => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
// prevent zoom behaviour during drag:
event.sourceEvent.stopPropagation();
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}function sparkbar(max) {
const colourScale = d3.scaleSequential(d3.interpolateCool)
.domain([0, max]);
return (x) => htl.html`<div style="
background: ${colourScale(x)};
color: black;
width: ${100 * x / max}%;
float: right;
padding-right: 3px;
box-sizing: border-box;
overflow: visible;
display: flex;
justify-content: end;">${x.toFixed(2).toLocaleString("en-US")}`
}
function tooltip(title, expl) {
const th = document.createElement("th");
th.title = expl
th.style.display = "flex";
th.style.justifyContent = "flex-end";
th.style.backgroundColor = "#f0f8ff";
th.style.width = "100%";
th.style.position = "relative";
const textSpan = document.createElement("span");
textSpan.textContent = title;
textSpan.style.flex = "1";
textSpan.style.textAlign = "right";
textSpan.style.position = "static";
textSpan.style.wordWrap = "break-word";
textSpan.style.overflowWrap = "break-word";
textSpan.style.whiteSpace = "normal";
th.appendChild(textSpan);
th.addEventListener("mouseover", () => th.style.background = "#d0e8ff");
th.addEventListener("mouseout", () => th.style.background = "#f0f8ff");
return th;
}
function pkgfmt(pkg) {
const th = document.createElement("th");
th.title = "hover"
th.style.background = "#f0f8ff";
th.textContent = pkg;
th.addEventListener("mouseover", () => th.style.background = "#d0e8ff");
th.addEventListener("mouseout", () => th.style.background = "#f0f8ff");
th.addEventListener("click", () => {
localStorage.setItem("orgmetricsRepo", pkg);
th.style.background="#a0f8ff";
window.location.href="/orgmetrics-ropensci/repo.html";
});
return th;
}
function ctbfmt(ctb) {
const th = document.createElement("th");
th.title = "hover"
th.style.background = "#f0f8ff";
th.textContent = ctb;
th.addEventListener("mouseover", () => th.style.background = "#d0e8ff");
th.addEventListener("mouseout", () => th.style.background = "#f0f8ff");
th.addEventListener("click", () => {
localStorage.setItem('orgmetricsMaintainer', ctb);
th.style.background="#a0f8ff";
window.location.href="/orgmetrics-ropensci/contributor.html";
});
return th;
}This second presents aggregated results from statistics assessed for each repository, and aggregated across the organizations into four distinct categories:
The Overall column is an average of all metrics across all of these four categorical groupings, and provides an overall metric of repository health.
Plot.plot({
color: {
legend: true,
label: "name",
swatchHeight: 4,
domain: ["Development", "GitHub", "Popularity", "Dep.+Rel.", "Overall"],
},
marks: [
Plot.lineY(metricsData, {
x: "date",
y: "value",
stroke: "name",
strokeWidth: 2,
strokeDasharray: "2,5",
}),
Plot.linearRegressionY(metricsData, {
x: "date",
y: "value",
stroke: "name",
strokeWidth: 2,
ci: 0
}),
Plot.axisY({
label: null,
}),
],
x: {
grid: true,
type: "utc",
domain: [d3.min(metricsData, d => d.date), d3.max(metricsData, d => d.date)],
tickFormat: "%Y",
ticks: [...new Set(metricsData.map(d => d.date.getFullYear()))].map(year => new Date(`${year}-01-01`)),
},
y: { grid: true },
style: {
fontSize: '16px',
}
})The following table shows metrics for each package, for the latest time period only aggregated into each of the four groups. Clicking on the “package” values will lead to the repository maintenance page with further details of the selected package or repository.
Inputs.table(metricsGroupedTable, {
width: {
package: 100,
total: 200,
development: 200,
issues: 200,
popularity: 200,
meta: 200,
},
format: {
package: d => pkgfmt(d),
development: sparkbar(d3.max(metricsGroupedTable, d => d.development)),
issues: sparkbar(d3.max(metricsGroupedTable, d => d.issues)),
popularity: sparkbar(d3.max(metricsGroupedTable, d => d.popularity)),
meta: sparkbar(d3.max(metricsGroupedTable, d => d.meta)),
total: sparkbar(d3.max(metricsGroupedTable, d => d.total)),
},
header: {
development: tooltip("Development", "Code development and maintenance metrics"),
issues: tooltip("Issues", "GitHub issues and pull request activity"),
popularity: tooltip("Popularity", "Project popularity on CRAN (where applicable) and GitHub"),
meta: tooltip("Dependencies and releases", ""),
total: tooltip("Overall", "Average across all four categories of metrics."),
},
})This next graph shows the maintenance deficit over time, as the difference between community engagement and developer responsiveness. Repositories with high community engagement yet low developer responsiveness have a high maintenance deficit, and vice-versa. The “Deficit” scores are scaled to fix within the same range as the metrics of community engagement and developer responsiveness.
Plot.plot({
color: {
legend: true,
label: "name",
swatchHeight: 4,
domain: ["Comm. Engage.", "Dev. Resp.", "Deficit"],
},
marks: [
Plot.lineY(maintenanceData, {
x: "date",
y: "value",
stroke: "name",
strokeWidth: 2,
strokeDasharray: "2,5",
}),
Plot.linearRegressionY(maintenanceData, {
x: "date",
y: "value",
stroke: "name",
strokeWidth: 2,
ci: 0
}),
Plot.axisY({
label: null,
}),
],
x: {
grid: true,
type: "utc",
domain: [d3.min(metricsData, d => d.date), d3.max(metricsData, d => d.date)],
tickFormat: "%Y",
ticks: [...new Set(metricsData.map(d => d.date.getFullYear()))].map(year => new Date(`${year}-01-01`)),
},
y: { grid: true },
style: {
fontSize: '16px',
}
})And these are maintenance deficit values for individual repositories (packages), for the latest time period only.
Inputs.table(maintenanceRepoData, {
width: {
package: 100,
comm_engage: 200,
dev_resp: 200,
maintenance: 200,
},
format: {
package: d => pkgfmt(d),
comm_engage: sparkbar(d3.max(maintenanceRepoData, d => d.comm_engage)),
dev_resp: sparkbar(d3.max(maintenanceRepoData, d => d.dev_resp)),
maintenance: sparkbar(d3.max(maintenanceRepoData, d => d.maintenance)),
},
header: {
comm_engage: tooltip("Community Engagement", "Community Engagement metrics"),
dev_resp: tooltip("Developer Responsivness", "Developer Responsiveness metrics"),
maintenance: tooltip(
"Maintenance Deficit",
"Community Engagment minus Developer Responsivess (rescaled)"
),
},
})The following show several more distinct indicators of maintenance need, all of which are assessed over the most recent period of repository activity:
Inputs.table(extraMetricsTable, {
width: {
repo: 100,
ctb_absence: 200,
response: 200,
labels: 200,
bugs: 200,
},
format: {
repo: d => pkgfmt(d),
ctb_absence: sparkbar(d3.max(extraMetricsTable, d => d.ctb_absence)),
response: sparkbar(d3.max(extraMetricsTable, d => d.response)),
labels: sparkbar(d3.max(extraMetricsTable, d => d.labels)),
bugs: sparkbar(d3.max(extraMetricsTable, d => d.bugs)),
},
header: {
ctb_absence: tooltip("Ctb. Absence", "Contributor absence factor"),
response: tooltip("Resp. Time", "Time to respond to GitHub issues and pull requests"),
labels: tooltip("Issue Labels", "Proportion of labelled issues"),
bugs: tooltip("Prop. Bugs", "Proportion of issues and PRs which are about bugs."),
},
})Finally, this table shows a metric of main contributor absence. Values are only shown for main contributors who have been recently absent from repositories. A contributor who has been entirely absent during the most recent period, and was responsible for 100% of the commits from some number, n, repositories, would have a contributor absence score of n. The same contributor absence of n could also reflect somebody contributing exactly 50% of the code to 2\(\times\)n repositories, and being entirely absent during the recent period. Any contributions by that contributor during the recent period would reduce the absence factor. In general, high absence factors describe recently absent contributors who have previously been major contributors to numerous repositories.
// The 'ctbfmt' function set the localStorage 'orgmetricsMaintainer' value, but
// the inputs here are full names, no GitHub handles, so unless the are
// identical, this currently fails and needs to be fixed.
Inputs.table(ctb_abs_ctb, {
width: {
name: 200,
login: 200,
measure: 200,
},
format: {
login: d => ctbfmt(d),
measure: sparkbar(d3.max(ctb_abs_ctb, d => d.measure)),
},
header: {
measure: tooltip("Ctb. Absence", "Contributor absence factor"),
},
})This section links to repositories with distinct maintenance priorities.