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

表单输入值变更时D3图表无法更新的问题求助

表单输入值变更时D3图表无法更新的问题求助

大家好,最近我在开发一个基于D3.js的元素关系可视化图表,遇到了个头疼的问题:当页面上的表单筛选条件(比如可选的道德类型、元素种类、要显示的关系类型)变化时,图表完全没响应,节点和链接还是停留在初始状态,根本不跟着筛选条件更新。

先简单说下这个图表的功能:它用来展示7种元素(electric、fire、leaf等)和三种道德类型(order、chaos、equipoise)之间的两种关系——powerup(增益)和counters(克制)。我写了数据生成函数来创建节点和链接数据,然后封装了一个D3Chart类来初始化和管理图表。

我已经写了一个updateFilters方法,打算在表单输入变化时调用它,传入新的筛选条件后触发图表更新,但不管怎么调,图表就是纹丝不动。我检查过过滤后的节点和链接数据是正确的,但DOM元素就是不更新,模拟也没正确运行。

下面是我的完整代码,麻烦各位大佬帮我看看哪里出问题了:

数据生成部分

const ELEMENTS = ['electric', 'fire', 'leaf', 'metal', 'rock', 'water', 'wind'];
const MORAL_TYPES = ['order'];//'chaos', 'equipoise'

const createNodes = () => {
  const nodes = [];
  MORAL_TYPES.forEach(moralType => {
    ELEMENTS.forEach(element => {
      nodes.push({ id: `${element}-${moralType}`, element, moralType });
    });
  });
  return nodes;
};

const createLinks = () => {
  const links = [];
  const baseLinks = [
    { source: 'Fire', target: 'Wind', type: 'powerup' },
    { source: 'Wind', target: 'Water', type: 'powerup' },
    { source: 'Water', target: 'Leaf', type: 'powerup' },
    { source: 'Leaf', target: 'Rock', type: 'powerup' },
    { source: 'Rock', target: 'Metal', type: 'powerup' },
    { source: 'Metal', target: 'Electric', type: 'powerup' },
    { source: 'Electric', target: 'Fire', type: 'powerup' },
    { source: 'Electric', target: 'Water', type: 'counters' },
    { source: 'Electric', target: 'Rock', type: 'counters' },
    { source: 'Fire', target: 'Metal', type: 'counters' },
    { source: 'Fire', target: 'Leaf', type: 'counters' },
    { source: 'Leaf', target: 'Electric', type: 'counters' },
    { source: 'Leaf', target: 'Wind', type: 'counters' },
    { source: 'Metal', target: 'Wind', type: 'counters' },
    { source: 'Metal', target: 'Leaf', type: 'counters' },
    { source: 'Rock', target: 'Fire', type: 'counters' },
    { source: 'Rock', target: 'Water', type: 'counters' },
    { source: 'Water', target: 'Metal', type: 'counters' },
    { source: 'Water', target: 'Fire', type: 'counters' },
    { source: 'Wind', target: 'Electric', type: 'counters' },
    { source: 'Wind', target: 'Rock', type: 'counters' },
  ];

  MORAL_TYPES.forEach(moralType => {
    baseLinks.forEach(baseLink => {
      links.push({
        source: `${baseLink.source.toLowerCase()}-${moralType}`,
        target: `${baseLink.target.toLowerCase()}-${moralType}`,
        type: baseLink.type,
      });
    });
  });

  ELEMENTS.forEach(element => {
    links.push({ source: `${element}-chaos`, target: `${element}-order`, type: 'counters' });
    links.push({ source: `${element}-equipoise`, target: `${element}-chaos`, type: 'counters' });
    links.push({ source: `${element}-order`, target: `${element}-equipoise`, type: 'counters' });
  });

  return links;
};

const bubblewockNodesData = createNodes();
const bubblewockLinksData = createLinks();

D3图表实现类

// --- D3 CHART IMPLEMENTATION ---
class D3Chart {
  constructor(containerId, initialFilters) {
    this.chartContainer = document.getElementById(containerId);
    this.svg = null;
    this.simulation = null;
    this.width = 0;
    this.height = 0;
    this.radius = 22;

    // Initial filters (mimicking Angular @Input)
    this.moralTypes = initialFilters.moralTypes || MORAL_TYPES;
    this.elements = initialFilters.elements || ELEMENTS;
    this.relationships = initialFilters.relationships || ['counters', 'powerup'];

    this.initializeChart();
    // Add a resize listener
    window.addEventListener('resize', () => this.handleResize());
  }

  initializeChart() {
    this.updateDimensions();
    this.createSvg();
    this.initializeSimulation();
    this.updateChart();
  }

  updateDimensions() {
    this.width = this.chartContainer.clientWidth;
    this.height = this.chartContainer.clientHeight;
  }

  handleResize() {
    this.updateDimensions();
    if (this.svg) {
      this.svg.attr('width', '100%').attr('height', '100%');
    }
    if (this.simulation) {
      this.simulation.force('center', d3.forceCenter(this.width / 2, this.height / 2));
      this.simulation.force('radialOrder', d3.forceRadial(this.height / 2.5, this.width / 2, this.height / 2).strength((d) => d.moralType === 'order' ? 0.7 : 0));
      this.simulation.force('radialEquipoise', d3.forceRadial(this.height / 4, this.width / 2, this.height / 2).strength((d) => d.moralType === 'equipoise' ? 0.6 : 0));
      this.simulation.force('radialChaos', d3.forceRadial(this.height / 12, this.width / 2, this.height / 2).strength((d) => d.moralType === 'chaos' ? 0.5 : 0));
      this.simulation.alpha(0.3).restart(); // Reheat simulation to adapt to new dimensions
    }
    this.ticked(); // Re-position elements immediately
  }

