
©[悠悠智汇笔记] 版权所有
🙏请尊重劳动成果,守护每一份劳动心血;⚖️未经授权,不得以为任何方式转载、摘编或抄袭。🔄转载合作请后台联系授权,侵权必究。

01

导入库并设置全局参数

导入依赖库,并统一设置输入文件、输出图片、双层高度、视角角度、主题环形布局半径、簇扩散尺度、聚类阈值和边筛选比例
import pandas as pdimport networkx as nximport matplotlib.pyplotas pltimport numpy as npfrom collections import defaultdict, Counterfrom matplotlib.colorsimport to_rgbafrom matplotlib.patchesimportPolygon, PathPatchfrom matplotlib.pathimportPath# =========================================================# 参数区:全局视觉与逻辑控制中心# =========================================================NODE_FILE = "sci_nodes.csv"EDGE_FILE = "sci_edges.csv"OUTPUT_PNG = "double_layer_clustered_network_local.png"SHOW_LABELS = FalseSEED = 42TOP_Y = 4.9BOTTOM_Y = 0.0ALPHA_DEG = 8BETA_DEG = 4OUTER_RING_RX = 11.8OUTER_RING_RZ = 8.2INNER_RING_RX = 6.1INNER_RING_RZ = 4.5CLUSTER_CENTER_RADIUS_AUTHORS = 1.55CLUSTER_CENTER_RADIUS_PAPERS = 1.85AUTHOR_BURST_SCALE = 2.55PAPER_BURST_SCALE = 3.05AUTHOR_MAX_CLUSTER_SIZE = 14PAPER_MAX_CLUSTER_SIZE = 18RECUR_CLUSTER_MIN_SIZE = 4RECUR_CLUSTER_MAX_DEPTH = 4COLLAB_KEEP_RATIO = 0.42CITATION_KEEP_RATIO = 0.18
02

数据预处理

读取 sci_nodes.csv 和 sci_edges.csv,检查必备字段,并补齐缺失字段。如果本地节点表里没有 paper_count 或 citation_received,就根据边表自动计算。
# =========================================================# 读取本地数据:数据清洗与预处理# =========================================================defload_local_data(node_file, edge_file):nodes_df = pd.read_csv(node_file)edges_df = pd.read_csv(edge_file)required_node_cols = {"id", "lvl"}required_edge_cols = {"source", "target", "rel"}if not required_node_cols.issubset(nodes_df.columns):missing = required_node_cols - set(nodes_df.columns)raise ValueError(f"nodes 文件缺少必要字段: {missing}")if not required_edge_cols.issubset(edges_df.columns):missing = required_edge_cols - set(edges_df.columns)raise ValueError(f"edges 文件缺少必要字段: {missing}")nodes_df = nodes_df.copy()edges_df = edges_df.copy()nodes_df["id"] = nodes_df["id"].astype(str)edges_df["source"] = edges_df["source"].astype(str)edges_df["target"] = edges_df["target"].astype(str)edges_df["rel"] = edges_df["rel"].astype(str)nodes_df["lvl"] = nodes_df["lvl"].astype(int)optional_node_cols = ["theme_true", "subtopic_true", "paper_count", "citation_received","year", "paper_type", "quality"]forcolin optional_node_cols:if col not in nodes_df.columns:nodes_df[col] = ""if nodes_df["paper_count"].replace("", np.nan).isna().all():author_paper_count = defaultdict(int)auth_edges = edges_df[edges_df["rel"] == "authorship"]id_to_lvl = dict(zip(nodes_df["id"], nodes_df["lvl"]))for_, row in auth_edges.iterrows():src, tgt =row["source"], row["target"]if src in id_to_lvl and tgt in id_to_lvl:ifint(id_to_lvl[src]) == 2andint(id_to_lvl[tgt]) == 1:author_paper_count[src] += 1nodes_df["paper_count"] = nodes_df["id"].map(author_paper_count).fillna(nodes_df["paper_count"])if nodes_df["citation_received"].replace("", np.nan).isna().all():cited_count = defaultdict(int)cit_edges = edges_df[edges_df["rel"] == "citation"]for_, row in cit_edges.iterrows():cited_count[row["target"]] +=1nodes_df["citation_received"] = nodes_df["id"].map(cited_count).fillna(nodes_df["citation_received"])nodes_df["theme_true"] = nodes_df["theme_true"].fillna("").astype(str)nodes_df["subtopic_true"] = nodes_df["subtopic_true"].fillna("").astype(str)return nodes_df, edges_df
03

