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

基于D3.js的交互式图形:元素添加与标记问题求助

基于D3.js的交互式节点图形问题修复

需求说明

初始展示3个可点击SVG节点,点击其中一个后:

  • 隐藏未点击的两个节点
  • 保留点击的节点并新增一个SVG节点
    后续仅允许点击最新添加的SVG节点来新增节点,旧节点不可点击。

当前核心问题

  • 无法从nodes数组中移除未点击的节点对象(仅能在页面移除对应SVG元素)
  • 交互第二阶段出现多余SVG,多次点击后节点数量远超预期,点击标记逻辑存在缺陷
  • 图形节点超出指定DOM元素范围

问题代码

const nodes = []; // array to contain created and visible node obj

let clicked = 'false'; // flag clicked SVG

const graphSpace = d3.select('#graph');
const width = window.innerWidth;
const height = window.innerHeight;
const svg = graphSpace.append('svg')
    .attr('width', width)
    .attr('height', height);

function init() {
    let initialNode;
    //create 3 objects in array and render svg elements in html
    for(let i = 0; i < 3; i++) {
        initialNode = new NodeObject(nodeData[i].name, nodeData[i].description, nodeData[i].symbol, nodeData[i].nodeArray, Math.random() * width, Math.random() * height);
        nodes.push(initialNode);
        createInitialNode();
    }
}

function createInitialNode() {
    // render SVG elements
    let rand = Math.floor(Math.random() * 5) + 1;

    svg.selectAll('.node')
        .data(nodes) // asocjacja kolekcji elementów z danymi
        .enter()
        .append('image')
        .attr('class', 'node')
        .attr('x', d => d.x)
        .attr('y', d => d.y)
        .attr('width', 220)
        .attr('height', 220)
        .attr('xlink:href', 'svg/s' + rand + '.svg')
}

function firstClick() {
    let notClickedNodes = [];
    // after first click leave clicked SVG, hide 2 not-clicked SVGs and create new SVG
    svg.selectAll('.node').on('click', (event, d) => {
        if(clicked === 'false') {
            d3.select(event.currentTarget).attr('class', 'clicked') // change class of the clicked element to 'clicked'
            d3.selectAll('.node') // select remaining SVGs
                .attr('class', 'notClickedInitial'); // add class to not clicked SVGs
            d3.selectAll('.notClickedInitial').remove();
            // remove those nodes from the nodes array - how?
            clicked = 'true';
        }
        addNode(d);
    });

}

init();
firstClick();

function addNode(d) { //
    nodes.push({x: Math.random() * width, y: Math.random() * height});

    let rand = Math.floor(Math.random() * 5) + 1;

    const node = svg.selectAll('.clicked')
        .data(nodes)
        .enter()
        .append('image')
        .attr('class', 'notClicked')
        .attr('x', d => d.x)
        .attr('y', d => d.y)
        .attr('width', 220)
        .attr('height', 220)
        .attr('xlink:href', 'svg/s' + rand + '.svg')

    let count = 0;

    svg.selectAll('.notClicked').on('click', (event, d) => {
        if(count === 0) {
            d3.select(event.currentTarget).attr('class', 'clicked')
            addNode(d);
            console.log(nodes);
            count += 1;
        }
    });
}

修复方案及代码

针对三个问题逐一修复,以下是修正后的完整代码:

const nodes = [];
// 追踪当前可点击的节点元素
let currentClickableNode = null;

const graphSpace = d3.select('#graph');
// 使用容器实际宽高,而非窗口宽高
const containerRect = graphSpace.node().getBoundingClientRect();
const width = containerRect.width;
const height = containerRect.height;
const nodeSize = 220; // 统一节点尺寸

const svg = graphSpace.append('svg')
    .attr('width', width)
    .attr('height', height);

// 生成合法位置,确保节点完全在容器内
function getValidPosition() {
    return {
        x: Math.random() * (width - nodeSize),
        y: Math.random() * (height - nodeSize)
    };
}

function init() {
    // 创建3个初始节点
    for(let i = 0; i < 3; i++) {
        const pos = getValidPosition();
        const initialNode = new NodeObject(
            nodeData[i].name, 
            nodeData[i].description, 
            nodeData[i].symbol, 
            nodeData[i].nodeArray, 
            pos.x, 
            pos.y
        );
        nodes.push(initialNode);
    }
    // 统一渲染初始节点
    renderNodes(nodes, 'node');
    // 绑定初始节点点击事件
    bindInitialClick();
}

// 通用节点渲染函数
function renderNodes(data, className) {
    svg.selectAll(`.${className}`)
        .data(data)
        .enter()
        .append('image')
        .attr('class', className)
        .attr('x', d => d.x)
        .attr('y', d => d.y)
        .attr('width', nodeSize)
        .attr('height', nodeSize)
        .attr('href', () => `svg/s${Math.floor(Math.random() * 5) + 1}.svg`); // 改用标准href属性
}

function bindInitialClick() {
    svg.selectAll('.node').on('click', (event, clickedNode) => {
        // 移除未点击的节点DOM
        svg.selectAll('.node').filter((d) => d !== clickedNode).remove();
        // 更新nodes数组,仅保留点击的节点
        nodes.length = 0;
        nodes.push(clickedNode);
        // 标记当前可点击节点
        currentClickableNode = d3.select(event.currentTarget).attr('class', 'clickable');
        // 新增第一个后续节点
        addNewNode();
    });
}

function addNewNode() {
    const newNodePos = getValidPosition();
    // 创建新的NodeObject实例,保持数据结构一致
    const newNode = new NodeObject(
        'New Node', 
        'Dynamic node', 
        'symbol', 
        [], 
        newNodePos.x, 
        newNodePos.y
    );
    nodes.push(newNode);
    // 渲染新节点
    renderNodes([newNode], 'clickable');
    // 移除旧节点的点击事件
    currentClickableNode.on('click', null);
    // 更新当前可点击节点为新节点,并绑定点击事件
    currentClickableNode = svg.selectAll('.clickable').filter((d) => d === newNode);
    currentClickableNode.on('click', () => {
        addNewNode();
    });
}

// 初始化执行
init();

修复关键点说明

  1. 数据与DOM同步:点击初始节点后,直接重置nodes数组并保留点击的节点,确保数据和页面元素一一对应。
  2. 精准事件绑定:每次新增节点后,仅给最新节点绑定点击事件,同时移除旧节点的事件监听,避免重复触发。
  3. 边界控制:通过getValidPosition函数计算节点位置,确保节点不会超出容器范围。
  4. 统一数据结构:新增节点时创建NodeObject实例,保持数组内数据类型一致,避免后续数据操作出错。
  5. 标准属性兼容:改用D3 v6+推荐的href属性替代xlink:href。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 09:52:07