You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

D3力导向图SVG过小且节点无排斥效果问题求助

D3力导向图节点拥挤、力配置无效问题解决建议

问题描述

我正尝试让D3力导向图可视化效果展开,避免图形过于紧凑。在代码中创建了仿真(simulation)和SVG,但设置.force()相关选项后SVG无任何变化。目前图形节点拥挤,线条还会穿过无关节点,作为D3新手,恳请提供解决建议。

代码实现

组件代码

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Service } from '../../services/service';
import * as d3 from 'd3';
import { Node } from 'src/app/shared/d3/node.model';
import { Link } from 'src/app/shared/d3/link.model';

@Component({
  selector: 'app-graph-visualization',
  templateUrl: './graph-visualization.component.html',
  styleUrls: [],
  standalone: true,
  imports: [
    CommonModule
  ],
})
export class GraphVisualizationComponent {

  // set the dimensions and margins of the graph
  svg: any;
  public width = 650;
  public height = 1000;

  public nodes: Node[] = [];
  public links: Link[] = [];
  public simulation: any;

  constructor(
    public service: Service
  ) {}

  ngOnInit(): void {
    this.getData();
  }

  getData() {
    this.service.getData()
      .subscribe({
        next: (response) => {
          if (response.data) {
            // receives array of nodes and sets to device connections
            const node_data = response.data.edges.map(d => d.node);

            this.formatData(node_data);
            this.generate();

            //this.generateSimulation();
          } else if (response.errors) {
            this.handleError(response.errors[0].message);
          }
        }
      });
  }

  handleError(errMessage: string) {
    this.error = errMessage.split(",")[0];
    this.color = 'error';
  }

  formatData(data: any) {
    this.generateNodes(data);
  }

  generateNodes(data: any) {
    for (let datum of data) {
      const sourceNode = new Node("uri", "name");
      this.nodes.push(sourceNode);
      for (let connection of datum.connectsToGraph) {
        const targetNode = new Node("uri", "name");
        this.generateLinks(sourceNode, targetNode,"1");
        this.nodes.push(targetNode);
      }
    }
  }

  generateLinks(source: Node, target: Node, conversationCount: string) {
    this.links.push(new Link(source, target, conversationCount));
  }

  private generate() {
    this.svg = d3.select("div#diagram")
      .append("svg")
      .attr("width", this.width)
      .attr("height", this.height)
      .append("g")
      .attr("transform",
            "translate("+this.width/2+","+this.height/2+")");

    this.simulation = d3.forceSimulation(this.nodes)
      // these aren't doing anything for some reason...
      // .force("link", d3.forceLink(this.links).distance(100))
      // .force("charge", d3.forceManyBody().strength(-5))
      // .force("collide", d3.forceCollide().radius(100))
      .stop(); // not using tick because I don't need the animations

      this.createSvg();
  }


  private createSvg(): void {
    this.svg.append("g")
      .attr("stroke", "#000")
      .selectAll("line")
      .data(this.links)
      .enter().append("line")
      .attr("stroke-width", function(d: Link) { return d.conversationCount; })
      .attr("x1", function(d: Link) { return d.source.x; })
      .attr("y1", function(d: Link) { return d.source.y; })
      .attr("x2", function(d: Link) { return d.target.x; })
      .attr("y2", function(d: Link) { return d.target.y; });

    this.svg.append("g")
      .attr("stroke", "#fff")
      .attr("stroke-width", 1.5)
      .selectAll("circle")
      .data(this.nodes)
      .enter().append("circle")
      .attr("cx", function(d: Node) { return d.x; })
      .attr("cy", function(d: Node) { return d.y; })
      .attr("r", 30);
  }
}

Link模型

import { Node } from './node.model';

export class Link implements d3.SimulationLinkDatum<Node> {
    // Optional - defining optional implementation properties - required for relevant typing assistance
    index?: number;

    source: Node;
    target: Node;
    conversationCount: string;

    constructor(source: Node, target: Node, conversationCount: string) {
        this.source = source;
        this.target = target;
        this.conversationCount = conversationCount;
    }
}

Node模型

export class Node implements d3.SimulationNodeDatum {
    // Optional - defining optional implementation properties - required for relevant typing assistance
    index?: number;
    x?: number;
    y?: number;
    vx?: number;
    vy?: number;
    fx?: number | null;
    fy?: number | null;
    
    id: string;
    name: string;
    
    constructor(uri: string, name: string) {
        this.id = uri;
        this.name = name;
    }
}

数据与当前效果

  • 数据示例:数据示例
  • 当前可视化效果(节点半径25):节点半径25时的图形
  • 节点半径4时的效果:
    注意:此时线条会穿过无关节点,这是我不希望出现的情况
    节点半径4时的图形

解决建议

1. 核心问题分析

你的代码里有两个关键问题导致力配置无效、节点拥挤:

  • 仿真直接调用了.stop(),没有运行任何迭代,节点的x/y属性未被力算法计算,保持初始值(默认0),所以所有节点堆在一起。
  • 力配置被注释,即使启用了也没有合理调整参数。

2. 具体修改步骤

(1)启用并配置力参数

取消注释力配置代码,并调整参数以增强布局效果:

this.simulation = d3.forceSimulation(this.nodes)
  .force("link", d3.forceLink(this.links).distance(100)) // 让相连节点保持100px距离
  .force("charge", d3.forceManyBody().strength(-300)) // 增强节点间排斥力(负值越大排斥越强)
  .force("collide", d3.forceCollide().radius(35)) // 碰撞力,防止节点重叠(半径设为节点半径+缓冲)
  .force("center", d3.forceCenter(0, 0)) // 保持图形在SVG中心
  .tick(300); // 运行300次迭代完成布局,无需动画

(2)修正节点ID问题

当前代码中创建Node时传入的是固定字符串"uri",导致所有节点ID相同,D3无法区分节点,力导向算法会失效。需要传入实际的唯一ID:

// 在generateNodes方法中,替换固定字符串为数据中的实际值
const sourceNode = new Node(datum.uri, datum.name); // 假设数据里有uri和name字段
const targetNode = new Node(connection.uri, connection.name);

(3)避免重复节点

当前generateNodes方法会为每个连接创建新节点,可能导致重复。可以维护一个节点映射表,先检查节点是否已存在:

private nodeMap = new Map<string, Node>();

generateNodes(data: any) {
  for (let datum of data) {
    let sourceNode = this.nodeMap.get(datum.uri);
    if (!sourceNode) {
      sourceNode = new Node(datum.uri, datum.name);
      this.nodeMap.set(datum.uri, sourceNode);
      this.nodes.push(sourceNode);
    }
    for (let connection of datum.connectsToGraph) {
      let targetNode = this.nodeMap.get(connection.uri);
      if (!targetNode) {
        targetNode = new Node(connection.uri, connection.name);
        this.nodeMap.set(connection.uri, targetNode);
        this.nodes.push(targetNode);
      }
      this.generateLinks(sourceNode, targetNode, "1");
    }
  }
}

3. 验证效果

修改后,仿真会运行足够迭代次数计算节点位置,排斥力和碰撞力会让节点分散开,连线力保证相连节点保持合理距离,避免线条穿过无关节点。

内容的提问来源于stack exchange,提问作者Christina Leuci

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 00:08:13