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")}`
}
Repositories
metricsTranspose = {
return transpose(metrics_in).map(row => ({
...row,
}));
}
repo_src = {
return transpose(repo_src_in).map(row => ({
...row,
}));
}
ctb_absTranspose = {
return transpose(ctb_abs_in).map(row => ({
...row,
}));
}
issue_respTranspose = {
return transpose(issue_resp_in).map(row => ({
...row,
}));
}
issue_bugsTranspose = {
return transpose(issue_bugs_in).map(row => ({
...row,
}));
}
depsTranspose = {
return transpose(deps_in).map(row => ({
...row,
}));
}
fnCallsTranspose = {
return transpose(fn_calls_in).map(row => ({
...row,
}));
}
json_data = FileAttachment("results-json-data.json").json();
gitlogDataTranspose = json_data['gitlog'].map(row => ({
...row,
first_commit: new Date(row.first_commit),
latest_commit: new Date(row.latest_commit)
}));
cranDataAll = json_data['cran'];
not_cran_in = json_data['not_cran'];
data_releases = json_data['data_releases'];
repos_on_r_univ = json_data['r_universe']['data_is_on_r_univ'];
data_r_univ_jobs = json_data['r_universe']['r_univ_jobs'];
data_r_univ_builds = json_data['r_universe']['r_univ_builds'];
data_r_univ_stats = json_data['r_universe']['r_univ_stats'];
reposAll = metricsTranspose.map(function(item) {
return item.package;
});
repos = Array.from(new Set(reposAll));
// Check whether repo specified in URL, and set if so:
location = new URL(document.baseURI);
repo_from_url = location.searchParams.get('repo');
setRepoFromUrl = function (r) {
localStorage.setItem("orgmetricsRepo", r.toString());
return r;
};
repoSet = repo_from_url ? setRepoFromUrl(repo_from_url) :
(localStorage.getItem("orgmetricsRepo") || repos [Math.floor(Math.random() * repos.length)]);
viewof repo = Inputs.select(
repos,
{
multiple: false,
value: repoSet,
label: htl.html`<b>Repository:</b>`
}
)
s = localStorage.setItem("orgmetricsRepo", repo.toString());
repoURL = repo_src.filter(function(r) {
return r.package === repo
})[0].url;
setLocation = function (repo) {
var location = new URL(document.baseURI);
location.searchParams.set('repo', repo);
return location;
}
repo_location = setLocation(repo);
htl.html`<div>
Click here for the source code of the
<a href=${repoURL} target="_blank"><i>${repo}</i> repository</a>,
or here for a
<a href=${repo_location}>shareable link to this page</a>.
</div>`
ctb_abs = ctb_absTranspose.filter(function(r) {
return r.repo === repo
})
issue_resp = issue_respTranspose.filter(function(r) {
return r.repo === repo
})
issue_bugs = issue_bugsTranspose.filter(function(r) {
return r.repo === repo
})
deps = depsTranspose.filter(function(r) {
return r.package === repo
})
fn_calls = fnCallsTranspose.filter(function(r) {
return r.package === repo
})
has_fn_calls = fn_calls.length > 0;
cran_data = cranDataAll.filter(function(r) {
return r.package === repo
})
has_cran_data = cran_data.length > 0;
not_on_cran = not_cran_in.includes(repo);
gitlog = gitlogDataTranspose.filter(function(r) {
return r.package === repo
})
gitlog_total_commits = gitlog[0]['num_commits'].toLocaleString();
gitlog_recent_commits = gitlog[0]['recent_commits'].toLocaleString();
gitlog_first_commit = gitlog[0]['first_commit'].toLocaleDateString("en-GB", {
month: 'short',
year: 'numeric'
});
gitlog_latest_commit = gitlog[0]['latest_commit'].toLocaleDateString("en-GB", {
month: 'short',
year: 'numeric'
});
releases = data_releases.filter(function(r) {
return r.package === repo
})
r_univ_page = repos_on_r_univ.filter(function(r) {
return r.package === repo
})
is_on_r_univ = r_univ_page.length > 0;
r_univ_universe = is_on_r_univ ? r_univ_page[0]['universe'] : undefined;
r_univ_package = is_on_r_univ ? r_univ_page[0]['package'] : undefined;
r_univ_jobs = data_r_univ_jobs[repo];
r_univ_builds = data_r_univ_builds[repo];
r_univ_stats = data_r_univ_stats.filter(function(r) {
return r.package === repo
})
r_univ_score = is_on_r_univ ? r_univ_stats[0]['score'].toLocaleString() : undefined;
r_univ_url = "https://" + r_univ_universe + ".r-universe.dev/" + r_univ_package;
r_univ_score_html = r_univ_score ?
htl.html`<a href=${r_univ_url} target="_blank">r-universe score</a>: ${r_univ_score}` :
htl.html`Repository not on r-universe`;
rank_index = rank_names_in.indexOf(repo);
rank_vector = ranks_in[rank_index];
order_vector = Array
.from(rank_vector.keys())
.sort((a, b) => rank_vector[b] - rank_vector[a])
.filter(item => item !== rank_index);
num_similar = 5
order_vector_sub = order_vector.slice(0, num_similar);
matched_names = order_vector_sub.map(i => rank_names_in[i]);
similar_pkgs = matched_names.map(i => {
const url = (repo_src.filter(function(r) {
return r.package === i
}, i)[0] || {}).url || null;
return htl.html`<li><a href="/repometrics-demo/repo.html" onclick="localStorage.setItem('orgmetricsRepo', '${i}')">${i}</a></li>`
});
function pluraliseObjects(x, what) {
if (x === 0) {
return "No " + what + "s";
} else if (x === 1) {
return "1 " + what;
} else {
return x + " " + what + "s";
}
}
release_latest = new Date(releases[0].latest).toLocaleDateString("en-GB", {
month: 'short',
year: 'numeric'
});
releases_txt_total = releases[0].total > 0 ?
(releases[0].total == 1 ? htl.html`1 release to date (${release_latest})` :
htl.html`${releases[0].total} releases (latest: ${release_latest}), ${releases[0].rel_per_year} per year`) :
htl.html`No releases to date`;
gitlog_txt_total = htl.html`${gitlog_total_commits} commits since ${gitlog_first_commit}`;
gitlog_txt_recent = htl.html`${gitlog_recent_commits} commits in past year`;
maintainer_count = repoMetrics.filter(function(m) {
return m.name == "maintainer_count"
})[0].value;
maintainer_count_txt = pluraliseObjects(maintainer_count, "primary maintainer");
maintainer_gh = [].concat(ctbs_gh.slice(0, maintainer_count) || []);
maintainer_gh_list = maintainer_gh.length == 0 ? undefined:
(maintainer_gh.length == 1 ? htl.html`
<div onclick=${() => localStorage.setItem('orgmetricsMaintainer', maintainer_gh)}>
<li><a href='/repometrics-demo/contributor.html'>${maintainer_gh}</a></li>
</div>
` : maintainer_gh.map(m => htl.html`
<div onclick=${() => localStorage.setItem('orgmetricsMaintainer', m)}>
<li><a href='/repometrics-demo/contributor.html'>${m}</a></li>
</div>
`));
maintainer_gh_list_txt = maintainer_gh.length > 0 ? htl.html`<ul>${maintainer_gh_list}</ul>` : undefined;
ctb_gh = [].concat(ctbs_gh.slice(maintainer_count) || []);
ctb_count = ctb_gh.length;
ctb_count_txt = pluraliseObjects(ctb_count, "additional contributor");
ctb_count_list = ctb_gh.map((item, i) => htl.html`
<span onclick=${() => localStorage.setItem('orgmetricsMaintainer', item)}>
<a href='/repometrics-demo/contributor.html'>${item}</a>${i < ctb_gh.length - 1 ? ", " : ""}</span>`);
ctb_count_html = ctb_count == 0 ? htl.html`<div>No additional contributors</div>` :
htl.html`<div>${ctb_count_txt}: ${ctb_count_list}</div>`;
num_commits = repoMetrics.filter(function(m) {
return m.name == "num_commits"
})[0].value;
test_coverage = repoMetrics.filter(function(m) {
return m.name == "test_coverage"
})[0].value;
test_coverage_txt = "Test coverage: " + (test_coverage ? (test_coverage * 100) + "%" : "none");
num_stars = repoMetrics.filter(function(m) {
return m.name == "num_stars"
})[0].value;
num_forks = repoMetrics.filter(function(m) {
return m.name == "num_forks"
})[0].value;
stars_forks = (num_stars + num_forks).toLocaleString();
stars_forks_html = htl.html`<a href=${repoURL} target="_blank">GitHub</a> stars and forks: ${stars_forks}`
num_dl = repoMetrics.filter(function(m) {
return m.name == "cran_downloads"
})[0].value.toLocaleString();
cran_dl_html = not_on_cran ?
htl.html`Package is not on CRAN` :
htl.html`Total <a href="https://cran.r-project.org/package=${repo}" target="_blank">CRAN</a> downloads: ${num_dl}`;
issues_active = repoMetrics.filter(function(m) {
return m.name == "issues_active"
})[0].value;
change_req_n_opened = repoMetrics.filter(function(m) {
return m.name == "change_req_n_opened"
})[0].value;
issue_count = issues_active + change_req_n_opened;
gh_text = (issue_count === 1) ? "GitHub Issue" : "GitHub Issues";
issue_count_html = issues_active ?
htl.html`Recent <a href="${repoURL}/issues" target="_blank">${gh_text}</a>: ${issue_count}` :
htl.html`No GitHub issues`;
// 'issue_cmt_count' from repometrics is *mean* value; convert back to
// full count here:
issue_cmt_count = Math.ceil(repoMetrics.filter(function(m) {
return m.name == "issue_cmt_count"
})[0].value * issues_active);
gh_issue_cmt_text = (issue_cmt_count === 1) ? "Issue comment" : "Issue comments";
issue_cmt_html = issues_active ?
htl.html`Recent <a href="${repoURL}/issues" target="_blank">${gh_issue_cmt_text}</a>: ${issue_cmt_count}` :
htl.html`No issue comments`;
recent_comment_html = (issue_count + issue_cmt_count) > 0 ?
htl.html`
<div style="margin-left:20px; margin-top:-15px; margin-bottom:10px; font-size:14px; white-space:normal;">
(<i>Recent</i> = previous year)
</div>` : htl.html`<div></div>`;
imp_txt = 'Imports: ' + deps[0]['imports'];
sug_txt = 'Suggests: ' + deps[0]['suggests'];
deps_in_org = [].concat(deps[0]['deps_in_org'] || []);
deps_in_org_txt = deps_in_org.length == 0 ?
'Depends on no other pkgs in org' :
'Depends on org pkgs:';
deps_in_org_list = deps_in_org.length === 0 ? undefined :
deps_in_org.map(d => htl.html`
<li>
<a href='/repometrics-demo/repo.html' onclick="localStorage.setItem('orgmetricsRepo', '${d}')">${d}</a>
</li>`);
n_deps_in_org_txt = deps_in_org.length > 0 ? htl.html`<ul>${deps_in_org_list}</ul>` : undefined;
revdeps = [].concat(deps[0]['revdeps'] || []);
revdeps_list = revdeps.length == 0 ? undefined :
revdeps.map(d => htl.html`
<li>
<a href='/repometrics-demo/repo.html' onclick="localStorage.setItem('orgmetricsRepo', '${d}')">${d}</a>
</li>`);
revdeps_in_org = revdeps.length > 0 ?
'Dependend on by org pkgs:' :
'Not depended on by any other pkgs in org';
revdeps_list_txt = revdeps.length > 0 ? htl.html`<ul>${revdeps_list}</ul>` : undefined;
pkgcheck Summary
This shows the summary output from rOpenSci’s pkgcheck
package. Passing checks are marked by ✅; failing checks by ❌; and optional checks that may be worth examining with 👀.
R-universe summary
build_url = is_on_r_univ ? r_univ_builds[0]['buildurl'] : undefined;
build_checks = is_on_r_univ ? r_univ_builds.filter(d => (d['status'] !== "success")) : [];
build_checks_okay = build_checks.length == 0;
is_on_r_univ ?
htl.html`<div>Links to
<a href="https://${r_univ_universe}.r-universe.dev/${r_univ_package}" target="_blank" rel="noopener noreferrer"><i>${r_univ_package}</i> on R-universe</a>
and to
<a href="${build_url}" target="_blank" rel="noopener noreferrer">recent R-universe builds</a>.
</div>` :
htl.html`<div>This package is not on R-universe</div>`;
r_univ_jobs_table = is_on_r_univ ?
Inputs.table(r_univ_jobs, {
columns: ["job", "config", "r", "check"],
format: {
job: d => htl.html`<a href="${build_url}/job/${d}" target="_blank" rel="noopener noreferrer">${d.toFixed(0)}</a>`,
},
}) : htl.html`<div></div>`;
is_on_r_univ ?
htl.html`<div style="margin-top: 10px;"><details><summary>Recent R-Universe Jobs</summary>${r_univ_jobs_table}</details></div>` : htl.html`<div></div>`;
CRAN Summary
cran_txt = has_cran_data ? "currently list these issues:" : "are all good";
cran_url = "https://cran.r-project.org/web/checks/check_results_" + repo + ".html";
not_on_cran ?
htl.html`<div>This package is not on CRAN</div>` :
htl.html`<div><a href="${cran_url}" target="_blank" rel="noopener noreferrer">CRAN checks</a> on this package ${cran_txt}</div>`;
Statistical properties
This section shows some of the statistical properties of the code base, as generated by the pkgstats
package. By default, only outlier values are shown, for which the “Limits” slider below can be used to reduce statistical properties to only those lying in the lower or upper percentiles of the specified value. The table uses the following abbreviations:
- “LOC” for Lines-of-Code
- “Nr.” for Number
- “fn” for function
- “Doc” for Documentation
Function usage in other packages
has_fn_calls ? htl.html`<div>
The following table shows the number of times different functions are used in
other packages. Numbers may be less than total numbers of packages listed above
in <q><i>Dependencies: Used by org pkgs</i></q> because some of those usages may be only
in tests, whereas the following usage counts are within actual R code only.
</div>` :
htl.html`<div>This package uses no other packages from the organizations,
and so its functions are not used anywhere else.</div>`;
CHAOSS metrics and models
url_chaoss = "https://chaoss.community/kb-metrics-and-metrics-models/";
url_chaoss_models = "https://chaoss.community/kbtopic/all-metrics-models/";
url_chaoss_metrics = "https://chaoss.community/kbtopic/all-metrics/";
htl.html`<div>
This section highlights important <a href="${url_chaoss}" target="_blank">
CHAOSS (<i> Community Health Analytics in Open Source Software</i>)</a> metrics
and models for the
<a href=${repoURL} target="_blank"><i>${repo}</i> repository</a>.
The first graph shows
<q><a href=${url_chaoss_models} target="_blank">models</a></q>,
which are aggregations of
<a href=${url_chaoss_metrics} target="_blank">metrics</a>
into conceptual or thematic groups, and the second provides more detail into
individual metrics. All values are standardised between 0 and 1 such that
higher values are always better than lower values.
</div>`
By default, the following charts of both models and metrics only show categories for which the repository is an outlier, defined as lying in the lower or upper 10% of all repositories. Note that these outlier proportions depend on distributions of values measured across all repositories, and will generally not correspond values beyond the limits of [10, 90]% on the scale shown. For example, values for metrics of models may be very concentrated around 0.5 with only a very few extreme values. The lower 10% of values may, for example, be all those below a value of 0.4. Clicking on the following button will toggle to display all values.
The model names shown on the bars are hyperlinked; clicking on any will take you to the model definitions on the CHAOSS Models pages.
CHAOSS Models
rm_metrics_models = json_data['rm_metrics_models'];
rm_models = rm_metrics_models['models'];
rm_model_text = rm_metrics_models['model_names'];
modelsTextMap = rm_model_text.reduce((map, item) => {
return map.set(String(item.name), item.airtable_name);
}, new Map());
rm_metrics = rm_metrics_models['metrics'];
metricsTextMap = rm_metrics.reduce((map, item) => {
return map.set(String(item.name), item.airtable_name);
}, new Map());
metricsUrlMap = rm_metrics.reduce((map, item) => {
return map.set(String(item.name), item.url);
}, new Map());
modelsUrlMap = rm_model_text.reduce((map, item) => {
return map.set(String(item.name), item.url);
}, new Map());
modelsAll = {
return transpose(models_in).map(row => {
const key = String(row.name);
const keyUrl = modelsUrlMap.has(key) ? modelsUrlMap.get(key) :
"https://chaoss.community/kbtopic/all-metrics-models/";
return {
...row,
url: keyUrl
}
});
}
modelRepoVarnames = modelsAll.filter(function(mod) {
return mod.package === repo && mod.name !== "final"
})
modelRepoAll = modelRepoVarnames.map(item => {
const key = String(item.name);
return {
...item,
name: modelsTextMap.has(key) ? modelsTextMap.get(key) : item.name
}
})
metricsAll = metricsTranspose.map(item => {
const key = String(item.name);
return {
...item,
name: metricsTextMap.has(key) ? metricsTextMap.get(key) : item.name,
url: metricsUrlMap.has(key) ? metricsUrlMap.get(key) : item.url
}
})
Plot.plot({
height: modelPlotHeight,
marginLeft: 60,
marginRight: 160,
marginTop: 50,
marginBottom: 50,
axis: null,
x: {
axis: "top",
grid: true,
label: "Model Scores"
},
y: { grid: true },
marks: [
Plot.barX(modelRepoFilt, {
y: "name",
x: "value",
sort: {y: "-x" },
fill: "value",
}),
Plot.barX(modelRepoFilt,
Plot.pointer(
{
y: "name",
x: "value",
sort: {y: "-x" },
stroke: "gray",
fill: "value",
strokeWidth: 2,
})
),
Plot.text(modelRepoFilt, {
x: d => d.value < 0 ? 0 : d.value,
y: d => d.name,
text: d => d.name,
href: d => d.url,
textAnchor: "start",
fontSize: 16,
dx: 5
})
],
color: {
scheme: "Cool",
type: "linear",
domain: [minVal, maxVal]
}
})
CHAOSS metrics
This graph provides more detailed insight into the state of the selected repository, through showing values for individual CHAOSS metrics used to inform the aggregate models. As for models, the names of the metrics are hyperlinked to the CHAOSS definitions, many of which are for general collections of metrics.
Plot.plot({
height: metricsPlotHeight,
marginLeft: 60,
marginRight: 160,
marginTop: 50,
marginBottom: 50,
axis: null,
x: {
axis: "top",
grid: true,
label: "Metric Scores"
},
y: { grid: true },
marks: [
Plot.barX(metricsRepo, {
y: "name",
x: "value",
sort: {y: "-x" },
fill: "value",
}),
Plot.barX(metricsRepo,
Plot.pointer(
{
y: "name",
x: "value",
sort: {y: "-x" },
stroke: "gray",
fill: "value",
strokeWidth: 2,
})
),
Plot.text(metricsRepo, {
x: (d) => d.value < 0 ? 0 : d.value,
y: d => d.name,
text: d => d.name,
href: d => d.url,
textAnchor: "start",
fontSize: 16,
dx: 5
})
],
color: {
scheme: "Cool",
type: "ordinal"
}
})