D3 根据节点大小重新计算箭头位置

ICo*_*ded 5 javascript d3.js

对于给定的 D3 强制图,我面临两个视觉问题。

\n
    \n
  1. 每个链接的最后一英寸仍然可见。箭头应该正好在节点边缘结束,情况就是如此,但下面的红色链接是可见的。我可以调整链接笔划宽度或箭头大小,但实际上不想更改这些属性。
  2. \n
\n

在此输入图像描述

\n
    \n
  1. 节点的半径在 mouseenter 事件发生时增加。如果节点大小增加,箭头就会错位并且不再终止于节点边缘。我认为我需要调整刻度函数,但没有找到合适的起点。
  2. \n
\n

在此输入图像描述

\n

我感谢任何让我更接近结果的链接、提示、帮助。

\n

\r\n
\r\n
        var width = window.innerWidth,\n            height = window.innerHeight;\n\n        var svg = d3.select("body").append("svg")\n            .attr("width", width)\n            .attr("height", height)\n            .call(d3.zoom().on("zoom", function(event) {\n                svg.attr("transform", event.transform)\n            }))\n            .append("g")\n\n        ////////////////////////\n        // outer force layout\n\n        var data = {\n            "nodes":[\n                { "id": "A" }, \n                { "id": "B" },\n                { "id": "C" },\n            ],\n            "links": [\n                { "source": "A", "target": "B"},\n                { "source": "B", "target": "C"},\n                { "source": "C", "target": "A"}\n            ]\n        };\n\n        var simulation = d3.forceSimulation()\n            .force("size", d3.forceCenter(width / 2, height / 2))\n            .force("charge", d3.forceManyBody().strength(-1000))\n            .force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))\n\n        svg.append("defs").append("marker")\n            .attr(\'id\', \'arrowhead\')\n            .attr(\'viewBox\', \'-0 -5 10 10\')\n            .attr("refX", 23.5)\n            .attr("refY", 0)\n            .attr(\'orient\', \'auto\')\n            .attr(\'markerWidth\', 10)\n            .attr(\'markerHeight\', 10)\n            .attr("orient", "auto")\n            .append(\'svg:path\')\n            .attr(\'d\', \'M 0,-5 L 10 ,0 L 0,5\')\n            .attr(\'fill\', \'#999\')\n            .style(\'stroke\', \'none\');\n\n        var links = svg.selectAll(".links")\n            .data(data.links)\n            .join("line")\n            .attr("stroke", "red")\n            .attr("stroke-width", 3)\n            .attr("marker-end", "url(#arrowhead)")\n   \n        var nodes = svg.selectAll("g.outer")\n            .data(data.nodes, function (d) { return d.id; })\n            .enter()\n            .append("g")\n            .attr("class", "outer")\n            .attr("id", function (d) { return d.id; })\n            .call(d3.drag()\n                .on("start", dragStarted)\n                .on("drag", dragged)\n                .on("end", dragEnded)\n            );\n\n        nodes\n            .append("circle")\n            .style("fill", "lightgrey")\n            .style("stroke", "blue")\n            .attr("r", 40)\n            .on("mouseenter", function() {\n                d3.select(this)\n                    .transition()\n                        .duration(250)\n                            .attr("r", 40 * 1.3)\n                            .attr("fill", "blue")\n            })\n            .on("mouseleave", function() {\n                d3.select(this)\n                    .transition()\n                        .duration(250)\n                            .attr("r", 40)\n                            .attr("fill", "lightgrey")\n            })            \n\n        simulation\n            .nodes(data.nodes)\n            .on("tick", tick)\n\n        simulation\n            .force("link")\n            .links(data.links)\n\n\n            \n        function tick() {\n            links\n                .attr("x1", function (d) { return d.source.x; })\n                .attr("y1", function (d) { return d.source.y; })\n                .attr("x2", function (d) { return d.target.x; })\n                .attr("y2", function (d) { return d.target.y; });\n                \n\n            nodes\n                .attr("transform", d => `translate(${d.x}, ${d.y})`);\n        }\n\n        function dragStarted(event, d) {\n            if (!event.active) simulation.alphaTarget(0.3).restart();\n            d.fx = d.x;\n            d.fy = d.y;\n        }\n\n        function dragged(event, d) {\n            d.fx = event.x;\n            d.fy = event.y;\n        }\n\n        function dragEnded(event, d) {\n            if (!event.active) simulation.alphaTarget(0);\n            d.fx = null;\n            d.fy = null;\n        }
