summaryrefslogtreecommitdiff
path: root/index.html
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-11 14:08:40 -0500
committerhaoyuren <13851610112@163.com>2026-03-11 14:08:40 -0500
commit975fe4de28d0c0aacb89a9709dd6f82abb47b398 (patch)
tree989d895356ca9538543abe718dad984829f69237 /index.html
parent44d4610ee873c1963a5475f5bad5720bc495fe9a (diff)
Add auto-labeling for Leiden communities using coverage × specificity
For each community, scores all tags (node tags + internal edge relations) by coverage (how common within the community) × specificity (how exclusive to the community), and picks the highest-scoring tag as the label. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'index.html')
-rw-r--r--index.html58
1 files changed, 56 insertions, 2 deletions
diff --git a/index.html b/index.html
index 74b4563..11014fa 100644
--- a/index.html
+++ b/index.html
@@ -3190,6 +3190,59 @@ function convexHullPadded(points, padding) {
});
}
+function communityAutoLabel(memberIds, allCommunityGroups) {
+ const memberSet = new Set(memberIds);
+ // Collect all tags for each community: node tags + edge relation tags (internal edges)
+ const tagInternalCount = {}; // tag -> count within this community
+ const tagTotalCount = {}; // tag -> count across all communities
+
+ // Precompute total counts for all tags across all communities
+ const allTagTotal = {};
+ for (const [, members] of Object.entries(allCommunityGroups)) {
+ const mSet = new Set(members);
+ const seen = {}; // tag -> count in this community
+ members.forEach(id => {
+ const node = data.nodes.find(n => n.id === id);
+ if (node) (node.tags || []).forEach(t => { seen[t] = (seen[t] || 0) + 1; });
+ });
+ data.edges.forEach(e => {
+ const s = typeof e.source === 'object' ? e.source.id : e.source;
+ const t = typeof e.target === 'object' ? e.target.id : e.target;
+ if (mSet.has(s) && mSet.has(t)) {
+ (e.relations || []).forEach(r => { seen[r] = (seen[r] || 0) + 1; });
+ }
+ });
+ for (const [t, c] of Object.entries(seen)) {
+ allTagTotal[t] = (allTagTotal[t] || 0) + c;
+ }
+ }
+
+ // Count tags within this community
+ memberIds.forEach(id => {
+ const node = data.nodes.find(n => n.id === id);
+ if (node) (node.tags || []).forEach(t => { tagInternalCount[t] = (tagInternalCount[t] || 0) + 1; });
+ });
+ data.edges.forEach(e => {
+ const s = typeof e.source === 'object' ? e.source.id : e.source;
+ const t = typeof e.target === 'object' ? e.target.id : e.target;
+ if (memberSet.has(s) && memberSet.has(t)) {
+ (e.relations || []).forEach(r => { tagInternalCount[r] = (tagInternalCount[r] || 0) + 1; });
+ }
+ });
+
+ // Score = coverage × specificity
+ let bestTag = null, bestScore = -1;
+ const size = memberIds.length;
+ for (const [tag, intCount] of Object.entries(tagInternalCount)) {
+ const coverage = intCount / size;
+ const total = allTagTotal[tag] || intCount;
+ const specificity = intCount / total;
+ const score = coverage * specificity;
+ if (score > bestScore) { bestScore = score; bestTag = tag; }
+ }
+ return bestTag;
+}
+
function renderCommunityHulls() {
if (!communityEnabled || !communityMap) { communityHullGroup.selectAll('*').remove(); return; }
// Group nodes by community
@@ -3200,7 +3253,8 @@ function renderCommunityHulls() {
const sn = simulation.nodes().find(n => n.id === id);
return sn ? [sn.x, sn.y] : null;
}).filter(Boolean);
- return { cid: parseInt(cid), points, members };
+ const label = communityAutoLabel(members, groups);
+ return { cid: parseInt(cid), points, members, label };
});
const hulls = communityHullGroup.selectAll('.community-hull').data(hullData, d => d.cid);
hulls.exit().remove();
@@ -3216,7 +3270,7 @@ function renderCommunityHulls() {
.merge(labels)
.attr('x', d => d3.mean(d.points, p => p[0]))
.attr('y', d => d3.mean(d.points, p => p[1]) - 20)
- .text(d => `社群${d.cid+1} (${d.members.length})`);
+ .text(d => (d.label || '社群' + (d.cid+1)) + ` (${d.members.length})`);
}
// ===== Manual Groups =====