数据转换

将节点表和边表转换成networkx图。包括三种关系图:作者合作图、论文引用图、层间署名图;同时也构建一张包含全部节点和边的混合图。
# =========================================================# 构建图:将表格转换为 NetworkX 拓扑对象# =========================================================defbuild_weighted_relation_graph(nodes_df, edges_df, relation):H = nx.Graph()if relation == "collaboration":valid_nodes = set(nodes_df.loc[nodes_df["lvl"] == 2, "id"])rel_edges = edges_df[edges_df["rel"] == "collaboration"]elif relation == "citation":valid_nodes = set(nodes_df.loc[nodes_df["lvl"] == 1, "id"])rel_edges = edges_df[edges_df["rel"] == "citation"]elif relation == "authorship":valid_nodes = set(nodes_df["id"])rel_edges = edges_df[edges_df["rel"] == "authorship"]else:raise ValueError("Unknown relation")fornin valid_nodes:lvl = int(nodes_df.loc[nodes_df["id"] == n, "lvl"].iloc[0])H.add_node(n, lvl=lvl)for_, row in rel_edges.iterrows():u, v =row["source"], row["target"]if u not in valid_nodes or v not in valid_nodes:continueif H.has_edge(u, v):H[u][v]["weight"] += 1else:H.add_edge(u, v, weight=1)return Hdefbuild_full_graph(nodes_df, edges_df):G = nx.Graph()for_, row in nodes_df.iterrows():G.add_node(row["id"], lvl=int(row["lvl"]))for_, row in edges_df.iterrows():u, v =row["source"], row["target"]if G.has_edge(u, v):G[u][v]["weight"] += 1else:G.add_edge(u, v, weight=1)return G
04

分层递归聚类

用递归方式把作者合作图和论文引用图自动划分成多个社区。社区太大就继续拆,直到大小合适或者达到最大递归深度。
# =========================================================# 递归聚类:基于模块化度算法自动社区划分# =========================================================def recursive_detect_clusters(H,prefix,max_cluster_size=16,min_cluster_size=4,max_depth=4):final_groups = []defsplit_nodes(nodes, depth):sub = H.subgraph(nodes).copy()iflen(nodes) <= max_cluster_size:final_groups.append(set(nodes))returnifsub.number_of_edges() == 0:fornin nodes:final_groups.append({n})returnifdepth>=max_depth:final_groups.append(set(nodes))returncomms = list(nx.community.greedy_modularity_communities(sub, weight="weight"))iflen(comms) <= 1:final_groups.append(set(nodes))returnforcin comms:iflen(c) > max_cluster_size and len(c) >= min_cluster_size * 2:split_nodes(list(c), depth + 1)else:final_groups.append(set(c))forcompin nx.connected_components(H):split_nodes(list(comp), 0)final_groups = sorted(final_groups, key=lambda x: (-len(x), sorted(list(x))[0]))cluster_map = {}rows = []for i, group in enumerate(final_groups, start=1):cid = f"{prefix}{i}"members = sorted(list(group))fornin members:cluster_map[n] = cidrows.append({"cluster_id": cid,"member_count": len(members),"members": ",".join(members)})return cluster_map, pd.DataFrame(rows)
05

聚类主题映射