Run Code Online (Sandbox Code Playgroud)\r\n
    body {\n        background: whitesmoke,\xc2\xb4;\n        overflow: hidden;\n        margin: 0px;\n    }
Run Code Online (Sandbox Code Playgroud)\r\n
<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset="utf-8">\n    <meta name="viewport" content="width=device-width, initial-scale=1">\n\n    <title>D3v7</title>\n    <!-- d3.js framework -->\n    <script src="https://d3js.org/d3.v7.js"></script>\n</head>\n\n\n\n<body>\n\n</body>\n\n</html>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

sun*_*eol 2

解决方案

\n

目前我能想到两种解决方案。

\n
    \n
  1. 计算每个<path(.links)/>向量并减去扩展圆半径
  2. \n
  3. 给每个偏移量<marker />
  4. \n
\n

我个人选择了第二种解决方案;它需要更少的计算并且更容易理解。

\n

1.给每个链接自定义箭头

\n

为每个链接添加单独的箭头,因为 svg 路径似乎不支持单独的偏移量。

\n
            // add custom arrowheads for each link\n            // to avoid exhaustive computations\n            svg.append("defs").selectAll(\'marker\')\n                .data(data.links)\n                .join("marker")\n                .attr(\'id\', d => `arrowhead-target-${d.target}`)\n// ...\n        var links = svg.selectAll(".links")\n            .data(data.links)\n            .join("line")\n            .attr("stroke", "red")\n            .attr("stroke-width", 3)\n            .attr("marker-end", d => `url(#arrowhead-target-${d.target})`)\n            // add source and target so that it can be traced back\n
Run Code Online (Sandbox Code Playgroud)\n

2. 使用 mouseevent 扩展、减去每个标记的偏移量

\n

的偏移量<marker />应用于.refX, .refY属性,如MDN 文档中所述

