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

D3力导向图点击节点缩放功能实现异常求助

解决D3点击节点缩放聚焦后视图重置的问题

问题根源

  1. 未使用D3 zoom官方API触发变换,直接修改transform属性会被后续zoom事件覆盖。
  2. zoomToNode函数中重复设置svg.attr("transform", event.transform),直接覆盖自定义变换,是视图重置的直接原因。
  3. 坐标计算未考虑SVG视口中心,点击后节点无法居中显示。
  4. 变量命名混淆:原代码中svg实际指向内部<g>元素,外层SVG未被正确引用,导致zoom事件绑定和变换目标错误。

修正方案

以下是修改后的完整代码,关键修改点已标注:

<!DOCTYPE html>
<html lang="de">
<head>
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
    <meta charset="utf-8">
    <script src="https://code.jquery.com/jquery-3.6.3.js"></script>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
    <style>
        .link {
            stroke: #000;
            stroke-width: 1.5px;
        }
        .nodes {
            fill: whitesmoke;
            cursor: pointer;
        }
        .buttons {
            margin: 0 1em 0 0;
        }
    </style>
</head>
<body>
<script>
    var width = window.innerWidth,
        height = window.innerHeight;

    // 分离外层SVG和内部缩放组,避免命名混淆
    var outerSvg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height);

    var zoomGroup = outerSvg.append("g"); // 所有需缩放的元素放入此组

    // 绑定zoom事件到外层SVG,变换作用于内部组
    outerSvg.call(d3.zoom().on("zoom", function (event) {
        zoomGroup.attr("transform", event.transform);
    }))

    ////////////////////////
    // 力导向图布局

    var data = {
        "nodes": [{"id": "A"},{"id": "B"},{"id": "C"},{"id": "D"},{"id": "E"},{"id": "F"},{"id": "G"}],
        "links": [{"source": "A","target": "B"},{"source": "B","target": "C"},{"source": "C","target": "D"},{"source": "D","target": "E"},{"source": "E","target": "F"},{"source": "F","target": "G"}]
    }

    var simulation = d3.forceSimulation()
        .force("size", d3.forceCenter(width / 2, height / 2))
        .force("charge", d3.forceManyBody().strength(-5000))
        .force("link", d3.forceLink().id(function (d) {
            return d.id
        }).distance(250))

    linksContainer = zoomGroup.append("g").attr("class", "linkscontainer")
    nodesContainer = zoomGroup.append("g").attr("class", "nodesContainer")

    links = linksContainer.selectAll(".linkPath")
        .data(data.links)
        .enter()
        .append("path")
        .attr("class", "linkPath")
        .attr("stroke", "red")
        .attr("fill", "transparent")
        .attr("stroke-width", 3)

    nodes = nodesContainer.selectAll(".nodes")
        .data(data.nodes, function (d) {
            return d.id;
        })
        .enter()
        .append("g")
        .attr("class", "nodes")
        .call(d3.drag()
            .on("start", dragStarted)
            .on("drag", dragged)
            .on("end", dragEnded)
        )
        .on("click", function(event, d) { // 修正D3 v7事件参数顺序
            zoomToNode(d.x, d.y);
        })

    nodes.selectAll("circle")
        .data(d => [d])
        .enter()
        .append("circle")
        .attr("class", "circle")
        .style("stroke", "blue")
        .attr("r", 40)
        .style("fill", "whitesmoke") // 补充填充色,让节点可见

    simulation
        .nodes(data.nodes)
        .on("tick", tick)

    simulation
        .force("link")
        .links(data.links)

    function tick() {
        links.attr("d", function (d) {
            var dx = (d.target.x - d.source.x),
                dy = (d.target.y - d.source.y),
                dr = Math.sqrt(dx * dx + dy * dy)
            return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
        })
        nodes.attr("transform", d => `translate(${d.x}, ${d.y})`);
    }

    function dragStarted(event, d) {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;
    }

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

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

    function zoomToNode(nodeX, nodeY) {
        // 构建目标变换:让节点居中,缩放倍数设为2
        var targetTransform = d3.zoomIdentity
            .translate(width / 2 - nodeX * 2, height / 2 - nodeY * 2)
            .scale(2);

        // 使用官方API触发缩放动画,确保状态同步
        outerSvg.transition()
            .duration(750)
            .call(d3.zoom().transform, targetTransform);
    }
</script>
</body>
</html>

关键修改说明

  • 分离SVG与缩放组:将外层SVG和承载可视化元素的内部<g>分开,确保zoom变换正确作用于目标内容。
  • 使用官方API触发缩放:用d3.zoom().transform()配合过渡动画,替代直接修改属性,避免后续事件重置视图。
  • 修正坐标计算:通过d3.zoomIdentity构建变换,让节点缩放后居中显示在视口。
  • 修正事件参数:D3 v7中click事件第一个参数为event,第二个为数据d,直接通过d.x/d.y获取坐标,避免DOM直接操作。
  • 移除错误重复设置:删除zoomToNode中覆盖变换的语句。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 03:07:16