先根据theme_true把每个聚类归到某个主题,如果没有明确主题,就用层间作者—论文关系推断。然后再为每个主题分配颜色。
# =========================================================# 聚类 -> 主题映射# =========================================================defbuild_cluster_theme_maps(nodes_df, edges_df, author_cluster_map, paper_cluster_map):id_to_theme = dict(zip(nodes_df["id"], nodes_df["theme_true"]))if (nodes_df["theme_true"] != "").any():cluster_theme_map = {}forcidinsorted(set(author_cluster_map.values())):members = [n for n, c in author_cluster_map.items() if c == cid]themes = [id_to_theme[n] for n in members if id_to_theme.get(n, "") != ""]cluster_theme_map[cid] = Counter(themes).most_common(1)[0][0] if themes else cidforcidinsorted(set(paper_cluster_map.values())):members = [n for n, c in paper_cluster_map.items() if c == cid]themes = [id_to_theme[n] for n in members if id_to_theme.get(n, "") != ""]cluster_theme_map[cid] = Counter(themes).most_common(1)[0][0] if themes else cidreturn cluster_theme_mapauthor_clusters = sorted(set(author_cluster_map.values()))paper_clusters = sorted(set(paper_cluster_map.values()))author_theme_map = {}for i, ac in enumerate(author_clusters, start=1):author_theme_map[ac] = f"T{i:02d}"ac_pc_weights = defaultdict(int)auth_edges = edges_df[edges_df["rel"] == "authorship"]for_, row in auth_edges.iterrows():a, p =row["source"], row["target"]ac = author_cluster_map.get(a)pc = paper_cluster_map.get(p)if ac isnotNoneand pc isnotNone:ac_pc_weights[(ac, pc)] += 1paper_theme_map = {}forpcin paper_clusters:linked = [(ac, w) for (ac, pc2), w in ac_pc_weights.items() if pc2 == pc]if linked:best_ac = sorted(linked, key=lambda x: x[1], reverse=True)[0][0]paper_theme_map[pc] = author_theme_map[best_ac]else:paper_theme_map[pc] = pccluster_theme_map = {}cluster_theme_map.update(author_theme_map)cluster_theme_map.update(paper_theme_map)return cluster_theme_mapdefbuild_theme_colors(cluster_theme_map):palette = (list(plt.cm.tab20.colors)+ list(plt.cm.Set3.colors)+ list(plt.cm.Paired.colors))themes = sorted(set(cluster_theme_map.values()))theme_color_map = {theme: palette[i % len(palette)] for i, theme in enumerate(themes)}cluster_color_map = {cid: theme_color_map[t] for cid, t in cluster_theme_map.items()}return theme_color_map, cluster_color_map
06

确定主题位置

先把主题放到外环和内环上,再让同一主题下的多个社区围着这个主题锚点排开。这样整体画面更稳定。
# =========================================================# 主题锚点:多环圆形布局# =========================================================defbuild_theme_anchors_multiring(theme_list, outer_rx=11.8, outer_rz=8.2, inner_rx=6.1, inner_rz=4.5):theme_list = sorted(theme_list)n = len(theme_list)anchors = {}ifn<=6:for i, theme in enumerate(theme_list):angle = 2 * np.pi * i / nanchors[theme] = np.array([outer_rx * np.cos(angle), outer_rz * np.sin(angle)])return anchorsn_inner = max(3, int(round(n * 0.35)))n_outer = n - n_innerouter_themes = theme_list[:n_outer]inner_themes = theme_list[n_outer:]for i, theme in enumerate(outer_themes):angle = 2 * np.pi * i / n_outeranchors[theme] = np.array([outer_rx * np.cos(angle), outer_rz * np.sin(angle)])for i, theme in enumerate(inner_themes):angle = 2 * np.pi * i / n_inner + np.pi / n_inneranchors[theme] = np.array([inner_rx * np.cos(angle), inner_rz * np.sin(angle)])return anchors# =========================================================# 每个主题下多个簇的中心# =========================================================defbuild_cluster_centers(cluster_map, cluster_theme_map, theme_anchors, radius=1.2):theme_to_clusters = defaultdict(list)cluster_counts = defaultdict(int)forn, cid in cluster_map.items():cluster_counts[cid] += 1forcidinsorted(set(cluster_map.values())):theme_to_clusters[cluster_theme_map[cid]].append(cid)cluster_centers = {}fortheme, cids in theme_to_clusters.items():anchor = theme_anchors[theme]cids = sorted(cids, key=lambda x: cluster_counts[x], reverse=True)m = len(cids)ifm==1:cluster_centers[cids[0]] = anchor.copy()else:for j, cid in enumerate(cids):angle = 2 * np.pi * j / moffset= radius * np.array([np.cos(angle), np.sin(angle)])cluster_centers[cid] = anchor +offsetreturn cluster_centers
07

确定节点位置

