简体   繁体   中英

Fade links and nodes that are not immediately connected to the node hovered on in a d3 graph

I am now to d3 and web development in general.

I am creating a graph using the d3 library. I am trying to ensure that whenever the user hovers upon a node, the opacity of its immediate parents and children should remain the same but the opacity of the rest of the nodes should decrease.

I am partly achieving my goal by letting the text written below fade for all the ones other than the one I hover on.

Here is my javascript code:

// setting up the canvas size :)
var width = 960,
    height = 500;

// initialization
var svg = d3.select("div").append("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("id", "blueLine"); // the graph invisible thing :)

var force = d3.layout.force()
    .gravity(0) // atom's cohesiveness / elasticity of imgs :)
    .distance(150) // how far the lines ---> arrows :)
    .charge(-50) // meta state transition excitement
    .linkDistance(140)
    //.friction(0.55) // similar to charge for quick reset :)
    .size([width, height]); // degree of freedom to the canvas

// exception handling
d3.json("graph.json", function(error, json) {
    if (error) throw error;

    // Restart the force layout
    force
        .nodes(json.nodes)
        .links(json.links)
        .start();

    // Build the link
    var link = svg.selectAll(".links")
        .data(json.links)
        .enter().append("line")
        .attr("class", "lol")
        .style("stroke-width", "2")
        .attr("stroke", function(d){
            return linkColor(d.colorCode);})
        .each(function(d) {
            var color = linkColor(d.colorCode);
            d3.select(this).attr("marker-end", marker(color));
        });

    function marker(color) {
        svg.append("svg:marker")
            .attr("id", color.replace("#", ""))
            .attr("viewBox", "0 -5 10 10")
            .attr("refX", 10)
            .attr("refY", 0)
            .attr("markerWidth", 15)
            .attr("markerHeight", 15)
            .attr("orient", "auto")
            .attr("markerUnits", "userSpaceOnUse")
            .append("svg:path")
            .attr("d", "M0,-5L10,0L0,5")
            .style("fill", color);

        return "url(" + color + ")";
    };

    // this link : https://stackoverflow.com/questions/32964457/match-arrowhead-color-to-line-color-in-d3

    // create a node
    var node = svg.selectAll(".nodes")
        .data(json.nodes)
        .enter().append("g")
        .attr("class", "node")
        .call(force.drag)
        .on("mouseover", fade(.2))
        .on("mouseout", fade(1));;

    // Define the div for the tooltip
    var div = d3.select("body").append("pre")
        .attr("class", "tooltip")
        .style("opacity", 0);

    // Append custom images
    node.append("svg:image")
        .attr("xlink:href",  function(d) { return d.img;}) // update the node with the image
        .attr("x", function(d) { return -5;}) // how far is the image from the link??
        .attr("y", function(d) { return -25;}) // --- same ---
        .attr("height", 55) // size
        .attr("width", 55);

    node.append("text")
        .attr("class", "labelText")
        .attr("x", function(d) { return -5;})
        .attr("y", function(d) { return 48;})
        .text(function(d) { return d.name });

    force.on("tick", function() {
        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; });

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

        force.stop();
    });

    function linkColor(linkCode) {
        switch (linkCode)
        {
            case 'ctoc':
                return '#0000FF';//blue
                break;
            case 'ctof':
                return '#00afaa';//green
                break;
            case 'ftoc':
                return '#fab800';//yellow
                break;
            case 'ftof':
                return '#7F007F';//purple
                break;
            default:
                return '#0950D0';//generic blue
                break;
        }
    }

    // build a dictionary of nodes that are linked
    var linkedByIndex = {};
    links.forEach(function(d) {
        linkedByIndex[d.source.id + "," + d.target.id] = 1;
    });

    // check the dictionary to see if nodes are linked
    function isConnected(a, b) {
        return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
    }

    // fade nodes on hover
    function fade(opacity) {
        return function(d) {
            // check all other nodes to see if they're connected
            // to this one. if so, keep the opacity at 1, otherwise
            // fade
            node.style("stroke-opacity", function(o) {
                thisOpacity = isConnected(d, o) ? 1 : opacity;
                return thisOpacity;
            });
            node.style("fill-opacity", function(o) {
                thisOpacity = isConnected(d, o) ? 1 : opacity;
                return thisOpacity;
            });
            // also style link accordingly
            link.style("stroke-opacity", function(o) {
                return o[0] === d || o[2] === d ? 1 : opacity;
            });
        };
    }
});

The css:

