diff --git a/plugins/value_rank/main.py b/plugins/value_rank/main.py index f0ab034..8e8ed80 100644 --- a/plugins/value_rank/main.py +++ b/plugins/value_rank/main.py @@ -1295,6 +1295,73 @@ class ValueRankPlugin(MessagePluginInterface): if not selected_edges: return None + # 关系图改为“全员 + 主要边”策略: + # 1. 用户希望不要限制图上的人数,因此节点仍尽量保留全量; + # 2. 真正导致看不清的不是人数,而是边过多、边标签过多; + # 3. 这里对展示边做一层稀疏化,只保留每个人的主要关系和全图最强的若干边。 + full_selected_edges = selected_edges + display_edge_cap = max(18, int(len(selected_nodes) * 1.18)) + display_edge_cap = min(len(full_selected_edges), display_edge_cap) + if len(full_selected_edges) > display_edge_cap: + core_node_count = max(3, min(6, len(selected_nodes) // 8 + 2)) + core_nodes = set(selected_nodes[:core_node_count]) + node_degree_cap: Dict[str, int] = { + uid: (6 if uid in core_nodes else 3) + for uid in selected_nodes + } + edge_degree_map: Dict[str, int] = {uid: 0 for uid in selected_nodes} + strongest_edge_by_node: Dict[str, Tuple[str, str, float, float, int, int]] = {} + sorted_edges = sorted( + full_selected_edges, + key=lambda item: ( + -float(item[3]), + -float(item[2]), + -min(len(partner_map.get(item[0], set())), len(partner_map.get(item[1], set()))), + item[0], + item[1], + ), + ) + + for edge in sorted_edges: + a, b = edge[0], edge[1] + if a not in strongest_edge_by_node: + strongest_edge_by_node[a] = edge + if b not in strongest_edge_by_node: + strongest_edge_by_node[b] = edge + + kept_edge_keys = set() + pruned_edges: List[Tuple[str, str, float, float, int, int]] = [] + + def try_append_edge(edge_item: Tuple[str, str, float, float, int, int], ignore_degree_cap: bool = False) -> bool: + edge_a, edge_b = edge_item[0], edge_item[1] + edge_key = (edge_a, edge_b) + if edge_key in kept_edge_keys: + return False + if not ignore_degree_cap: + if edge_degree_map.get(edge_a, 0) >= node_degree_cap.get(edge_a, 3): + return False + if edge_degree_map.get(edge_b, 0) >= node_degree_cap.get(edge_b, 3): + return False + kept_edge_keys.add(edge_key) + pruned_edges.append(edge_item) + edge_degree_map[edge_a] = edge_degree_map.get(edge_a, 0) + 1 + edge_degree_map[edge_b] = edge_degree_map.get(edge_b, 0) + 1 + return True + + # 先保证每个人至少能挂上一条自己最强的关系线,避免外围节点变成“孤点”。 + for uid in selected_nodes: + edge = strongest_edge_by_node.get(uid) + if edge: + try_append_edge(edge, ignore_degree_cap=True) + + # 再按全图强度补满,兼顾整体关系结构,但限制每个节点的展示边数。 + for edge in sorted_edges: + if len(pruned_edges) >= display_edge_cap: + break + try_append_edge(edge, ignore_degree_cap=False) + + selected_edges = pruned_edges + html_content = self._build_social_graph_html(group_id, selected_nodes, selected_edges, partner_map, node_score_map) if not html_content: return None @@ -1325,8 +1392,8 @@ class ValueRankPlugin(MessagePluginInterface): import html - width = 1460 - height = 1040 + width = 1540 + height = 1120 cx, cy = width // 2, height // 2 + 24 node_count = len(selected_nodes) @@ -1338,9 +1405,9 @@ class ValueRankPlugin(MessagePluginInterface): # 1. 核心节点(连接人数更多)放在内圈,外围节点逐层向外扩散; # 2. 每圈容量递增,避免所有人都挤在一个圆上; # 3. 外圈增加轻微抖动和椭圆拉伸,让大图下更分散,不会像等分钟表一样拥挤。 - inner_margin = 150.0 - outer_margin_x = 120.0 - outer_margin_y = 110.0 + inner_margin = 188.0 + outer_margin_x = 108.0 + outer_margin_y = 102.0 max_rx = width * 0.5 - outer_margin_x max_ry = height * 0.5 - outer_margin_y ring_capacities: List[int] = [] @@ -1348,9 +1415,9 @@ class ValueRankPlugin(MessagePluginInterface): ring_index = 0 while placed < node_count: if ring_index == 0: - capacity = min(4, node_count) + capacity = min(3, node_count) elif ring_index == 1: - capacity = 8 + capacity = 7 else: capacity = 12 + ring_index * 6 ring_capacities.append(capacity) @@ -1393,20 +1460,42 @@ class ValueRankPlugin(MessagePluginInterface): cm = ContactManager.get_instance() edge_svg_parts: List[str] = [] - # 双向@标签层: - # 1. 每条边只保留双向次数数字,避免标签信息量过大把图面压得太满; - # 2. 仍保留白底半透明标签,确保在密集连线里也能看清数值。 + # 边数字标签只给“最重要的少数边”: + # 1. 全边标数字时,图面会被 0/1、1/0 这类噪声淹没; + # 2. 这里保留最强的一小部分边标签,其余只看粗细即可; + # 3. 这样既能保留方向信息,也不至于牺牲整体结构可读性。 edge_label_parts: List[str] = [] + max_labeled_edges = max(6, min(12, len(selected_nodes) // 4 + 3)) + labeled_edge_keys = { + (edge[0], edge[1]) + for edge in sorted( + selected_edges, + key=lambda item: (-float(item[3]), -float(item[2]), item[0], item[1]), + )[:max_labeled_edges] + } + + def _shorten_graph_nick(raw_name: str, max_len: int = 8) -> str: + text = str(raw_name or "").strip() + return text if len(text) <= max_len else f"{text[:max_len]}…" + + def _estimate_label_units(raw_text: str) -> float: + units = 0.0 + for ch in str(raw_text or ""): + units += 1.0 if ord(ch) < 128 else 1.75 + return units + for edge_idx, (a, b, mention_count, score, a_to_b_count, b_to_a_count) in enumerate(selected_edges, start=1): ax, ay = pos_map[a] bx, by = pos_map[b] normalized = max(0.12, min(score / max_edge_score, 1.0)) - stroke_width = 1.0 + 7.0 * normalized - opacity = 0.20 + 0.55 * normalized + stroke_width = 1.2 + 6.2 * normalized + opacity = 0.14 + 0.42 * normalized edge_svg_parts.append( f'' ) + if (a, b) not in labeled_edge_keys: + continue # 通过字典序固定方向,确保同一条边每次渲染文案方向一致。 # 当前标签只保留双向互动数字,同时继续沿法线方向偏移,避免数字压在线条上。 mx, my = (ax + bx) / 2.0, (ay + by) / 2.0 @@ -1427,12 +1516,12 @@ class ValueRankPlugin(MessagePluginInterface): angle_deg += 180 label_text = f"{int(a_to_b_count)}/{int(b_to_a_count)}" safe_label = html.escape(label_text) - label_width = max(52.0, min(92.0, 10.0 * len(label_text) + 24.0)) + label_width = max(50.0, min(88.0, 10.0 * len(label_text) + 20.0)) edge_label_parts.append( f'' f'' - f'{safe_label}' + f'rx="7" ry="7" fill="rgba(255,255,255,0.84)">' + f'{safe_label}' f'' ) @@ -1450,7 +1539,8 @@ class ValueRankPlugin(MessagePluginInterface): score_norm = max(0.10, min(score / max(max_node_score, 1.0), 1.0)) node_radius = 18.0 + 18.0 * size_norm + 6.0 * score_norm nick = cm.get_group_name(group_id, uid) or uid - safe_nick = html.escape(str(nick)) + display_nick = _shorten_graph_nick(str(nick), 8) + safe_nick = html.escape(display_nick) avatar_url = str(cm.get_head_image(uid) or "").strip() ring_idx = int(node_meta_map.get(uid, {}).get("ring_idx", 0)) angle = float(node_meta_map.get(uid, {}).get("angle", 0.0)) @@ -1486,30 +1576,57 @@ class ValueRankPlugin(MessagePluginInterface): f'{html.escape(str(nick)[:1] or "?")}' ) - # 节点文案继续放在头像外侧,但整体收紧一圈: - # 1. 用户反馈昵称和头像分得太开,这里缩短外偏移距离; - # 2. 昵称字号调小,减少节点一多时的横向挤压; - # 3. 统计行与昵称保持较近距离,让“头像-昵称-数据”看起来是一组。 + # “连接人数”改成贴在头像上的数字徽标: + # 1. 用户反馈原先那行“连接x · 分数y”混在线条里看不清; + # 2. 数字徽标直接挂在节点边缘,读图时更像“这个人连了多少人”; + # 3. 图里移除分数展示,只保留更关键的连接人数,减少认知负担。 + badge_text = str(int(partner_count)) + badge_font_size = 11 if len(badge_text) <= 2 else 10 + badge_width = 16.0 + len(badge_text) * 7.5 + badge_height = 20.0 + badge_x = x + node_radius * 0.58 - badge_width / 2.0 + badge_y = y + node_radius * 0.55 - badge_height / 2.0 + node_svg_parts.append( + f'' + ) + node_svg_parts.append( + f'{badge_text}' + ) + + # 节点文案只保留昵称,并加半透明底板: + # 1. 去掉第二行数值后,名字可以更干净地贴在节点旁边; + # 2. 底板能把名字从复杂连线背景里“托”出来; + # 3. 长昵称做截断,避免外围节点之间互相顶住。 outward_dx = math.cos(angle) outward_dy = math.sin(angle) - label_gap = node_radius + (11.0 if ring_idx == 0 else 17.0 + ring_idx * 2.0) + label_gap = node_radius + (14.0 if ring_idx == 0 else 18.0 + ring_idx * 2.0) title_x = x + outward_dx * label_gap title_y = y + outward_dy * label_gap - info_x = x + outward_dx * (label_gap + 12.0) - info_y = y + outward_dy * (label_gap + 12.0) + 1.5 if abs(outward_dx) < 0.20: anchor = "middle" elif outward_dx > 0: anchor = "start" else: anchor = "end" + label_units = _estimate_label_units(display_nick) + label_width = max(44.0, min(156.0, 10.0 * label_units + 18.0)) + label_height = 24.0 + if anchor == "middle": + label_box_x = title_x - label_width / 2.0 + elif anchor == "start": + label_box_x = title_x - 8.0 + else: + label_box_x = title_x - label_width + 8.0 + label_box_y = title_y - 18.0 node_svg_parts.append( - f'{safe_nick}' + f'' ) node_svg_parts.append( - f'连接{partner_count} · {score:.1f}' + f'{safe_nick}' ) group_title = html.escape(ContactManager.get_instance().get_nickname(group_id) or group_id) diff --git a/plugins/value_rank/templates/social_graph.html b/plugins/value_rank/templates/social_graph.html index 6ce9614..3ca8e18 100644 --- a/plugins/value_rank/templates/social_graph.html +++ b/plugins/value_rank/templates/social_graph.html @@ -58,7 +58,7 @@
- 说明:节点越大代表连接群友越多;连线越粗代表互动越强。节点优先显示成员头像,缺失头像时自动回退昵称首字。 + 说明:节点越大代表连接群友越多;头像右下角数字是连接人数;连线越粗代表互动越强。为保证可读性,图中仅保留主要关系线,边上的数字只标最强的一小部分关系。