rOpenSci
  • Repositories
  • Contributors
  • Community Health
    • Feedback
    • Source Code

On this page

  • Package Network
  • Aggregate metrics
  • Maintenance deficit
  • Additional metrics and indicators
    • Contributor Absence
  • Maintenance Priorities

rOpenSci Organizational Overview

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

Urgent mainetance required

10 repositories

with high community engagement, yet low developer responsiveness:


c14bazAAR

goodpractice

magick

osmdata

pkgcheck

rinat

sits

skimr

vcr

webchem

sparkline plot of annual numbers of commits.

Total commits:

180,792

between 2009 and 2025

(Plot below shows time series)

sparkline plot of annual GitHub activity.

GitHub Activity

133,785

Total issues, comments, and pull requests


The dashboard currently has four main pages:

  1. This main page which provides an organizational overview, and identifies maintenance priorities across all repositories.
  2. A repository maintenance page providing additional detail on maintenance needs of any selected repository.
  3. A contributor page with details of individual maintainers and contributors, their repositories and co-contributors.
  4. A Community Health providing additional detail on scores for the various CHAOSS (Community Health Analytics in Open Source Software) models and metrics for each repository.

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.

Package Network

This graph shows dependencies between all packages which use any other organization package.

  • Nodes show individual packages, with circles for each package scaled by total numbers of dependent packages; that is, numbers of other packages which depend on that package.
  • Edges connect dependent packages, with thicknesses scaled by total numbers of function calls by all dependent packages.

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.

pkg_nodes = {
    return transpose(nodes_in).map(row => ({
        ...row,
        colour: "blue"
    }));
}
pkg_edges = {
    return transpose(edges_in).map(row => ({
        ...row,
    }));
}
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}});
}
function linkArc(d) {
    const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);
    return `
    M${d.source.x},${d.source.y}
    A${r},${r} 0 0,1 ${d.target.x},${d.target.y}
    `;
}
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;
}

Aggregate metrics

This second presents aggregated results from statistics assessed for each repository, and aggregated across the organizations into four distinct categories:

  1. Development: Metrics of both code development and maintainer continuity and diversity. High scores reflect repositories with high levels of code development from a diverse community of maintainers.
  2. GitHub: Metrics derived from GitHub issues and pull requests. High scores reflect repositories with active use of issues, pull requests, code reviews, and rapid responses from core maintainers to issues or pull requests opened by wider community members.
  3. Popularity: Metrics of repository popularity, derived from CRAN download numbers (where applicable), GitHub stars and forks, and issue comments from user communities beyond core maintainers.
  4. Dependencies and Releases: High scores reflect repositories with fewer dependencies and frequent releases.

The Overall column is an average of all metrics across all of these four categorical groupings, and provides an overall metric of repository health.

metricsData = {
    return transpose(metrics_dates_in).map(row => ({
        ...row,
        date: new Date(row.date)
    }));
}
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.

metricsGroupedTable = {
    return transpose(metrics_table_in).map(row => ({
        ...row,
    }));
}
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."),
    },
})

Maintenance deficit

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.

maintenanceData = {
    return transpose(maintenance_ts_in).map(row => ({
        ...row,
        date: new Date(row.date)
    }));
}
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.

maintenanceRepoData = {
    return transpose(maintenance_repo_in).map(row => ({
        ...row,
    }));
}
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)"
        ),
    },
})

Additional metrics and indicators

The following show several more distinct indicators of maintenance need, all of which are assessed over the most recent period of repository activity:

  • Ctb. Absence: A measure of “contributor absence” for each repository, indicating maintenance deficit arising through absence of primary contributors.
  • Resp. Time: The average time for a core maintainer to respond to a new issue or pull request
  • Issue Labels: The proportion of issues with labels
  • Prop. Bugs: The proportion of new issues opened that were bug reports
extraMetricsTable = {
    return transpose(data_extra_metrics_in).map(row => ({
        ...row,
    }));
}
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."),
    },
})

Contributor Absence

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.

Plot = import("https://esm.sh/@observablehq/plot")
ctb_abs_ctb = {
    return transpose(ctb_abs_ctb_in).map(row => ({
        ...row,
    }));
}
ctb_abs_ctb_len = ctb_abs_ctb.length;
// 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"),
    },
})

Maintenance Priorities

This section links to repositories with distinct maintenance priorities.

CRAN downloads

pkgcheck

Defect resolution duration

osmdata

sits

webchem

Number of dependencies

sits

Issue response time

goodpractice

pkgcheck

sits

skimr

vcr

webchem

PR response time

c14bazAAR

goodpractice

osmdata

rinat

skimr

webchem

PR review time

goodpractice

osmdata

pkgcheck

vcr

Response time

c14bazAAR

goodpractice

osmdata

rinat

skimr

webchem

Time to close issues

c14bazAAR

goodpractice

osmdata

pkgcheck

rinat

sits

skimr

vcr

 
Cookie Preferences