Home > OS >  D3 recalculate arrowhead position based on node size
D3 recalculate arrowhead position based on node size

Time:12-30

I am facing two visual problems with the given D3 forced graph.

  1. The last inch of each link is still visible. The arrowhead should end exactly at the node edge, which is the case, but the red link underneath is visible. I could adjust the link stroke-width or the arrow size but actually do not want to change those attributes.

enter image description here

  1. The radius of a node increases on a mouseenter event. If node size increases, the arrowhead is out of position and does not end on the node endge anymore. I assume I need to adjust the tick function, but do not find a proper starting point.

enter image description here

I appreciate any link, hint, help which gets me closer to the result.

        var width = window.innerWidth,
            height = window.innerHeight;

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function(event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        ////////////////////////
        // outer force layout

        var data = {
            "nodes":[
                { "id": "A" }, 
                { "id": "B" },
                { "id": "C" },
            ],
            "links": [
                { "source": "A", "target": "B"},
                { "source": "B", "target": "C"},
                { "source": "C", "target": "A"}
            ]
        };

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

        svg.append("defs").append("marker")
            .attr('id', 'arrowhead')
            .attr('viewBox', '-0 -5 10 10')
            .attr("refX", 23.5)
            .attr("refY", 0)
            .attr('orient', 'auto')
            .attr('markerWidth', 10)
            .attr('markerHeight', 10)
            .attr("orient", "auto")
            .append('svg:path')
            .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
            .attr('fill', '#999')
            .style('stroke', 'none');

        var links = svg.selectAll(".links")
            .data(data.links)
            .join("line")
            .attr("stroke", "red")
            .attr("stroke-width", 3)
            .attr("marker-end", "url(#arrowhead)")
   
        var nodes = svg.selectAll("g.outer")
            .data(data.nodes, function (d) { return d.id; })
            .enter()
            .append("g")
            .attr("class", "outer")
            .attr("id", function (d) { return d.id; })
            .call(d3.drag()
                .on("start", dragStarted)
                .on("drag", dragged)
                .on("end", dragEnded)
            );

        nodes
            .append("circle")
            .style("fill", "lightgrey")
            .style("stroke", "blue")
            .attr("r", 40)
            .on("mouseenter", function() {
                d3.select(this)
                    .transition()
                        .duration(250)
                            .attr("r", 40 * 1.3)
                            .attr("fill", "blue")
            })
            .on("mouseleave", function() {
                d3.select(this)
                    .transition()
                        .duration(250)
                            .attr("r", 40)
                            .attr("fill", "lightgrey")
            })            

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

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


            
        function tick() {
            links
                .attr("x1", function (d) { return d.source.x; })
                .attr("y1", function (d) { return d.source.y; })
                .attr("x2", function (d) { return d.target.x; })
                .attr("y2", function (d) { return 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;
        }
    body {
        background: whitesmoke,´;
        overflow: hidden;
        margin: 0px;
    }
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>D3v7</title>
    <!-- d3.js framework -->
    <script src="https://d3js.org/d3.v7.js"></script>
</head>



<body>

</body>

</html>

CodePudding user response:

solution

at the moment I can think of two solutions.

  1. calculate each <path(.links)/> vector and subtract with extended circle radius
  2. give offset to each <marker />

personally I've chosen the 2nd solution; it requires less computation and easier to understand.

1. give each links custom arrowhead

add separate arrowheads for each link because it seems individual offset is not supported by svg path.

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

2. extend, subtract each individual marker's offset with mouseevent

the offset for <marker /> is applied with .refX, .refY attributes, as noted in MDN docs

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

Try run the code below. The size of extension and subtraction is purposefully exaggerated so that you can see the difference.

var width = window.innerWidth,
            height = window.innerHeight;

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function(event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        ////////////////////////
        // outer force layout

        var data = {
            "nodes":[
                { "id": "A" }, 
                { "id": "B" },
                { "id": "C" },
            ],
            "links": [
                { "source": "A", "target": "B"},
                { "source": "B", "target": "C"},
                { "source": "C", "target": "A"}
            ]
        };

        var simulation = d3.forceSimulation()
            .force("size", d3.forceCenter(width / 2, height / 2))
            .force("charge", d3.forceManyBody().strength(-1000))
            .force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
        // add custom arrowheads for each link
        // to avoid exhaustive computations
        svg.append("defs").selectAll('marker')
            .data(data.links)
            .join("marker")
            .attr('id', d => `arrowhead-target-${d.target}`)
            .attr('viewBox', '-0 -5 10 10')
            .attr("refX", 23.5)
            .attr("refY", 0)
            .attr('orient', 'auto')
            .attr('markerWidth', 10)
            .attr('markerHeight', 10)
            .attr("orient", "auto")
            .append('svg:path')
            .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
            .attr('fill', '#999')
            .style('stroke', 'none');

        var links = svg.selectAll(".links")
            .data(data.links)
            .join("line")
            .attr("stroke", "red")
            .attr("stroke-width", 3)
            .attr("marker-end", d => `url(#arrowhead-target-${d.target})`)
            // add source and target so that it can be traced back
            .attr('data-source', d => d.source)
            .attr('data-target', d => d.target)
   
        var nodes = svg.selectAll("g.outer")
            .data(data.nodes, function (d) { return d.id; })
            .enter()
            .append("g")
            .attr("class", "outer")
            .attr("id", function (d) { return d.id; })
            .call(d3.drag()
                .on("start", dragStarted)
                .on("drag", dragged)
                .on("end", dragEnded)
            );

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

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

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


            
        function tick() {
            links
                .attr("x1", function (d) { return d.source.x; })
                .attr("y1", function (d) { return d.source.y; })
                .attr("x2", function (d) { return d.target.x; })
                .attr("y2", function (d) { return 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;
        }
body {
        background: whitesmoke,´;
        overflow: hidden;
        margin: 0px;
    }
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>D3v7</title>
    <!-- d3.js framework -->
    <script src="https://d3js.org/d3.v7.js"></script>
</head>



<body>

</body>

</html>

  • Related