表单输入值变更时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
相关产品推荐
相关产品推荐

