Question

Essentially I'm trying to have all the paths except the one being hovered over turn gray, while the one being selected keeps it's original color. I've been able to turn all of the other paths gray, however I'm having trouble with the "select.this" function and actually accessing the path I want to change style. It would appear that I've actually managed to get down to the path element within the g group, however I'm confronted with an error in the console saying

Uncaught TypeError: Property 'style' of object #<SVGGElement> is not a function

Relevant code:

    svg.selectAll("g.team")
    .on("mouseover",function(){
            console.log("I see you!");
            var lineName;
            var accuracy = 10;

            svg.selectAll("path.team.line").style("stroke","#C0C0C0"); 
            //set all to gray

            var selectedArray = d3.select(this);
            console.log(selectedArray);

            var selectGroup = selectedArray[0];
            console.log("should be group:"+selectGroup);

            var selectedLine = selectGroup[0];;

            selectedLine.style("color",function(d){  //let active keep color
            lineName = abbrDict[d.name];  //full name to be at end of line
            return color(d.name);
        });

            //get position of end of line
        var len = d3.select(this).children().node().getTotalLength();
        var pos = d3.select(this).node().getPointAtLength(len);  
        //append text to end of line
        svg.append("text")
            .attr("id","tooltip")
            .attr("x",pos.x-55)
            .attr("y",pos.y)
            .text(lineName)
            .style("font-family","sans-serif")
            .style("font-size",13);

            this.parentNode.parentNode.appendChild(this.parentNode); 
            //brings team to front, must select the path's g parent 
            //to reorder it 

    })
    .on("mouseout",function(){
            d3.select("#tooltip").remove();

            d3.selectAll("team").selectAll("path")
              .transition()
              .style("stroke",function(d){
                  return color(d.name);  //return all the colors
               });

            d3.selectAll("axis").selectAll("line").style("color","black");

    });

Please and thank you!

Was it helpful?

Solution

D3 selections are arrays of arrays of DOM elements. (They are nested arrays so they can implement nested selections, while keeping separate index counts and properties for each sub-selection.)

So when you run a statement like:

var selectedArray = d3.select(this);

The selectedArray is of the structure [[ {SVGGElement} ]]. That much you seem to understand.

But your selectedArray isn't just an array containing an array containing a single DOM element. It is also a d3.selection object, with all the special functions that selections have, including the .style() function.

However, when you extract the sub-array in the next line:

var selectGroup = selectedArray[0];

You now just have an ordinary array containing an SVG <g> element node. It has no special functions from d3. And finally, when you extract the element from that array:

var selectedLine = selectGroup[0];

You just return the DOM element node itself. Which is the exact same object as the this that you originally selected. That node has a .style property, but not a .style() function.

Sometimes, you do want to extract a node from a d3 selection in order to use the properties or methods that are part of the DOM interface. If you did want to do this, the approach above would work, or you could access it all in one line with

var svgNode = d3.select("svg")[0][0];

Or, you could use the selection.node() method which does the exact same thing (grabs the first node in the first nest in the selection):

var svgNode = d3.select("svg").node();

But, if you want to use d3 selection methods on a single DOM element, you select that element and then call the methods on the selection. It doesn't matter whether the selection contains one element or 1000, your code is just the same. (It won't even throw an error if your selection is completely empty -- it just won't do anything!) If you want to use d3 methods on a child of your original selection, you need to use a subselection method (either selection.select() or selection.selectAll()).

svg.selectAll("g.team")
 .on("mouseover",function(){

    var lineName;
    svg.selectAll("path.team.line").style("stroke","#C0C0C0"); 
            //set all to gray

    var selectedGroup = d3.select(this);
    var selectedLine = selectedGroup.select("path.team.line");
                       //this only selects the (first) path that is a child
                       //of the selected group

    selectedLine.style("color",function(d){  //let active keep color
        lineName = abbrDict[d.name];  //full name to be at end of line
        return color(d.name);
    });

   /* ...etc... */

By the way, when you add an event handler to an element with d3's selection.on() method, d3 will automatically pass that element's data object as the first parameter to your event handling function. Which means you can simplify your code to avoid the secondary function call:

svg.selectAll("g.team")
 .on("mouseover",function(d){ //d as a function parameter

    var lineName;
    svg.selectAll("path.team.line").style("stroke","#C0C0C0"); 
            //set all to gray

    var selectedLine = d3.select(this);

    selectedLine.style("color", color(d.name) ); 
        //already have the `d` for this element available

    lineName = abbrDict[d.name];

   /* ...etc... */

To continue with your code: For positioning your text element, you are trying to use the .getTotalLength() and .getPointAtLength() methods of the <path> element. Now, these methods are DOM interface methods, not d3 methods, so you need the actual {SVGPathElement} node, not a d3 selection. However, you're mixing up your d3 selections with your DOM methods currently.

To access the node using pure Javascript from the this element (which is it's parent <g> element):

 var pathNode = this.children[0]; //assuming the <path> is the first child of <g>
 var len = pathNode.getTotalLength();
 var pos = pathNode.getPointAtLength( len );

Alternatively, you can access the <path> element from the d3 selection you created above:

 var pathNode = selectedLine.node(); //grab the node from the selection
 var len = pathNode.getTotalLength();
 var pos = pathNode.getPointAtLength( len );

Finally, this line:

this.parentNode.parentNode.appendChild(this.parentNode); 
        //brings team to front, must select the path's g parent 
        //to reorder it

I think should be just:

this.parentNode.appendChild(this); 
        //brings team to front, must select the path's g parent 
        //to reorder it

(since this is the <g> element already).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top