function renderCouplingSankey(rows) {
const container = document.getElementById('widget-coupling-sankey-body');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<div class="empty">No coupling rows. Either the ' +
'repo has too few co-changes to be Fisher-significant or the ' +
'analysis was not wired through.</div>';
return;
}
const TOP_N = 30;
const sankeyLayout = (window.Alpine && window.Alpine.store)
? window.Alpine.store('layout') : null;
const userSankeyDepth = sankeyLayout ? sankeyLayout.sankeyDepth : 'files';
function modulePathSeg(p, depth) {
const parts = (p || '').split('/');
if (parts.length <= depth) {
const lastSlash = (p || '').lastIndexOf('/');
return lastSlash < 0 ? (p || '') : p.slice(0, lastSlash);
}
return parts.slice(0, depth).join('/');
}
var workingRows;
if (typeof userSankeyDepth === 'number') {
const aggregated = {};
for (var i = 0; i < rows.length; i++) {
const r = rows[i];
const a = modulePathSeg(r.entity_a, userSankeyDepth);
const b = modulePathSeg(r.entity_b, userSankeyDepth);
if (!a || !b || a === b) continue;
const key = a < b ? a + '\x00' + b : b + '\x00' + a;
if (!aggregated[key]) {
aggregated[key] = {
entity_a: a < b ? a : b,
entity_b: a < b ? b : a,
shared: 0,
degree: 0,
};
}
aggregated[key].shared += (r.shared || 0);
const strength = (typeof r.degree === 'number') ? r.degree : 0;
if (strength > aggregated[key].degree) {
aggregated[key].degree = strength;
}
}
workingRows = Object.keys(aggregated).map(function (k) { return aggregated[k]; });
} else {
workingRows = rows;
}
const topRows = workingRows.slice()
.sort(function (a, b) {
const ca = (typeof a.degree === 'number') ? a.degree : 0;
const cb = (typeof b.degree === 'number') ? b.degree : 0;
return cb - ca;
})
.slice(0, TOP_N);
const nodeNames = new Set();
const links = topRows.map(function (r) {
nodeNames.add(r.entity_a);
nodeNames.add(r.entity_b);
return {
source: r.entity_a,
target: r.entity_b,
value: r.shared || 0,
};
});
const nodes = Array.from(nodeNames).map(function (name) {
return { name: name };
});
setChartAriaLabel(container,
'Change-coupling Sankey flow across ' + nodes.length + ' entities, ' +
links.length + ' co-change links');
const chart = mountEcharts(container);
chart.setOption({
tooltip: {
trigger: 'item',
formatter: function (params) {
if (params.dataType === 'edge') {
return '<b>' + escapeHtml(params.data.source) + ' ↔ ' +
escapeHtml(params.data.target) + '</b>' +
'<br/>shared revs: ' + params.data.value;
}
return '<b>' + escapeHtml(params.data.name) + '</b>';
},
},
series: [{
type: 'sankey',
layout: 'none',
nodeAlign: 'left',
emphasis: { focus: 'adjacency' },
data: nodes,
links: links,
lineStyle: { color: 'gradient', curveness: 0.5 },
label: { color: token('--label-on-dark'), fontSize: 11 },
}],
});
chart.on('click', function (params) {
if (params.dataType === 'node' && params.data && params.data.name) {
if (userSankeyDepth === 'files' && window._codeloreShowDetail) {
window._codeloreShowDetail(params.data.name);
} else {
showFileDetailDrawer(params.data.name, data);
}
}
});
window._codeloreRegisterSelectionListener('coupling', function (selectedPath) {
chart.dispatchAction({ type: 'downplay' });
if (!selectedPath) return;
const nodeName = (typeof userSankeyDepth === 'number')
? modulePathSeg(selectedPath, userSankeyDepth)
: selectedPath;
chart.dispatchAction({ type: 'highlight', seriesIndex: 0, name: nodeName });
});
}
function renderTrends(rows) {
const container = document.getElementById('widget-trends-body');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<div class="empty">No trend data — repo too small or analyses not wired.</div>';
return;
}
const trendsLayout = (window.Alpine && window.Alpine.store)
? window.Alpine.store('layout') : null;
const trendsTopN = trendsLayout ? trendsLayout.trendsTopN : 10;
const months = Array.from(new Set(rows.map(function (r) { return r.month; }))).sort();
const byMonth = {};
const pathTotals = {};
for (var i = 0; i < rows.length; i++) {
const r = rows[i];
if (!byMonth[r.month]) byMonth[r.month] = {};
byMonth[r.month][r.path] = r.hotspot_score;
pathTotals[r.path] = (pathTotals[r.path] || 0) + (r.hotspot_score || 0);
}
const allPaths = Object.keys(pathTotals)
.sort(function (a, b) { return pathTotals[b] - pathTotals[a]; });
const paths = (trendsTopN === 'all')
? allPaths
: allPaths.slice(0, Number(trendsTopN));
const series = paths.map(function (p) {
return {
name: p,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
emphasis: { focus: 'series', lineStyle: { width: 3 } },
blur: { lineStyle: { opacity: 0.15 } },
data: months.map(function (m) {
return (byMonth[m] && byMonth[m][p]) || 0;
}),
};
});
function shortPath(p) {
const parts = (p || '').split('/');
if (parts.length <= 3) return p;
return parts[0] + '/…/' + parts.slice(-2).join('/');
}
const shortByLong = {};
const longByShort = {};
paths.forEach(function (p) {
let label = shortPath(p);
if (longByShort[label] !== undefined && longByShort[label] !== p) {
label = p;
}
shortByLong[p] = label;
longByShort[label] = p;
});
const legendData = paths.map(function (p) { return shortByLong[p]; });
setChartAriaLabel(container,
'Hotspot-score trend for ' + paths.length + ' files over ' +
months.length + ' months');
const chart = mountEcharts(container);
chart.setOption({
tooltip: {
trigger: 'axis',
formatter: function (params) {
if (!params || !params.length) return '';
var html = '<b>' + escapeHtml(params[0].axisValueLabel || params[0].name) + '</b>';
for (var i = 0; i < params.length; i++) {
const p = params[i];
const full = longByShort[p.seriesName] || p.seriesName;
html += '<br/>' + p.marker + escapeHtml(full) + ': <b>' + p.value + '</b>';
}
return html;
},
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 8,
top: 8,
bottom: 30,
textStyle: { color: getCssVar('--fg-dim'), fontSize: 11 },
pageTextStyle: { color: getCssVar('--fg-dim'), fontSize: 11 },
data: legendData,
itemGap: 6,
pageButtonGap: 4,
width: 220,
selector: [
{ type: 'all', title: 'All' },
{ type: 'inverse', title: 'Swap' },
],
selectorPosition: 'end',
selectorButtonGap: 4,
selectorLabel: {
color: getCssVar('--fg-dim'),
fontSize: 10,
padding: [2, 6],
borderColor: getCssVar('--border'),
borderWidth: 1,
borderRadius: 4,
},
},
grid: { top: 16, left: 70, right: 248, bottom: 40 },
xAxis: {
type: 'category',
data: months,
axisLabel: { color: getCssVar('--fg-dim'), fontSize: 11 },
axisLine: { lineStyle: { color: getCssVar('--border') } },
},
yAxis: {
type: 'value',
name: 'revisions / month',
nameLocation: 'middle',
nameRotate: 90,
nameGap: 40,
nameTextStyle: { color: getCssVar('--fg-dim'), fontSize: 11 },
axisLabel: { color: getCssVar('--fg-dim'), fontSize: 11 },
splitLine: { lineStyle: { color: getCssVar('--bg-elev-2') } },
},
series: series.map(function (s) {
return Object.assign({}, s, { name: shortByLong[s.name] || shortPath(s.name) });
}),
});
window._codeloreRegisterSelectionListener('trends', function (selectedPath) {
chart.dispatchAction({ type: 'downplay' });
if (!selectedPath) return;
const idx = paths.indexOf(selectedPath);
if (idx >= 0) {
chart.dispatchAction({ type: 'highlight', seriesIndex: idx });
}
});
}
function renderKameiRiskSparkline(allRows) {
const container = document.getElementById('widget-kamei-risk-body');
if (!container) return;
if (!allRows.length) {
container.innerHTML = '<div class="empty">No Kamei JIT-SDP data — repo too small or kamei::enrich was not wired.</div>';
return;
}
const kameiLayout = (window.Alpine && window.Alpine.store)
? window.Alpine.store('layout') : null;
const kameiWindow = kameiLayout ? kameiLayout.kameiWindow : 30;
const rows = (kameiWindow === 'all')
? allRows
: allRows.slice(-Number(kameiWindow));
function logCap(v) { return Math.log1p(Math.max(0, v)); }
var maxSize = 1, maxSpread = 1, maxConcurrency = 1, maxExp = 1, maxEntropy = 1;
for (var i = 0; i < rows.length; i++) {
const r = rows[i];
maxSize = Math.max(maxSize, logCap((r.la || 0) + (r.ld || 0)));
maxSpread = Math.max(maxSpread, logCap(r.nf || 0));
maxConcurrency = Math.max(maxConcurrency, logCap(r.ndev || 0));
maxExp = Math.max(maxExp, logCap(r.exp || 0));
maxEntropy = Math.max(maxEntropy, r.entropy || 0);
}
function scoreOf(r) {
const size = logCap((r.la || 0) + (r.ld || 0)) / maxSize;
const spread = logCap(r.nf || 0) / maxSpread;
const concurrency = logCap(r.ndev || 0) / maxConcurrency;
const inexperience = 1 - (logCap(r.exp || 0) / maxExp);
const entropy = (r.entropy || 0) / (maxEntropy || 1);
const composite = 0.30 * size + 0.20 * spread + 0.20 * concurrency
+ 0.20 * inexperience + 0.10 * entropy;
return {
composite: Math.max(0, Math.min(1, composite)),
size: size, spread: spread, concurrency: concurrency,
inexperience: inexperience, entropy: entropy,
};
}
function dominantDimension(s) {
const dims = [
{ name: 'size', v: 0.30 * s.size },
{ name: 'spread', v: 0.20 * s.spread },
{ name: 'concurrency', v: 0.20 * s.concurrency },
{ name: 'inexperience',v: 0.20 * s.inexperience },
{ name: 'entropy', v: 0.10 * s.entropy },
];
dims.sort(function (a, b) { return b.v - a.v; });
return dims[0].name;
}
const seriesData = rows.map(function (r) {
const s = scoreOf(r);
const dom = dominantDimension(s);
const color = r.fix
? token('--color-error')
: heatRamp(s.composite);
return {
value: s.composite,
itemStyle: { color: color },
_rev: r.rev, _date: r.date, _row: r, _score: s, _dom: dom,
};
});
var fixCount = 0;
for (var fi = 0; fi < rows.length; fi++) {
if (rows[fi].fix) fixCount++;
}
setChartAriaLabel(container,
'Kamei delivery-risk per commit over ' + rows.length + ' recent commits, ' +
fixCount + ' flagged as bug-fixes');
const chart = mountEcharts(container);
chart.setOption({
tooltip: {
trigger: 'item',
appendTo: function (chartDom) {
return chartDom.closest('section.widget') || document.body;
},
position: function () {
return { top: 4, right: 4 };
},
formatter: function (params) {
const d = params.data || {};
const r = d._row || {};
const s = d._score || {};
const dom = d._dom || 'size';
const fmtPct = function (v) { return Math.round(v * 100) + '%'; };
return '<b>' + escapeHtml((r.rev || '').slice(0, 8)) + '</b>'
+ '<br/><small>' + escapeHtml(r.date || '') + '</small>'
+ '<br/>composite: <b>' + fmtPct(s.composite) + '</b>'
+ ' · dominant: <b>' + dom + '</b>'
+ (r.fix ? '<br/><span class="badge badge-error badge-sm">bug-fix</span>' : '')
+ '<br/><br/><small>'
+ 'la=' + (r.la || 0) + ' · ld=' + (r.ld || 0)
+ ' · nf=' + (r.nf || 0) + ' · ndev=' + (r.ndev || 0)
+ ' · exp=' + (r.exp || 0) + ' · entropy=' + (r.entropy || 0).toFixed(2)
+ '</small>'
+ '<br/><small style="opacity:.6;">'
+ 'size=' + fmtPct(s.size) + ' · spread=' + fmtPct(s.spread)
+ ' · concurrency=' + fmtPct(s.concurrency)
+ ' · inexp=' + fmtPct(s.inexperience)
+ ' · entropy=' + fmtPct(s.entropy)
+ '</small>';
},
},
grid: { top: 14, left: 50, right: 20, bottom: 30 },
xAxis: {
type: 'category',
data: rows.map(function (r) { return r.date; }),
axisLabel: { color: getCssVar('--fg-dim'), fontSize: 10, rotate: 45 },
axisLine: { lineStyle: { color: getCssVar('--border') } },
},
yAxis: {
type: 'value',
min: 0, max: 1,
name: 'risk',
nameTextStyle: { color: getCssVar('--fg-dim'), fontSize: 10 },
axisLabel: {
color: getCssVar('--fg-dim'), fontSize: 10,
formatter: function (v) { return Math.round(v * 100) + '%'; },
},
splitLine: { lineStyle: { color: getCssVar('--bg-elev-2') } },
},
series: [{
type: 'bar',
data: seriesData,
barCategoryGap: '20%',
emphasis: { disabled: true },
}],
});
}
function renderDeliveryCard(d) {
const container = document.getElementById('widget-delivery-card-body');
if (!container) return;
const dm = d.delivery_metrics || [];
const cadence = d.release_cadence || [];
const friction = d.delivery_friction || [];
if (!dm.length && !cadence.length && !friction.length) {
container.innerHTML = '<div class="empty">No delivery data — run with --include-merges and release tags matching --release-tag-glob.</div>';
return;
}
function findMetric(name) {
for (var i = 0; i < dm.length; i++) {
if (dm[i].metric === name) return dm[i];
}
return null;
}
var rows = '';
var rework = findMetric('rework_pct');
if (rework) {
var rPct = typeof rework.p50 === 'number' ? rework.p50.toFixed(1) : '—';
var rBand = rework.p50 < 9 ? 'green' : rework.p50 < 15 ? 'yellow' : 'red';
rows += '<tr><td>Rework</td>' +
'<td class="delivery-value" style="color:' + bandColor(rBand) + ';">' + rPct + ' %</td>' +
'<td class="delivery-caveat">' + escapeHtml(rework.caveat || '') + '</td></tr>';
}
var branch = findMetric('branch_duration_hours');
if (branch) {
var bVal = typeof branch.p75 === 'number' ? branch.p75.toFixed(0) + ' h' : '—';
rows += '<tr><td>Branch p75</td><td class="delivery-value">' + bVal + '</td>' +
'<td class="delivery-caveat">' + escapeHtml(branch.caveat || '') + '</td></tr>';
}
var lead = findMetric('lead_proxy_hours');
if (lead) {
var lVal = typeof lead.p50 === 'number' ? lead.p50.toFixed(0) + ' h' : '—';
rows += '<tr><td>Lead proxy p50</td><td class="delivery-value">' + lVal + '</td>' +
'<td class="delivery-caveat">' + escapeHtml(lead.caveat || '') + '</td></tr>';
}
var summary = null;
for (var ci = 0; ci < cadence.length; ci++) {
if (cadence[ci].tag === '__summary__') { summary = cadence[ci]; break; }
}
if (summary && typeof summary.days_since_prev === 'number') {
var cVal = summary.days_since_prev.toFixed(0) + ' d';
var trend = summary.trend ? ' (' + escapeHtml(summary.trend) + ')' : '';
rows += '<tr><td>Cadence median</td><td class="delivery-value">' + cVal + trend + '</td><td></td></tr>';
}
var frictionHtml = '';
if (friction.length > 0) {
frictionHtml = '<div class="delivery-friction-header">Where is friction:</div>' +
'<ol class="delivery-friction-list">';
for (var fi = 0; fi < friction.length; fi++) {
var f = friction[fi];
frictionHtml += '<li>' + escapeHtml(f.path || '') + '</li>';
}
frictionHtml += '</ol>';
}
container.innerHTML =
'<table class="delivery-table">' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
frictionHtml +
'<div class="delivery-disclaimer">Git-only proxies — not DORA metrics.</div>';
}
function renderHotspotTreemap(rows) {
const container = document.getElementById('widget-hotspot-treemap-body');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<div class="empty">No hotspot data for treemap.</div>';
return;
}
const TREEMAP_CAP = 200;
const top = rows.slice()
.sort(function (a, b) {
const sa = (typeof a.hotspot_score === 'number') ? a.hotspot_score : -Infinity;
const sb = (typeof b.hotspot_score === 'number') ? b.hotspot_score : -Infinity;
return sb - sa;
})
.slice(0, TREEMAP_CAP);
const grouped = {};
for (var i = 0; i < top.length; i++) {
const r = top[i];
const parts = (r.path || '').split('/');
const dir = parts.length > 1 ? parts[0] : '<root>';
if (!grouped[dir]) grouped[dir] = [];
grouped[dir].push({
name: r.path,
value: r.revisions || 1,
cognitive: r.cognitive || 0,
cognitive_health: r.cognitive_health,
hotspot_score: r.hotspot_score,
});
}
const treeData = Object.keys(grouped).sort().map(function (dir) {
return { name: dir, children: grouped[dir] };
});
setChartAriaLabel(container,
'Hotspot treemap of ' + top.length + ' files across ' +
treeData.length + ' top-level directories, sized by revisions');
const chart = mountEcharts(container);
chart.setOption({
tooltip: {
formatter: function (params) {
const d = params.data || {};
if (!d.cognitive) return '<b>' + escapeHtml(d.name || '') + '</b><br/>directory';
return '<b>' + escapeHtml(d.name) + '</b>' +
'<br/>revisions: ' + (d.value || 0) +
'<br/>cognitive: ' + d.cognitive.toFixed(0) +
(d.cognitive_health != null ? '<br/>cognitive health: ' + d.cognitive_health.toFixed(1) : '') +
(d.hotspot_score != null ? '<br/>score: ' + d.hotspot_score.toFixed(2) : '');
},
},
series: [{
type: 'treemap',
data: treeData,
roam: false,
leafDepth: 2,
breadcrumb: {
show: true,
top: 6,
left: 6,
itemStyle: {
color: getCssVar('--bg-elev'),
borderColor: getCssVar('--border'),
textStyle: { color: getCssVar('--fg') },
},
},
label: { show: true, color: token('--label-on-saturated'), fontSize: 11 },
upperLabel: { show: true, height: 18, color: getCssVar('--fg-dim'), fontSize: 11 },
levels: [
{ itemStyle: { borderColor: getCssVar('--border'), borderWidth: 3, gapWidth: 3 } },
{ itemStyle: { borderColor: getCssVar('--border'), borderWidth: 2, gapWidth: 2 } },
],
}],
});
chart.on('click', function (params) {
const d = params && params.data;
if (d && d.cognitive != null) {
if (window._codeloreShowDetail) {
window._codeloreShowDetail(d.name);
} else {
showFileDetailDrawer(d.name, data);
}
}
});
}
function renderParallelCoords(rows) {
const container = document.getElementById('widget-parallel-coords-body');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<div class="empty">No hotspot data for parallel coords.</div>';
return;
}
const parallelLayout = (window.Alpine && window.Alpine.store)
? window.Alpine.store('layout') : null;
const parallelTopN = parallelLayout ? parallelLayout.parallelTopN : 20;
const sorted = rows.slice()
.sort(function (a, b) {
const sa = (typeof a.hotspot_score === 'number') ? a.hotspot_score : -Infinity;
const sb = (typeof b.hotspot_score === 'number') ? b.hotspot_score : -Infinity;
return sb - sa;
});
const top = (parallelTopN === 'all') ? sorted : sorted.slice(0, Number(parallelTopN));
setChartAriaLabel(container,
'Parallel-coordinates plot of ' + top.length + ' files across ' +
'revisions, cognitive complexity, cognitive health, hotspot score and MI rank');
const chart = mountEcharts(container);
const parallelData = top.map(function (r) {
return {
name: r.path,
value: [
r.revisions || 0,
r.cognitive || 0,
r.cognitive_health != null ? r.cognitive_health : 0,
r.hotspot_score != null ? r.hotspot_score : 0,
typeof r.mi_rank === 'number' ? r.mi_rank : 0,
],
};
});
chart.setOption({
parallelAxis: [
{ dim: 0, name: 'Revisions' },
{ dim: 1, name: 'Cognitive' },
{ dim: 2, name: 'Cognitive health', inverse: true },
{ dim: 3, name: 'Hotspot score' },
{ dim: 4, name: 'MI rank', max: 1.0 },
],
parallel: {
left: 50, right: 50, top: 30, bottom: 30,
axisExpandable: false,
parallelAxisDefault: {
axisLabel: { color: getCssVar('--fg-dim'), fontSize: 10 },
nameTextStyle: { color: getCssVar('--fg-dim'), fontSize: 11 },
axisLine: { lineStyle: { color: getCssVar('--border') } },
},
},
tooltip: {
trigger: 'item',
position: function (point, params, dom, rect, size) {
return [size.viewSize[0] - size.contentSize[0] - 12, 8];
},
confine: true,
formatter: function (params) {
const v = params.value || [];
return '<b>' + escapeHtml(params.name || '') + '</b>' +
'<br/>revisions: ' + (v[0] || 0) +
'<br/>cognitive: ' + (v[1] || 0).toFixed(0) +
'<br/>health: ' + (v[2] || 0).toFixed(1) +
'<br/>score: ' + (v[3] || 0).toFixed(2) +
'<br/>MI rank: ' + (typeof v[4] === 'number' ? (v[4] * 100).toFixed(0) + '%' : '—');
},
},
series: [{
type: 'parallel',
lineStyle: { width: 1, opacity: 0.6, color: token('--color-warning') },
emphasis: { disabled: true },
data: parallelData,
}],
});
chart.on('click', function (params) {
if (params && params.name) {
if (window._codeloreShowDetail) {
window._codeloreShowDetail(params.name);
} else {
showFileDetailDrawer(params.name, data);
}
}
});
const parallelPaths = top.map(function (r) { return r.path; });
const parallelBase = { color: token('--color-warning'), width: 1, opacity: 0.6 };
window._codeloreRegisterSelectionListener('parallel-coords', function (selectedPath) {
const idx = selectedPath ? parallelPaths.indexOf(selectedPath) : -1;
for (var i = 0; i < parallelData.length; i++) {
if (idx < 0) {
parallelData[i].lineStyle = parallelBase;
} else if (i === idx) {
parallelData[i].lineStyle = { color: token('--color-info'), width: 3, opacity: 1 };
} else {
parallelData[i].lineStyle = { color: token('--color-warning'), width: 1, opacity: 0.12 };
}
}
chart.setOption({ series: [{ data: parallelData }] });
});
}
function renderCognitiveBoxplot(rows) {
const container = document.getElementById('widget-cognitive-boxplot-body');
if (!container) return;
const values = rows
.map(function (r) { return r.cognitive; })
.filter(function (v) { return typeof v === 'number' && v > 0; })
.sort(function (a, b) { return a - b; });
if (values.length < 5) {
container.innerHTML = '<div class="empty">Insufficient data for boxplot.</div>';
return;
}
function quantile(arr, q) {
const pos = (arr.length - 1) * q;
const base = Math.floor(pos);
const rest = pos - base;
return arr[base + 1] !== undefined
? arr[base] + rest * (arr[base + 1] - arr[base])
: arr[base];
}
const min = values[0];
const q1 = quantile(values, 0.25);
const med = quantile(values, 0.5);
const q3 = quantile(values, 0.75);
const iqr = q3 - q1;
const upperFence = q3 + 1.5 * iqr;
const lowerFence = Math.max(0, q1 - 1.5 * iqr);
const max = Math.min(upperFence, values[values.length - 1]);
const outliers = [];
for (var i = 0; i < values.length; i++) {
if (values[i] > upperFence || values[i] < lowerFence) {
outliers.push([0, values[i]]);
}
}
const chart = mountEcharts(container);
const maxOutlier = outliers.length
? outliers.reduce(function (m, o) { return o[1] > m ? o[1] : m; }, 0)
: 0;
setChartAriaLabel(container,
'Cognitive-complexity distribution across ' + values.length +
' functions, median ' + Math.round(med) + ', ' + outliers.length + ' outliers');
const yAxisMax = Math.ceil(upperFence * 1.15);
chart.setOption({
tooltip: { trigger: 'item' },
grid: { top: 30, left: 60, right: 24, bottom: 36 },
xAxis: {
type: 'category',
data: ['cognitive'],
boundaryGap: true,
axisLabel: { color: getCssVar('--fg-dim') },
},
yAxis: {
type: 'value',
min: 0,
max: yAxisMax,
axisLabel: { color: getCssVar('--fg-dim') },
splitLine: { lineStyle: { color: getCssVar('--bg-elev-2') } },
},
series: [
{
type: 'boxplot',
boxWidth: [60, 140],
data: [[min, q1, med, q3, max]],
itemStyle: { color: token('--color-warning'), borderColor: token('--color-error') },
},
],
graphic: outliers.length
? [{
type: 'text',
right: 16,
top: 8,
style: {
text: '+' + outliers.length + ' outliers · max ' + Math.round(maxOutlier),
fill: getCssVar('--fg-dim'),
fontSize: 11,
},
}]
: [],
});
}
function renderModuleChord(rows) {
const container = document.getElementById('widget-module-chord-body');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<div class="empty">No coupling data for module chord.</div>';
return;
}
function modulePath(p, depth) {
const parts = (p || '').split('/');
if (parts.length <= depth) {
const lastSlash = (p || '').lastIndexOf('/');
return lastSlash < 0 ? (p || '') : p.slice(0, lastSlash);
}
return parts.slice(0, depth).join('/');
}
function isInfrastructureFile(p) {
if (!p) return false;
if (/\.lock$/i.test(p) || /-lock\.json$/i.test(p)) return true;
if (/(^|\/)\.env(\..+)?$/i.test(p)) return true;
if (/^docs?\//i.test(p)) return true;
if (/\.(md|rst|txt|adoc)$/i.test(p)) return true;
if (/(^|\/)(pyproject|Cargo|package|composer|Gemfile|setup)\.(toml|json|yaml|yml)$/i.test(p)) return true;
if (/(^|\/)(requirements[^/]*|setup)\.(txt|cfg|py)$/i.test(p)) return true;
if (/^\.github\//i.test(p) || /^\.gitlab/i.test(p)) return true;
if (/^\.(gitignore|gitattributes|dockerignore|editorconfig|prettierrc|eslintrc)/i.test(p)) return true;
if (/(^|\/)VERSION$/.test(p) || /(^|\/)CHANGELOG(\.[^/]+)?$/i.test(p)) return true;
return false;
}
const MIN_NODES_FOR_USEFUL_CHORD = 6;
const layout = (window.Alpine && window.Alpine.store)
? window.Alpine.store('layout') : null;
const userChordDepth = layout ? layout.chordDepth : 'auto';
function aggregateAt(depth) {
const ee = {};
const nn = {};
for (var i = 0; i < rows.length; i++) {
const r = rows[i];
if (isInfrastructureFile(r.entity_a) || isInfrastructureFile(r.entity_b)) continue;
const a = modulePath(r.entity_a, depth);
const b = modulePath(r.entity_b, depth);
if (!a || !b || a === b) continue;
const key = a < b ? a + '\x00' + b : b + '\x00' + a;
ee[key] = (ee[key] || 0) + (r.shared || 1);
nn[a] = true;
nn[b] = true;
}
return { edges: ee, nodeCount: Object.keys(nn).length };
}
var edges = {};
if (typeof userChordDepth === 'number') {
edges = aggregateAt(userChordDepth).edges;
} else {
for (var depth = 2; depth <= 6; depth++) {
const result = aggregateAt(depth);
edges = result.edges;
if (result.nodeCount >= MIN_NODES_FOR_USEFUL_CHORD) break;
}
}
const linkRows = Object.keys(edges).map(function (k) {
const parts = k.split('\x00');
return { source: parts[0], target: parts[1], value: edges[k] };
});
if (!linkRows.length) {
container.innerHTML = '<div class="empty">All change-coupling stays inside a single 2-segment module after dropping infrastructure files (lock / env / docs / build manifests). See the raw <em>coupling</em> table for the full pair list.</div>';
return;
}
const nodes = {};
for (var ei = 0; ei < linkRows.length; ei++) {
nodes[linkRows[ei].source] = true;
nodes[linkRows[ei].target] = true;
}
function chordTopGroup(name) {
const slash = name.indexOf('/');
return slash < 0 ? name : name.slice(0, slash);
}
const sortedNames = Object.keys(nodes).sort();
const groupNames = [];
const groupIndex = {};
for (var gi = 0; gi < sortedNames.length; gi++) {
const g = chordTopGroup(sortedNames[gi]);
if (!(g in groupIndex)) { groupIndex[g] = groupNames.length; groupNames.push(g); }
}
const chordPerNode = groupNames.length < 2;
const categories = chordPerNode
? sortedNames.map(function (n) { return { name: n }; })
: groupNames.map(function (g) { return { name: g }; });
const nodeArr = sortedNames.map(function (n, idx) {
return { name: n, category: chordPerNode ? idx : groupIndex[chordTopGroup(n)] };
});
setChartAriaLabel(container,
'Module change-coupling chord diagram, ' + nodeArr.length + ' modules and ' +
linkRows.length + ' coupled pairs');
const chart = mountEcharts(container);
chart.setOption({
tooltip: { trigger: 'item' },
series: [{
type: 'graph',
layout: 'circular',
circular: { rotateLabel: true },
data: nodeArr,
categories: categories,
links: linkRows,
roam: false,
label: { show: true, color: getCssVar('--fg-dim'), fontSize: 10, position: 'right' },
lineStyle: {
color: 'source',
opacity: 0.45,
curveness: 0.45,
},
emphasis: { focus: 'adjacency', lineStyle: { width: 3 } },
}],
});
}