Tuesday, 31 August 2021

D3.js highlight related nodes / links

Question: I want to fade / highlight a whole dependency chain, based on the link type.

To do so I utilize the mouseEnter event, which currently store all links and nodes. Further I fade all nodes and links and only highlight those nodes which where filtered as related nodes and links. It would require to check all related nodes and links again, if those have connections from type need too. This must be done as long as dependency connections are found.. I can´t figure out a proper algorythm.

Examples:

For better understanding I created a beer ingredients dependency, which looks lika a star. For those purposes my version is fine. BUT the second chain, about car -> wheel -> tires -> rubber and the radio is giving me headache. The radio is a "use" dependency, means its not mandatory for the chain and shouldn´t be hightlighted.

Expected result:

If the cursor is over car all connected nodes with a "need" dependency should be highlighted and the rest should fade.

For those who wants to help me with, please dont hesitate to ask, if anything is unclear.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- d3.js framework -->
    <script src="https://d3js.org/d3.v6.js"></script>
    <!-- fontawesome stylesheet https://fontawesome.com/ -->
    <script src="https://kit.fontawesome.com/39094309d6.js" crossorigin="anonymous"></script>
</head>

<style>
    body {
        height: 100%;
        background: #e6e7ee;
        overflow: hidden;
        margin: 0px;
    }

    .faded {
        opacity: 0.1;
        transition: 0.3s opacity;
    }

    .highlight {
        opacity: 1;
    }
</style>

