remlapmot.r-universe.dev
  • Repositories
  • Contributors
  • Community Health
    • Feedback
    • Source Code
    • github.com/remlapmot
    • github.com/MRCIEU
    • github.com/Hemken
    • github.com/wjchulme
    • github.com/Causal-LDA
    • github.com/CausalInference
    • github.com/dea-hazewinkel
    • github.com/gqi
    • github.com/jalvesaq
    • github.com/jrgant
    • github.com/ManuelHentschel
    • github.com/nx10
    • github.com/qingyuanzhao
    • github.com/quarto-dev
    • github.com/rondolab
    • github.com/WSpiller

On this page

  • Maintained Repositories
  • Contributor Repositories
  • Network
  • Co-Contributors

Contributors and Maintainers

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.background = "#f0f8ff";
    th.textContent = title;

    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="../remlapmot.r-universe.dev/repo.html";
    });

    return th;
}
repo_src = {
    return transpose(repo_src_in).map(row => ({
        ...row,
    }));
}
json_data = FileAttachment("results-json-data.json").json();
maintainer_pkgs = json_data['maintainer_pkgs'];
contributor_pkgs = json_data['contributor_pkgs'];
comaintainers = json_data['comaintainers'];
maintainers = Object.keys(maintainer_pkgs);
all_logins = [...new Set([...Object.keys(maintainer_pkgs), ...Object.keys(contributor_pkgs)])].sort();
searchParams = new URLSearchParams(window.location.search);
setInitialMaintainer = function(searchParams, maintainers) {
    var initialMaintainer = searchParams.get('ctb');
    if (initialMaintainer) {
        localStorage.setItem("orgmetricsMaintainer", initialMaintainer);
    } else {
        initialMaintainer = localStorage.getItem("orgmetricsMaintainer");
    }

    if (!initialMaintainer) {
        initialMaintainer = maintainers [Math.floor(Math.random() * maintainers.length)];
    }

    return initialMaintainer;
}
maintainerSet = setInitialMaintainer(searchParams, maintainers);

viewof maintainer = Inputs.select(
    all_logins,
    {
        multiple: false,
        value: maintainerSet,
        label: htl.html`<b>Contributor:</b>`
    }
)
s = localStorage.setItem("orgmetricsMaintainer", maintainer.toString());
maintainer_gh_url =
    htl.html`<a href="https://github.com/${maintainer}" target="_blank"><i>${maintainer}</i></a>`

updateSearchParams = function(searchParams, key, value) {
    if (value) {
        searchParams.set(key, value);
    }
    return searchParams;
}
searchParamsMaintainer = updateSearchParams(searchParams, 'ctb', maintainer.toString());
url = window.location.pathname;
hash = window.location.hash;
url_new = `${url}?${searchParamsMaintainer.toString()}${hash}`;
tempvar = window.history.replaceState(null, '', url_new);
these_pkgs = maintainer_pkgs[maintainer] || [];
these_ctb_pkgs = contributor_pkgs[maintainer] || [];
these_ctb_pkg_names = these_ctb_pkgs.map(d => d.package);

these_cos = comaintainers[maintainer] || null
these_cos_list = these_cos ?
these_cos.map(i => htl.html`
<div onclick=${() => localStorage.setItem('orgmetricsMaintainer', i)}>
<li><a href="../remlapmot.r-universe.dev/contributor.html">${i}</a></li>
</div>`) : htl.html`<li>No co-maintainers</li>`;

Maintained Repositories

This is a sub-set of the main table on the Organization page, showing repository metrics aggregated across the four categories described there. Values are shown only for repositories where is listed as a maintainer or author in the package DESCRIPTION file, and are scaled between 0 and 1 based on the distribution of values across the entire organization, with higher values always better than lower values.

metricsGroupedTable = {
    return transpose(metrics_table_in).map(row => ({
        ...row,
    }));
}
metricsTable = these_pkgs.length > 0
    ? metricsGroupedTable.filter(i => these_pkgs.includes(i.package))
    : [];
Inputs.table(metricsTable, {
    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."),
    },
})

Contributor Repositories

Packages to which has committed but is not listed as a maintainer or author in the package DESCRIPTION file. Commit counts are for this contributor only; metrics describe the package as a whole.

ctbCommitsMax = d3.max(these_ctb_pkgs, d => d.contributions) || 1;
these_ctb_pkgs.length > 0
    ? Inputs.table(these_ctb_pkgs, {
        columns: ["package", "contributions"],
        header: {
            package: "Package",
            contributions: tooltip("Commits", "Number of commits by this contributor to the package")
        },
        format: {
            package: d => pkgfmt(d),
            contributions: sparkbar(ctbCommitsMax)
        }
    })
    : htl.html`<p><i>No contributor-only repositories found.</i></p>`
ctbMetricsTable = these_ctb_pkg_names.length > 0
    ? metricsGroupedTable.filter(i => these_ctb_pkg_names.includes(i.package))
    : [];
ctbMetricsTable.length > 0
    ? Inputs.table(ctbMetricsTable, {
        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."),
        },
    })
    : htl.html``

Network

The following network diagram can be zoomed, moved, and dragged to dynamically explore how the selected maintainer is related to both packages and other maintainers. Although zooming and moving can be controlled within the diagram itself using either mouse or gestures, gestures may become confused for large, dense diagrams. The sliders immediately below may offer an easier way to control movement.

