1

I am new to D3.js and am using it to build a tree diagram as in this JSFiddle (note this is not mine, this is just exactly what mine looks like).

I am trying to use D3-tip to create tooltips that get triggered upon hovering over the path links in the tree. However, because the actual size of the path element is so small, it is very difficult to trigger.

Is there any way to increase the area of the D3 path element transparently and from the Javascript so that it is easier to trigger these events? I have seen other SO examples such as in here and here but I am unable to implement them properly in the Javascript. I have obviously tried to increase the stroke of the path elements, but this looks silly.

Thank you.

My code is below:

export function createTree(json) {
    var width = 600;
    var height = 300;
    var maxLabel = 120;
    var duration = 200;
    var radius = 8;

    var i = 0;
    var root;

    var tree = d3.layout.tree()
        .size([height - 20, width - 20]);

    var diagonal = d3.svg.diagonal()
        .projection(function (d) {
            return [d.y, d.x];
        });

    var tip = d3.tip()
        .attr('class', 'd3-tip')
        .html(function(d) {
            var html = "<div id='popover-permission' align='center'> <div class='col-md-12'>" +
                "<div class='row text-center'><span style='color:#444444;'>Distribution Contract:</span><br>" +
            "<span class='textSmall' style='color:#444444;'>" + "3473247xxx78728347" + "</span></div></div></div>";

        return html;
    })

var svg = d3.select("#tree")
    .append("div")
    .classed("svg-container", true) //container class to make it responsive
    .append("svg")
    //responsive SVG needs these 2 attributes and no width and height attr
    .attr("preserveAspectRatio", "xMinYMin meet")
    .attr("viewBox", "0 0 " + width + " " + height)
    //class to make it responsive
    .classed("svg-content-responsive", true)
    .append("g")
    .attr("transform", "translate(" + maxLabel + ",0)");

svg.call(tip);

root = json;
root.x0 = height / 2;
root.y0 = 0;

function update(source) {
    // Compute the new tree layout.
    var nodes = tree.nodes(root).reverse();
    var links = tree.links(nodes);

    // Normalize for fixed-depth.
    nodes.forEach(function (d) {
        d.y = d.depth * maxLabel;
    });

    // Update the nodes…
    var node = svg.selectAll("g.node")
        .data(nodes, function (d) {
            return d.id || (d.id = ++i);
        });

    // Enter any new nodes at the parent's previous position.
    var nodeEnter = node.enter()
        .append("g")
        .attr("class", "node")
        .attr("transform", function (d) {
            return "translate(" + source.y0 + "," + source.x0 + ")";
        })
        .on("click", click);

    nodeEnter.append("circle")
        .attr("r", 0)
        .style("fill", function (d) {
            return d._children ? "lightsteelblue" : "white";
        });

    nodeEnter.append("text")
        .attr("text-anchor", "middle")
        .attr('x', 0)
        .attr('y', 30)
        .attr("dy", "-30")
        .attr("class", "node-text textSmall")
        .append('tspan')
        .attr('x', 0)
        .attr('dy', 10)
        .text(function(d) { return d.name; })
        .append('tspan')
        .attr('x', 0)
        .attr('dy', 20)
        .attr("class", "textSmall tree-balance")
        .text(function(d) {
            return "(" + d.value + " ETH)";
        })

    // Transition nodes to their new position.
    var nodeUpdate = node.transition()
        .duration(duration)
        .attr("transform", function (d) {
            return "translate(" + d.y + "," + d.x + ")";
        });

    nodeUpdate.select("circle")
        .attr("r", function (d) {
            return computeRadius(d);
        })
        .style("fill", function (d) {
            return d._children ? "lightsteelblue" : "#fff";
        });

    nodeUpdate.select("text").style("fill-opacity", 1);

    // Transition exiting nodes to the parent's new position.
    var nodeExit = node.exit().transition()
        .duration(duration)
        .attr("transform", function (d) {
            return "translate(" + source.y + "," + source.x + ")";
        })
        .remove();

    nodeExit.select("circle").attr("r", 0);
    nodeExit.select("text").style("fill-opacity", 0);

    // Update the links…
    var link = svg.selectAll("path.link")
        .data(links, function (d) {
            return d.target.id;
        });

    // Enter any new links at the parent's previous position.
    link.enter().insert("path", "g")
        .attr("class", "link")
        .attr("id", function(d) {
            return d.target.address;
        })
        .attr("d", function (d) {
            //console.log(d.source.name + d.target.name + " ");
            var o = {x: source.x0, y: source.y0};
            return diagonal({source: o, target: o});
        })
        .on('mouseover', tip.show)
        .on('mouseleave', tip.hide);

    // Transition links to their new position.
    link.transition()
        .duration(duration)
        .attr("d", diagonal);

    // Transition exiting nodes to the parent's new position.
    link.exit().transition()
        .duration(duration)
        .attr("d", function (d) {
            var o = {x: source.x, y: source.y};
            return diagonal({source: o, target: o});
        })
        .remove();

    // Stash the old positions for transition.
    nodes.forEach(function (d) {
        d.x0 = d.x;
        d.y0 = d.y;
    });
}

function computeRadius(d) {
    if (d.children || d._children) return radius + (radius * nbEndNodes(d) / 10);
    else return radius;
}

function nbEndNodes(n) {
    nb = 0;
    if (n.children) {
        n.children.forEach(function (c) {
            nb += nbEndNodes(c);
        });
    }
    else if (n._children) {
        n._children.forEach(function (c) {
            nb += nbEndNodes(c);
        });
    }
    else nb++;

    return nb;
}

function click(d) {
    if (d.children) {
        d._children = d.children;
        d.children = null;
    }
    else {
        d.children = d._children;
        d._children = null;
    }
    update(d);
}

function collapse(d) {
    if (d.children) {
        d._children = d.children;
        d._children.forEach(collapse);
        d.children = null;
    }
}

update(root);

}

EDIT: I also found this solution and added a transparent line to each path like so:

lines = gEnter
    .selectAll('.path').data(['visible', 'invisible'])
 lines.enter()
    .append('line')
    .attr('class', 'path')
    .attr('marker-end', function(d, i, j) {
        // j is the parent's i
        if (j === 2) {
            return 'url(#arrow)';
        } else {
            return null;
        }
    })
.attr({
    // returning null from these functions simply defaults to whatever the
    // .path class's CSS props are doing
    'stroke-width': function(d, i) { return d == 'invisible' ? 10 : null },
    'stroke': function(d, i) { return d == 'invisible' ? 'transparent' : null }
})

The thing is, even though the line is drawn it does not show up as an element in the browser, and hence doesn't affect the area at all. Is there any way to change this?

Community
  • 1
  • 1
annikam
  • 273
  • 3
  • 17
  • 1
    I would try to draw a transparent line over your visible line, and add the mouse events to the transparent line. I see you mention this in your EDIT, but it looks like you are taking a round-about way. Just add a transparent line the exact same way you add the visible lines. Would be happy to help if you provide a fiddle of your specific code. – wdickerson Jun 16 '16 at 13:13

1 Answers1

1

This is what I did: I duplicated your link as link2, with all the same attributes, but with a different class, which I set like this in the CSS:

.link2{
  fill: none;
  stroke: lightgray;
  stroke-width: 20px;
  opacity: 0;
}

Hover next to the path to see the title. Here is the fiddle: http://jsfiddle.net/gerardofurtado/JnNwu/1025/

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171