\n
        nodes\n            .append("circle")\n            .style("fill", "lightgrey")\n            .style("stroke", "blue")\n            .attr("r", 40)\n            .on("mouseenter", function(ev, d) {\n               // trace back to custom arrowhead with given id\n                const arrowHead = d3.select(`#arrowhead-target-${d.id}`)\n               arrowHead.transition().duration(250).attr(\'refX\', 40)\n                d3.select(this)\n                    .transition()\n                        .duration(250)\n                            .attr("r", 40 * 1.3)\n                            .attr("fill", "blue")\n            })\n            .on("mouseleave", function(ev, d) {\n                const arrowHead = d3.select(`#arrowhead-target-${d.id}`)\n               arrowHead.transition().duration(250).attr(\'refX\', 23.5)\n                d3.select(this)\n                    .transition()\n                        .duration(250)\n                            .attr("r", 40)\n                            .attr("fill", "lightgrey")\n            })            \n
Run Code Online (Sandbox Code Playgroud)\n

尝试运行下面的代码。扩展和减法的大小被故意夸大,以便您可以看到差异。

\n

\r\n
\r\n
var width = window.innerWidth,\n            height = window.innerHeight;\n\n        var svg = d3.select("body").append("svg")\n            .attr("width", width)\n            .attr("height", height)\n            .call(d3.zoom().on("zoom", function(event) {\n                svg.attr("transform", event.transform)\n            }))\n            .append("g")\n\n        ////////////////////////\n        // outer force layout\n\n        var data = {\n            "nodes":[\n                { "id": "A" }, \n                { "id": "B" },\n                { "id": "C" },\n            ],\n            "links": [\n                { "source": "A", "target": "B"},\n                { "source": "B", "target": "C"},\n                { "source": "C", "target": "A"}\n            ]\n        };\n\n        var simulation = d3.forceSimulation()\n            .force("size", d3.forceCenter(width / 2, height / 2))\n            .force("charge", d3.forceManyBody().strength(-1000))\n            .force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))\n        // add custom arrowheads for each link\n        // to avoid exhaustive computations\n        svg.append("defs").selectAll(\'marker\')\n            .data(data.links)\n            .join("marker")\n            .attr(\'id\', d => `arrowhead-target-${d.target}`)\n            .attr(\'viewBox\', \'-0 -5 10 10\')\n            .attr("refX", 23.5)\n            .attr("refY", 0)\n            .attr(\'orient\', \'auto\')\n            .attr(\'markerWidth\', 10)\n            .attr(\'markerHeight\', 10)\n            .attr("orient", "auto")\n            .append(\'svg:path\')\n            .attr(\'d\', \'M 0,-5 L 10 ,0 L 0,5\')\n            .attr(\'fill\', \'#999\')\n            .style(\'stroke\', \'none\');\n\n        var links = svg.selectAll(".links")\n            .data(data.links)\n            .join("line")\n            .attr("stroke", "red")\n            .attr("stroke-width", 3)\n            .attr("marker-end", d => `url(#arrowhead-target-${d.target})`)\n            // add source and target so that it can be traced back\n            .attr(\'data-source\', d => d.source)\n            .attr(\'data-target\', d => d.target)\n   \n        var nodes = svg.selectAll("g.outer")\n            .data(data.nodes, function (d) { return d.id; })\n            .enter()\n            .append("g")\n            .attr("class", "outer")\n            .attr("id", function (d) { return d.id; })\n            .call(d3.drag()\n                .on("start", dragStarted)\n                .on("drag", dragged)\n                .on("end", dragEnded)\n            );\n\n        nodes\n            .append("circle")\n            .style("fill", "lightgrey")\n            .style("stroke", "blue")\n            .attr("r", 40)\n            .on("mouseenter", function(ev, d) {\n               // trace back to custom arrowhead with given id\n                const arrowHead = d3.select(`#arrowhead-target-${d.id}`)\n               arrowHead.transition().duration(250).attr(\'refX\', 40)\n                d3.select(this)\n                    .transition()\n                        .duration(250)\n                            .attr("r", 40 * 1.3)\n                            .attr("fill", "blue")\n            })\n            .on("mouseleave", function(ev, d) {\n                const arrowHead = d3.select(`#arrowhead-target-${d.id}`)\n               arrowHead.transition().duration(250).attr(\'refX\', 23.5)\n                d3.select(this)\n                    .transition()\n                        .duration(250)\n                            .attr("r", 40)\n                            .attr("fill", "lightgrey")\n            })            \n\n        simulation\n            .nodes(data.nodes)\n            .on("tick", tick)\n\n        simulation\n            .force("link")\n            .links(data.links)\n\n\n            \n        function tick() {\n            links\n                .attr("x1", function (d) { return d.source.x; })\n                .attr("y1", function (d) { return d.source.y; })\n                .attr("x2", function (d) { return d.target.x; })\n                .attr("y2", function (d) { return d.target.y; });\n                \n\n            nodes\n                .attr("transform", d => `translate(${d.x}, ${d.y})`);\n        }\n\n        function dragStarted(event, d) {\n            if (!event.active) simulation.alphaTarget(0.3).restart();\n            d.fx = d.x;\n            d.fy = d.y;\n        }\n\n        function dragged(event, d) {\n            d.fx = event.x;\n            d.fy = event.y;\n        }\n\n        function dragEnded(event, d) {\n            if (!event.active) simulation.alphaTarget(0);\n            d.fx = null;\n            d.fy = null;\n        }
Run Code Online (Sandbox Code Playgroud)\r\n
body {\n        background: whitesmoke,\xc2\xb4;\n        overflow: hidden;\n        margin: 0px;\n    }
Run Code Online (Sandbox Code Playgroud)\r\n
<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset="utf-8">\n    <meta name="viewport" content="width=device-width, initial-scale=1">\n\n    <title>D3v7</title>\n    <!-- d3.js framework -->\n    <script src="https://d3js.org/d3.v7.js"></script>\n</head>\n\n\n\n<body>\n\n</body>\n\n</html>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n