width = 928;
height = 600;

viewof x0 = {
    let input = Inputs.range([-width, width],
        {
            value: -width / 2,
            step: 10,
            label: "x"
        }
    )
    d3.select(input).select('input[type="number"]').style("display", "none");
    return input;
}

viewof y0 = {
    let input = Inputs.range([-height, height],
        {
            value: -height / 2,
            step: 10,
            label: "y"
        }
    )
    d3.select(input).select('input[type="number"]').style("display", "none");
    return input;
}

viewof strength = {
    let input = Inputs.range([0, 800],
        {
            value: 50,
            step: 10,
            label: "zoom"
        }
    )
    d3.select(input).select('input[type="number"]').style("display", "none");
    return input;
}
co_pkgs = these_cos ? these_cos.map(i => maintainer_pkgs[i]).flat() : [];
pkgs_expanded_full = [
    ...these_pkgs,
    ...co_pkgs
];
// Reduce to unique pkgs:
pkgs_expanded = [...new Set(pkgs_expanded_full)];

co_nodes = these_cos ?
    these_cos.map(item => ({ id: item, group: "Co-maintainer", size: 6 })) : [];
ctb_pkg_nodes = these_ctb_pkg_names
    .filter(item => !pkgs_expanded.includes(item))
    .map(item => ({ id: item, group: "Contributed packages", size: 5 }));
nodes = [
    { id: maintainer, group: "Maintainer", size: 10 },
    ...pkgs_expanded.map(item => ({
        id: item,
        group: these_pkgs.includes(item) ? "packages" : "otherPackages",
        size: these_pkgs.includes(item) ? 8 : 4,
    })),
    ...co_nodes,
    ...ctb_pkg_nodes
];

// edges are mappings from co-maintainers to all packages. First collect list
// of all packages from co-maintainers:
these_co_pkgs = these_cos ? these_cos.reduce((acc, key) => {
    if (maintainer_pkgs.hasOwnProperty(key)) {
        if (!these_pkgs.includes(key)) {
            acc[key] = maintainer_pkgs[key];
        }
    }
    return acc;
}, {}) : [];
// Then flatten that to (source, target) pairs of (maintainer, package):
these_co_pkgs_flat = Object.entries(these_co_pkgs).flatMap(([source, targets]) =>
    targets.map(target => ({ source, target }))
);
links = [
    ...these_pkgs.map(item => ({
        source: maintainer, target: item, value: 4
    })),
    ...these_co_pkgs_flat.map(item => ({
        source: item.source, target: item.target, value: 2
    })),
    ...these_ctb_pkg_names
        .filter(item => !pkgs_expanded.includes(item))
        .map(item => ({ source: maintainer, target: item, value: 1 }))
];
types = Array.from(new Set(nodes.map(d => d.group))).sort((a, b) => {
    if (a === "otherPackages") return 1;
    if (b === "otherPackages") return -1;
    return 0;
});

viewof selectedGroups = Inputs.checkbox(
    types,
    {
        label: "Filter by group:",
        value: types.filter(t => t !== "otherPackages")
    }
)

filteredNodes = nodes.filter(node => selectedGroups.includes(node.group) || selectedGroups.length === 0);

nodeIds = new Set(filteredNodes.map(node => node.id));
filteredLinks = links.filter(link =>
    nodeIds.has(typeof link.source === 'object' ? link.source.id : link.source) &&
    nodeIds.has(typeof link.target === 'object' ? link.target.id : link.target)
);
import {Swatches} from "@d3/color-legend"
Swatches(chart.scales.color)
chart = {

    const width = 928;
    const height = 600;

    const color = d3.scaleOrdinal(types, d3.schemeCategory10);

    const simulation = d3.forceSimulation(filteredNodes)
        .force("link", d3.forceLink(filteredLinks).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 observable variables:
            if (viewof x0) viewof x0.value = newX0;
            if (viewof y0) viewof y0.value = newY0;

            // Update strength based on zoom level (inverse relationship)
            const newStrength = Math.max(10, Math.min(800, strength * transform.k));
            if (viewof strength) viewof strength.value = newStrength;

            // 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(filteredLinks)
        .join("path")
            .attr("stroke", "gray")
            .attr("stroke-width", d => d.value);

    const node = container.append("g")
        .selectAll("g")
        .data(filteredNodes)
        .join("g")
            .call(drag(simulation));

    node.append("circle")
        .attr("stroke", "white")
        .attr("stroke-width", 1.5)
        .data(filteredNodes)
        .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 => d.group === "Co-maintainer" ?
            `<a href="../remlapmot.r-universe.dev/contributor.html"
                onclick="localStorage.setItem('orgmetricsMaintainer', '${d.id}')">${d.id}</a>` :
            ((d.group === "packages" || d.group === "otherPackages" || d.group === "Contributed packages") ?
                `<a href="../remlapmot.r-universe.dev/repo.html"
                    onclick="localStorage.setItem('orgmetricsRepo', '${d.id}')">${d.id}</a>` :
            d.id))
        .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);
}

Co-Contributors

htl.html`<ul>${these_cos_list}</ul>`
 
Cookie Preferences