<body>
    <svg id="svg"></svg>

    <script>
        var graph = {
            "nodes": [
                {
                    "id": 0,
                    "name": "beer",
                },
                {
                    "id": 1,
                    "name": "water",
                },
                {
                    "id": 2,
                    "name": "hop",
                },
                {
                    "id": 3,
                    "name": "malt",
                },
                {
                    "id": 4,
                    "name": "yeast",
                },
                {
                    "id": 10,
                    "name": "car",
                },
                {
                    "id": 11,
                    "name": "wheels",
                },
                {
                    "id": 12,
                    "name": "tires",
                },
                {
                    "id": 13,
                    "name": "rubber",
                },
                {
                    "id": 14,
                    "name": "radio",
                }
            ],
            "links": [
                {
                    "source": 0,
                    "target": 1,
                    "type": "need"
                },
                {
                    "source": 0,
                    "target": 2,
                    "type": "need"
                },
                {
                    "source": 0,
                    "target": 3,
                    "type": "need"
                },
                {
                    "source": 0,
                    "target": 4,
                    "type": "need"
                },
                {
                    "source": 10,
                    "target": 11,
                    "type": "need"
                },
                {
                    "source": 11,
                    "target": 12,
                    "type": "need"
                },
                {
                    "source": 12,
                    "target": 13,
                    "type": "need"
                },
                {
                    "source": 10,
                    "target": 14,
                    "type": "use"
                }

            ]
        }

        var svg = d3.select("svg")
            .attr("class", "canvas")
            .attr("width", window.innerWidth)
            .attr("height", window.innerHeight)
            .call(d3.zoom().on("zoom", function (event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        // append markers to svg
        svg.append("defs").append("marker")
            .attr("id", "arrowhead")
            .attr("viewBox", "-0 -5 10 10")
            .attr("refX", 8)
            .attr("refY", 0)
            .attr("orient", "auto")
            .attr("markerWidth", 50)
            .attr("markerHeight", 50)
            .attr("xoverflow", "visible")
            .append("svg:path")
            .attr("d", "M 0,-1 L 2 ,0 L 0,1")
            .attr("fill", "black")
            .style("stroke", "none")

        var linksContainer = svg.append("g").attr("class", linksContainer)
        var nodesContainer = svg.append("g").attr("class", nodesContainer)

        var force = d3.forceSimulation()
            .force("link", d3.forceLink().id(function (d) {
                return d.id
            }).distance(80))
            .force("charge", d3.forceManyBody().strength(-100))
            .force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
            .force("collision", d3.forceCollide().radius(90))

        initialize()

        function initialize() {

            link = linksContainer.selectAll(".link")
                .data(graph.links)
                .join("line")
                .attr("class", "link")
                .attr('marker-end', 'url(#arrowhead)')
                .style("display", "block")
                .style("stroke", "black")
                .style("stroke-width", 1)

            linkPaths = linksContainer.selectAll(".linkPath")
                .data(graph.links)
                .join("path")
                .style("pointer-events", "none")
                .attr("class", "linkPath")
                .attr("fill-opacity", 1)
                .attr("stroke-opacity", 1)
                .attr("id", function (d, i) { return "linkPath" + i })
                .style("display", "block")

            linkLabels = linksContainer.selectAll(".linkLabel")
                .data(graph.links)
                .join("text")
                .style("pointer-events", "none")
                .attr("class", "linkLabel")
                .attr("id", function (d, i) { return "linkLabel" + i })
                .attr("font-size", 16)
                .attr("fill", "black")
                .text("")

            linkLabels
                .append("textPath")
                .attr('xlink:href', function (d, i) { return '#linkPath' + i })
                .style("text-anchor", "middle")
                .style("pointer-events", "none")
                .attr("startOffset", "50%")
                .text(function (d) { return d.type })

            node = nodesContainer.selectAll(".node")
                .data(graph.nodes, d => d.id)
                .join("g")
                .attr("class", "node")
                .call(d3.drag()
                    .on("start", dragStarted)
                    .on("drag", dragged)
                    .on("end", dragEnded)
                )

            node.selectAll("circle")
                .data(d => [d])
                .join("circle")
                .attr("r", 30)
                .style("fill", "whitesmoke")
                .on("mouseenter", mouseEnter)
                .on("mouseleave", mouseLeave)

            node.selectAll("text")
                .data(d => [d])
                .join("text")
                .style("class", "icon")
                .attr("font-family", "FontAwesome")
                .attr("dominant-baseline", "central")
                .attr("text-anchor", "middle")
                .attr("font-size", 20)
                .attr("fill", "black")
                .attr("pointer-events", "none")
                .attr("dy", "-1em")
                .text(function (d) {
                    return d.name
                })
            node.append("text")
                .attr("dominant-baseline", "central")
                .attr("text-anchor", "middle")
                .attr("font-size", 13)
                .attr("fill", "black")
                .attr("pointer-events", "none")
                .attr("dy", "0.5em")
                .text(function (d) {
                    return d.id
                })

            force
                .nodes(graph.nodes)
                .on("tick", ticked);

            force
                .force("link")
                .links(graph.links)
        }

        function mouseEnter(event, d) {
            const selNodes = node.selectAll("circle")
            const selLink = link
            const selLinkLabel = linkLabels
            const selText = node.selectAll("text")
            const related = []
            const relatedLinks = []

            related.push(d)
            force.force('link').links().forEach((link) => {
                if (link.source === d || link.target === d) {
                    relatedLinks.push(link)
                    if (related.indexOf(link.source) === -1) { related.push(link.source) }
                    if (related.indexOf(link.target) === -1) { related.push(link.target) }
                }
            })
            selNodes.classed('faded', true)
            selNodes.filter((dNodes) => related.indexOf(dNodes) > -1)
                .classed('highlight', true)
            selLink.classed('faded', true)
            selLink.filter((dLink) => dLink.source === d || dLink.target === d)
                .classed('highlight', true)
            selLinkLabel.classed('faded', true)
            selLinkLabel.filter((dLinkLabel) => dLinkLabel.source === d || dLinkLabel.target === d)
                .classed('highlight', true)
            selText.classed('faded', true)
            selText.filter((dText) => related.indexOf(dText) > -1)
                .classed('highlight', true)
            
            force.alphaTarget(0.0001).restart()
        }

        function mouseLeave(event, d) {
            const selNodes = node.selectAll("circle")
            const selLink = link
            const selLinkLabel = linkLabels
            const selText = node.selectAll("text")

            selNodes.classed('faded', false)
            selNodes.classed('highlight', false)
            selLink.classed('faded', false)
            selLink.classed('highlight', false)
            selLinkLabel.classed('faded', false)
            selLinkLabel.classed('highlight', false)
            selText.classed('faded', false)
            selText.classed('highlight', false)
            
            force.restart()
        }

        function ticked() {
            // update link positions
            link
                .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;
                });

            // update node positions
            node
                .attr("transform", function (d) {
                    return "translate(" + d.x + ", " + d.y + ")";
                });

            linkPaths.attr('d', function (d) {
                return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
            });

            linkLabels.attr('transform', function (d) {
                if (d.target.x < d.source.x) {
                    var bbox = this.getBBox();

                    rx = bbox.x + bbox.width / 2;
                    ry = bbox.y + bbox.height / 2;
                    return 'rotate(180 ' + rx + ' ' + ry + ')';
                }
                else {
                    return 'rotate(0)';
                }
            });

        }

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

            PosX = d.x
            PosY = d.y
        }

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

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

    </script>
</body>

</html>


from D3.js highlight related nodes / links

No comments:

Post a Comment