将簇内节点散开,不要全部挤成一团。先按subtopic_true分组,再让每组节点沿几条“手臂”向外扩散。
# =========================================================# 烟花式布局# =========================================================def firework_cluster_layout(H,cluster_map,cluster_centers,node_subtopic_map=None,burst_scale=1.3,seed=42):rng = np.random.default_rng(seed)pos_xz = {}deg = dict(H.degree(weight="weight"))unique_clusters = sorted(set(cluster_map.values()))for idx, cid in enumerate(unique_clusters):members = [n for n, c in cluster_map.items() if c == cid]center = cluster_centers[cid]iflen(members)== 1:pos_xz[members[0]] = centercontinuegroups = defaultdict(list)fornin members:st = node_subtopic_map.get(n, f"{cid}_g0") if node_subtopic_map elsef"{cid}_g0"groups[st].append(n)group_keys = sorted(groups.keys())n_groups = len(group_keys)group_centers = {}ifn_groups==1:group_centers[group_keys[0]] = centerelse:group_ring = (0.92 + 0.22 * np.sqrt(len(members))) * burst_scalefor j, g in enumerate(group_keys):angle = 2 * np.pi * j / n_groups + 0.25offset= group_ring * np.array([np.cos(angle), np.sin(angle)])group_centers[g] = center +offsetfor j, g in enumerate(group_keys):g_nodes = sorted(groups[g], key=lambda x: deg.get(x, 0), reverse=True)g_center = group_centers[g]base_angle = 2 * np.pi * j / max(n_groups, 1)n = len(g_nodes)core_n = max(1, int(round(n * 0.14)))core_nodes = g_nodes[:core_n]outer_nodes = g_nodes[core_n:]fornodein core_nodes:ang = rng.uniform(0, 2 * np.pi)rad = rng.uniform(0.08, 0.24) * burst_scalepos_xz[node] = g_center + rad * np.array([np.cos(ang), np.sin(ang)])if outer_nodes:n_arms = min(6, max(3, int(np.ceil(len(outer_nodes) / 6))))arm_spread = 0.52for t, node in enumerate(outer_nodes):arm = t % n_armslayer = t // n_armsarm_angle = base_angle + (arm - (n_arms - 1) / 2) * arm_spread + rng.normal(0, 0.05)radius = (0.48 + layer * 0.36 + rng.uniform(-0.06, 0.08)) * burst_scalebase_vec = np.array([np.cos(arm_angle), np.sin(arm_angle)])tang_vec = np.array([-np.sin(arm_angle), np.cos(arm_angle)])tang_shift = rng.uniform(-0.12, 0.12) * burst_scalepos_xz[node] = g_center + radius * base_vec + tang_shift * tang_vecreturn pos_xz
08

布局投影成双层 3D 视觉

将作者层和论文层从x-z平面投到最终二维画布上,同时提供背景平面绘制、边筛选和层内曲线边绘制工具。
# =========================================================# 3D -> 2D 投影# =========================================================defproject_point(x, y, z, alpha_deg=8, beta_deg=4):alpha = np.radians(alpha_deg)beta = np.radians(beta_deg)xp = x * np.cos(beta) + z * np.sin(beta)yp = -x * np.sin(beta) * np.sin(alpha) + y * np.cos(alpha) + z * np.cos(beta) * np.sin(alpha)return np.array([xp, yp])defproject_multilevel_positions(nodes_df, author_pos_xz, paper_pos_xz, alpha_deg=8, beta_deg=4):pos_2d = {}for_, row in nodes_df.iterrows():n = row["id"]lvl = int(row["lvl"])iflvl==2:x, z = author_pos_xz[n]y = TOP_Yelse:x, z = paper_pos_xz[n]y = BOTTOM_Ypos_2d[n] = project_point(x, y, z, alpha_deg, beta_deg)return pos_2d# =========================================================# 背景平面# =========================================================defdraw_plane(ax, x_min, x_max, z_min, z_max, y_level, facecolor, alpha):corners_3d = [(x_min, y_level, z_min),(x_max, y_level, z_min),(x_max, y_level, z_max),(x_min, y_level, z_max),]corners_2d = [project_point(x, y, z, ALPHA_DEG, BETA_DEG) for x, y, z in corners_3d]poly = Polygon(corners_2d, closed=True, facecolor=facecolor, edgecolor="none", alpha=alpha, zorder=0)ax.add_patch(poly)# =========================================================# 边筛选# =========================================================defselect_top_edges(edges_with_score, keep_ratio):if not edges_with_score:return []edges_with_score = sorted(edges_with_score, key=lambda x: x[2], reverse=True)keep_n = max(1, int(len(edges_with_score) * keep_ratio))return edges_with_score[:keep_n]# =========================================================# 层内部曲线边# =========================================================defdraw_curved_intralayer_edges(ax, edge_items, pos, node_theme, node_color, same_alpha, cross_alpha, bend_ratio):for i, (u, v, score, width) in enumerate(edge_items):p0 = np.array(pos[u], dtype=float)p2 = np.array(pos[v], dtype=float)vec = p2 - p0dist = np.linalg.norm(vec)ifdist==0:continueperp = np.array([-vec[1], vec[0]], dtype=float)perp_norm = np.linalg.norm(perp)ifperp_norm==0:continueperp = perp / perp_normsign = 1 if (i % 2 == 0) else -1ctrl = (p0 + p2) / 2 + sign * bend_ratio * dist * perpverts = [p0, ctrl, p2]codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]path = Path(verts, codes)if node_theme[u] == node_theme[v]:color = to_rgba(node_color[u], same_alpha)else:color = (0.45, 0.45, 0.45, cross_alpha)patch = PathPatch(path, facecolor="none", edgecolor=color, lw=width, zorder=1)ax.add_patch(patch)
09

