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

On this page

  • 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="/orgmetrics-ropensci/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'];
comaintainers = json_data['comaintainers'];
maintainers = Object.keys(maintainer_pkgs);
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(
    maintainers,
    {
        multiple: false,
        value: maintainerSet,
        label: htl.html`<b>Maintainer:</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] || null;

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="/orgmetrics-ropensci/contributor.html">${i}</a></li>
</div>`) : htl.html`<li>No co-maintainers</li>`;

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 any repositories to which has contributed, 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 = 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."),
    },
})

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 })) : [];
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
];

// 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
    }))
];
import {Swatches} from "@d3/color-legend"
Swatches(chart.scales.color)
chart = {

    const width = 928;
    const height = 600;

    const types = Array.from(new Set(nodes.map(d => d.group)));

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

    const simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).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(links)
        .join("path")
            .attr("stroke", "gray")
            .attr("stroke-width", d => d.value);

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

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