.node text {
    font-size: 1rem;
    text-decoration: underline;
    fill: #aeb4bf;
    font-weight: 700;
    text-anchor: end;
    alignment-baseline: central;
    pointer-events: none;
}
.node:not(:hover) .nodetext {
    display: none;
}

pre.tooltip {
    position: absolute;
    text-align: left;
    width: auto;
    height: auto;
    padding: 5px;
    font: 14px "Helvetica","Arial",sans-serif bold;
    background: #273142;
    border: 0;
    border-radius: 8px;
    cursor: pointer!important;
    pointer-events: none;
    color: #aeb4bf;
}

and my json file:

{
  "nodes": [
    {"x": 100, "y": 100, "name": "A", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 0},
    {"x": 250, "y": 100, "name": "B", "img":"https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 1},
    {"x": 400, "y": 100, "name": "C", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id": 2},
    {"x": 550, "y": 200, "name": "D", "img":"https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 3},
    {"x": 700, "y": 200, "name": "E", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 4},
    {"x": 100, "y": 300, "name": "F", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 5},
    {"x": 250, "y": 300, "name": "G", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 6},
    {"x": 400, "y": 300, "name": "H", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id": 7}
  ],
  "links": [
    {"source":  0, "target":  1, "colorCode" : "ctof"},
    {"source":  1, "target":  2, "colorCode" : "ftoc"},
    {"source":  2, "target":  3, "colorCode" : "ctof"},
    {"source":  3, "target":  4, "colorCode" : "ftoc"},
    {"source":  5, "target":  6, "colorCode" : "ctof"},
    {"source":  6, "target":  7, "colorCode" : "ftoc"},
    {"source":  7, "target":  3, "colorCode" : "ctof"}
  ]
}

I dont know where I am going wrong. I need to achieve two things: 1. The immediate parents and children of X should stay unfaded if I hover over X and 2. The other nodes which aren't directly related to X should fade just the way the other links do. Currently none of the node fades.

I researched over my code and realized that it says that all the nodes are connected to each other so my isConnected() is the culprit. I still have no clue about the links though.

Please help me.

two issues to resolve

  1. For your nodes, as they are image files, you need set their 'opacity', and not the stroke/fill opacity.

     node.style("opacity", function(o) { thisOpacity = isConnected(d, o) ? 1 : opacity; return thisOpacity; }); 
  2. For your links, assuming the name attributes are unique, you should match the link's source and target to the chosen node's name.

     link.style("stroke-opacity", function(o) { return o.source.name === d.name || o.target.name === d.name ? 1 : opacity; }); 

Addition to @TomShanley answer

Why are you using d3v3 if you are new to d3? Currently we are at d3v5 and the API has much been improved.

The program does not run out of the box because in determining linkedByIndex it complains that links does not exist. It should be json.links .

There is no need to put break after a return in linkColor .

You search for elements of class svg.selectAll(".nodes") but you create elements with .attr("class", "node") . This will not work if you want to use the enter-exit-update properly. The same with the links: search for class links but add elements with class lol .

Your markers are not unique and no need to use each to add the marker-end . Maybe best to create a set of markers based on color and just reference them. In the original code you have multiple tags with the same id . And an id in HTML should be unique.

// Build the link
var link = svg.selectAll(".lol")
    .data(json.links)
    .enter().append("line")
    .attr("class", "lol")
    .style("stroke-width", "2")
    .attr("stroke", function(d){
        return linkColor(d.colorCode);})
    .attr("marker-end", function(d, i){
        return marker(i, linkColor(d.colorCode));} );

function marker(i, color) {
    var markId = "#marker"+i;
    svg.append("svg:marker")
        .attr("id", markId.replace("#", ""))
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 10)
        .attr("refY", 0)
        .attr("markerWidth", 15)
        .attr("markerHeight", 15)
        .attr("orient", "auto")
        .attr("markerUnits", "userSpaceOnUse")
        .append("svg:path")
        .attr("d", "M0,-5L10,0L0,5")
        .style("fill", color);

    return "url(" + markId + ")";
};

Edit Unique markers, link is path from edge to edge

I have modified the code to have:

  • unique markers for each color put in the defs tag of the svg. Create a new marker if not already done for this color using an object to keep track.
  • links are now paths to apply a marker trick described by Gerardo
  • images are now centered on the node position, this only works for circular images.

Here is the full code

var width = 960,
    height = 500;

// initialization
var svg = d3.select("div").append("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("id", "blueLine"); // the graph invisible thing :)

var svgDefs = svg.append("defs");

var force = d3.layout.force()
    .gravity(0) // atom's cohesiveness / elasticity of imgs :)
    .distance(150) // how far the lines ---> arrows :)
    .charge(-50) // meta state transition excitement
    .linkDistance(140)
    //.friction(0.55) // similar to charge for quick reset :)
    .size([width, height]); // degree of freedom to the canvas

// exception handling
d3.json("/fade-links.json", function(error, json) {
    if (error) throw error;

    var imageSize = { width:55, height:55 };

    // Restart the force layout
    force
        .nodes(json.nodes)
        .links(json.links)
        .start();

    var markersDone = {};

    // Build the link
    var link = svg.selectAll(".lol")
        .data(json.links)
        .enter().append("path")
        .attr("class", "lol")
        .style("stroke-width", "2")
        .attr("stroke", function(d){
            return linkColor(d.colorCode);})
        .attr("marker-end", function(d){
            return marker(linkColor(d.colorCode));} );

    function marker(color) {
        var markerId = markersDone[color];
        if (!markerId) {
            markerId = color;
            markersDone[color] = markerId;
            svgDefs.append("svg:marker")
                .attr("id", color.replace("#", ""))
                .attr("viewBox", "0 -5 10 10")
                .attr("refX", 10)
                .attr("refY", 0)
                .attr("markerWidth", 15)
                .attr("markerHeight", 15)
                .attr("orient", "auto")
                .attr("markerUnits", "userSpaceOnUse")
                .append("svg:path")
                .attr("d", "M0,-5L10,0L0,5")
                .style("fill", color);
        }
        return "url(" + markerId + ")";
    };

    // this link : https://stackoverflow.com/questions/32964457/match-arrowhead-color-to-line-color-in-d3

    // create a node
    var node = svg.selectAll(".node")
        .data(json.nodes)
        .enter().append("g")
        .attr("class", "node")
        .call(force.drag)
        .on("mouseover", fade(.2))
        .on("mouseout", fade(1));

    // Define the div for the tooltip
    var div = d3.select("body").append("pre")
        .attr("class", "tooltip")
        .style("opacity", 0);

    // Append custom images
    node.append("svg:image")
        .attr("xlink:href",  function(d) { return d.img;}) // update the node with the image
        .attr("x", function(d) { return -imageSize.width*0.5;}) // how far is the image from the link??
        .attr("y", function(d) { return -imageSize.height*0.5;}) // --- same ---
        .attr("height", imageSize.width)
        .attr("width", imageSize.height);

    node.append("text")
        .attr("class", "labelText")
        .attr("x", function(d) { return 0;})
        .attr("y", function(d) { return imageSize.height*0.75;})
        .text(function(d) { return d.name });

    force.on("tick", function() {
        // use trick described by Gerardo to only draw link from image border to border:  https://stackoverflow.com/q/51399062/9938317
        link.attr("d", function(d) {
            var dx = d.target.x - d.source.x,
                dy = d.target.y - d.source.y;
            var angle = Math.atan2(dy, dx);
            var radius = imageSize.width*0.5;
            var offsetX = radius * Math.cos(angle);
            var offsetY = radius * Math.sin(angle);
            return ( `M${d.source.x + offsetX},${d.source.y + offsetY}L${d.target.x - offsetX},${d.target.y - offsetY}`);
          });

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

        force.stop();
    });

    function linkColor(linkCode) {
        switch (linkCode)
        {
            case 'ctoc': return '#0000FF';//blue
            case 'ctof': return '#00afaa';//green
            case 'ftoc': return '#fab800';//yellow
            case 'ftof': return '#7F007F';//purple
        }
        return '#0950D0';//generic blue
    }

    // build a dictionary of nodes that are linked
    var linkedByIndex = {};
    json.links.forEach(function(d) {
        linkedByIndex[d.source.id + "," + d.target.id] = 1;
    });

    // check the dictionary to see if nodes are linked
    function isConnected(a, b) {
        return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
    }

    // fade nodes on hover
    function fade(opacity) {
        return function(d) {
            // check all other nodes to see if they're connected
            // to this one. if so, keep the opacity at 1, otherwise
            // fade
            node.style("opacity", function(o) {
                return isConnected(d, o) ? 1 : opacity;
            });
            // also style link accordingly
            link.style("opacity", function(o) {
                return o.source.name === d.name || o.target.name === d.name ? 1 : opacity;
            });
        };
    }
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM