EpiVerse
  • Repositories
  • Contributors
  • Community Health
    • Feedback
    • Source Code
    • github.com/reconhub
    • github.com/epiverse-trace
    • github.com/reconverse

On this page

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

EpiVerse Organizational Overview

This dashboard presents metrics and models for each repository within the epiverse-trace, reconhub, and reconverse GitHub organizations, and some related repositories from other sources. These organizations are linked under the icon on the top right. 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

84 repositories

including

32 from reconhub

31 from epiverse-trace

6 from reconverse

Urgent mainetance required

5 repositories

with high community engagement, yet low developer responsiveness:


EpiEstim

epireview

incidence

sandpaper

varnish

sparkline plot of annual numbers of commits.

Total commits:

30,120

between 2008 and 2025

(Plot below shows time series)

sparkline plot of annual GitHub activity.

GitHub Activity

20,816

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.

pkg_nodes = {
    return transpose(nodes_in).map(row => ({
        ...row,
    }));
}
pkg_edges = {
    return transpose(edges_in).map(row => ({
        ...row,
    }));
}
strength = -400;

chart = {

    const width = 928;
    const height = 600;

    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", [-width / 2, -height / 2, width, height])
        .attr("width", width)
        .attr("height", height)
        .attr("style", "max-width: 100%; height: auto; font: 14px sans-serif;");

    const link = svg.append("g")
        .attr("fill", "none")
        .attr("stroke-width", 1.5)
        .selectAll("path")
        .data(pkg_edges)
        .join("path")
            .attr("stroke", "gray")
            .attr("stroke-width", d => d.value);

    const node = svg.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 => d.size);

    node.append("text")
        .attr("x", 8)
        .attr("y", "0.31em")
        .text(d => d.id)
        .html(d =>
            `<a href="/repometrics-demo/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})`);
    });

    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;
    }

    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="/repometrics-demo/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="/repometrics-demo/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.

Development burstiness

EpiEstim

incidence

Number of dependencies

sandpaper

Issue response time

sandpaper

varnish

PR response time

EpiEstim

epireview

sandpaper

varnish

PR review time

EpiEstim

epireview

varnish

Release frequency

EpiEstim

Response time

EpiEstim

epireview

varnish

Time to close issues

epireview

sandpaper


 
Cookie Preferences