  createSvg() {
    // Clear existing SVG if any
    d3.select(this.chartContainer).select('svg').remove();
    this.svg = d3.select(this.chartContainer).append('svg')
      .attr('width', '100%')
      .attr('height', '100%')
      .attr('viewBox', `0 0 ${this.width} ${this.height}`) // Add viewBox for responsiveness
      .attr('preserveAspectRatio', 'xMidYMid meet'); // Maintain aspect ratio

    this.svg.append('g').attr('class', 'links');
    this.svg.append('g').attr('class', 'nodes');

    // Define arrow markers
    this.svg.append('defs').selectAll('marker')
      .data(['counters', 'powerup'])
      .join('marker')
      .attr('id', (d) => `arrow-${d}`)
      .attr('viewBox', '0 -5 10 10')
      .attr('refX', 28) // Adjust refX based on node radius for arrow to point to circle edge
      .attr('refY', 0)
      .attr('markerWidth', 6)
      .attr('markerHeight', 6)
      .attr('orient', 'auto')
      .append('path')
      .attr('fill', (d) => d === 'counters' ? '#E74C3C' : '#2ECC71')
      .attr('d', 'M0,-5L10,0L0,5');
  }

  initializeSimulation() {
    this.simulation = d3.forceSimulation()
      .force('link', d3.forceLink().id((d) => d.id).strength(0.1).distance(100))
      .force('charge', d3.forceManyBody().strength(-300))
      .force('center', d3.forceCenter(this.width / 2, this.height / 2))
      .force('radialOrder', d3.forceRadial(this.height / 2.5, this.width / 2, this.height / 2).strength((d) => d.moralType === 'order' ? 0.7 : 0))
      .force('radialEquipoise', d3.forceRadial(this.height / 4, this.width / 2, this.height / 2).strength((d) => d.moralType === 'equipoise' ? 0.6 : 0))
      .force('radialChaos', d3.forceRadial(this.height / 12, this.width / 2, this.height / 2).strength((d) => d.moralType === 'chaos' ? 0.5 : 0));
  }

  updateChart() {
    // 这里我尝试写更新逻辑,但不知道怎么正确过滤数据和绑定DOM
    const filteredNodes = bubblewockNodesData.filter(node => this.moralTypes.includes(node.moralType) && this.elements.includes(node.element));
    const filteredLinks = bubblewockLinksData.filter(link => this.relationships.includes(link.type) && filteredNodes.some(n => n.id === link.source.id || n.id === link.source) && filteredNodes.some(n => n.id === link.target.id || n.id === link.target));

    // 处理链接
    const links = this.svg.select('.links').selectAll('line')
      .data(filteredLinks);

    links.exit().remove();

    links.enter().append('line')
      .attr('stroke', d => d.type === 'counters' ? '#E74C3C' : '#2ECC71')
      .attr('stroke-width', 2)
      .attr('marker-end', d => `url(#arrow-${d.type})`);

    // 处理节点
    const nodes = this.svg.select('.nodes').selectAll('circle')
      .data(filteredNodes);

    nodes.exit().remove();

    const nodeEnter = nodes.enter().append('circle')
      .attr('r', this.radius)
      .attr('fill', d => {
        // 这里可以根据元素设置颜色,暂时用默认
        return '#3498DB';
      })
      .call(d3.drag()
        .on('start', (event, d) => this.dragStarted(event, d))
        .on('drag', (event, d) => this.dragged(event, d))
        .on('end', (event, d) => this.dragEnded(event, d)));

    // 添加节点文本
    const text = this.svg.select('.nodes').selectAll('text')
      .data(filteredNodes);

    text.exit().remove();

    text.enter().append('text')
      .attr('text-anchor', 'middle')
      .attr('dy', '.35em')
      .text(d => d.element);

    // 更新模拟数据
    this.simulation.nodes(filteredNodes).on('tick', () => this.ticked());
    this.simulation.force('link').links(filteredLinks);
    this.simulation.alpha(0.3).restart();
  }

  ticked() {
    this.svg.select('.links').selectAll('line')
      .attr('x1', d => d.source.x)
      .attr('y1', d => d.source.y)
      .attr('x2', d => d.target.x)
      .attr('y2', d => d.target.y);

    this.svg.select('.nodes').selectAll('circle')
      .attr('cx', d => d.x)
      .attr('cy', d => d.y);

    this.svg.select('.nodes').selectAll('text')
      .attr('x', d => d.x)
      .attr('y', d => d.y);
  }

  // 拖拽事件处理
  dragStarted(event, d) {
    if (!event.active) this.simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }

  dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }

  dragEnded(event, d) {
    if (!event.active) this.simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }

  // 我想在表单变化时调用这个方法来更新筛选条件并刷新图表
  updateFilters(newFilters) {
    this.moralTypes = newFilters.moralTypes || this.moralTypes;
    this.elements = newFilters.elements || this.elements;
    this.relationships = newFilters.relationships || this.relationships;
    this.updateChart();
  }
}

我怀疑问题出在updateChart方法里的数据绑定或者模拟更新的部分,但试了好几种调整都没用。比如,是不是应该用join()方法而分开处理enter/exit?或者ticked方法里的选择器有问题?

有没有大佬能帮我诊断一下,告诉我怎么正确实现筛选条件变化后的图表更新?万分感谢!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 09:18:04