绘图

最后,将前面所有结果拼起来:算节点大小、画层间直线、画层内曲线边、画光晕和实体节点、保存图片,并导出聚类结果表。
# =========================================================# 绘图# =========================================================def draw_double_layer_clustered_network(nodes_df,edges_df,full_graph,pos,author_cluster_map,paper_cluster_map,cluster_theme_map,cluster_color_map,author_pos_xz,paper_pos_xz,output_png="double_layer_clustered_network_local.png",show_labels=False):fig, ax = plt.subplots(figsize=(18, 12), facecolor="#E9E9E9")ax.set_facecolor("#E9E9E9")node_theme = {}node_color = {}for_, row in nodes_df.iterrows():n = row["id"]lvl = int(row["lvl"])cid = author_cluster_map[n] if lvl == 2 else paper_cluster_map[n]node_theme[n] = cluster_theme_map[cid]node_color[n] = cluster_color_map[cid]authors = nodes_df[nodes_df["lvl"] == 2]["id"].tolist()papers = nodes_df[nodes_df["lvl"] == 1]["id"].tolist()G_auth = build_weighted_relation_graph(nodes_df, edges_df, "authorship")G_col = build_weighted_relation_graph(nodes_df, edges_df, "collaboration")G_cit = build_weighted_relation_graph(nodes_df, edges_df, "citation")id_to_paper_count = dict(zip(nodes_df["id"], nodes_df["paper_count"]))id_to_cit = dict(zip(nodes_df["id"], nodes_df["citation_received"]))author_deg = dict(G_col.degree(weight="weight"))paper_deg = dict(G_cit.degree(weight="weight"))author_sizes = {}for a in authors:paper_count = id_to_paper_count.get(a, 0)try:paper_count = 0 if paper_count == ""elsefloat(paper_count)except:paper_count = 0author_sizes[a] = 35 + 6.5 * paper_count + 15.5 * np.sqrt(max(1, author_deg.get(a, 1)))paper_sizes = {}for p in papers:cit = id_to_cit.get(p, 0)try:cit = 0 if cit == ""elsefloat(cit)except:cit = 0paper_sizes[p] = 25 + 2.5 * cit + 10.2 * np.sqrt(max(1, paper_deg.get(p, 1)))all_author_x = [author_pos_xz[n][0] for n in authors]all_author_z = [author_pos_xz[n][1] for n in authors]all_paper_x = [paper_pos_xz[n][0] for n in papers]all_paper_z = [paper_pos_xz[n][1] for n in papers]x_min = min(min(all_author_x), min(all_paper_x)) - 3.6x_max = max(max(all_author_x), max(all_paper_x)) + 3.6z_min = min(min(all_author_z), min(all_paper_z)) - 3.2z_max = max(max(all_author_z), max(all_paper_z)) + 3.2draw_plane(ax, x_min, x_max, z_min, z_max, BOTTOM_Y, "#F6F6F6", 0.82)draw_plane(ax, x_min, x_max, z_min, z_max, TOP_Y, "#DCE7F2", 0.40)left_top = project_point(x_min, TOP_Y, z_max, ALPHA_DEG, BETA_DEG)left_bottom = project_point(x_min, BOTTOM_Y, z_max, ALPHA_DEG, BETA_DEG)ax.text(left_top[0] - 0.45, left_top[1] + 0.18, "Author Collaboration Layer",fontsize=15, fontweight="bold", color="#355A7A")ax.text(left_bottom[0] - 0.45, left_bottom[1] - 0.35, "Paper Citation Layer",fontsize=15, fontweight="bold", color="#7A3E5E")auth_edgelist = list(G_auth.edges())auth_colors = []auth_widths = []foru, v in auth_edgelist:if node_theme[u] == node_theme[v]:auth_colors.append(to_rgba(node_color[u], 0.30))else:auth_colors.append((0.50, 0.50, 0.50, 0.05))auth_widths.append(0.07 + 0.12 * np.log1p(G_auth[u][v]["weight"]))nx.draw_networkx_edges(G_auth, pos,edgelist=auth_edgelist,edge_color=auth_colors,width=auth_widths,ax=ax)citation_edge_items = []foru, v in G_cit.edges():target_influence = 0if id_to_cit.get(v, "") == ""elsefloat(id_to_cit.get(v, 0))source_influence = 0if id_to_cit.get(u, "") == ""elsefloat(id_to_cit.get(u, 0))score = 1.0 + 0.16 * np.log1p(target_influence) + 0.07 * np.log1p(source_influence)if paper_cluster_map[u] == paper_cluster_map[v]:score += 0.22width = 0.18 + 0.42 * scorecitation_edge_items.append((u, v, score, width))citation_edge_items = select_top_edges(citation_edge_items, CITATION_KEEP_RATIO)draw_curved_intralayer_edges(ax=ax,edge_items=citation_edge_items,pos=pos,node_theme=node_theme,node_color=node_color,same_alpha=0.34,cross_alpha=0.08,bend_ratio=0.12)collab_edge_items = []foru, v in G_col.edges():score = float(G_col[u][v]["weight"])if author_cluster_map[u] == author_cluster_map[v]:score += 0.25width = 0.28 + 0.55 * np.log1p(G_col[u][v]["weight"])collab_edge_items.append((u, v, score, width))collab_edge_items = select_top_edges(collab_edge_items, COLLAB_KEEP_RATIO)draw_curved_intralayer_edges(ax=ax,edge_items=collab_edge_items,pos=pos,node_theme=node_theme,node_color=node_color,same_alpha=0.42,cross_alpha=0.10,bend_ratio=0.10)nx.draw_networkx_nodes(full_graph, pos,nodelist=papers,node_color=[node_color[n] for n in papers],node_size=[paper_sizes[n] * 1.95 for n in papers],alpha=0.08,linewidths=0,ax=ax)nx.draw_networkx_nodes(full_graph, pos,nodelist=authors,node_color=[node_color[n] for n in authors],node_size=[author_sizes[n] * 2.10 for n in authors],alpha=0.09,linewidths=0,ax=ax)nx.draw_networkx_nodes(full_graph, pos,nodelist=papers,node_color=[node_color[n] for n in papers],node_size=[paper_sizes[n] for n in papers],edgecolors="#2A2A2A",linewidths=0.24,alpha=0.97,node_shape="o",ax=ax)nx.draw_networkx_nodes(full_graph, pos,nodelist=authors,node_color=[node_color[n] for n in authors],node_size=[author_sizes[n] for n in authors],edgecolors="#111111",linewidths=0.42,alpha=0.98,node_shape="s",ax=ax)if show_labels:labels = {}fornin authors:if author_sizes[n] > 95:labels[n] = nfornin papers:if paper_sizes[n] > 55:labels[n] = nnx.draw_networkx_labels(full_graph, pos,labels=labels,font_size=6.8,ax=ax)ax.axis("off")plt.tight_layout()plt.savefig(output_png, dpi=300, bbox_inches="tight", facecolor="#E9E9E9")plt.show()
🌿 今日的分享就到这里啦~如果这些内容有为你带来帮助,欢迎轻点右下角的【👍赞】和【👀在看】,也欢迎分享给更多需要的人,感恩~
THE
END


数据和代码怎么获取?
点击关注后,后台回复关键词:
2026_map_017可直接获取完整的示例数据和代码
如有帮助,您的点赞、评论、转发是我持续创作的动力~

