Julia Spyrou
Julia Spyrou

Reputation: 31

D3 Mouse Over Groups Issue

I am trying to do a mouseover for each group of countries that displays the corresponding chords. I have created an opacity function at the bottom of my code for this but it doesn't seem to register the groups or mouseover functions for some reason.

            <!DOCTYPE html>
            <meta charset="utf-8">
            <head>
            <style>
            body {
              font: 10px sans-serif;
              background-color: #F0F0F0;

            }

            h1 {
              font-size: 400%;
              position: relative;
              font-family: Helvetica;
            }

            h3 {
              font-family: Helvetica;
              position: relative;
            }

            p {
              position: relative;
              font-style: italic;
              font-family: Helvetica;
              top:1000px;
              left: 100px;
            }


            @media only screen and (max-width:1000px) {
              /* For tablets: */
              .main {
                width: 80%;
                padding: 0;
              }
              .right {
                width: 100%;
              }
            }

            button {
                background-color: #4CA350;
                position: relative;
                border: none;
                color: white;
                padding: 15px 32px;
                text-align: center;
                text-decoration: none;
                display: inline-block;
                font-size: 16px;
                margin: 4px 2px;
                cursor: pointer;
            }

            .group-tick line {
              stroke: #000;
            }

            .ribbons {
              fill-opacity: 1;

            }

            #tooltip.total {
                position: relative;
                width: 200px;
                height: auto;
                padding: 10px;
                background-color: white;
                -webkit-border-radius: 10px;
                -moz-border-radius: 10px;
                border-radius: 10px;
                -webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
                -moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
                box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
                pointer-events: none;
            }

            #tooltip {
                position: relative;
                font-family: Helvetica Neue;
                font-size: 16px;
                top:870px;
                left:720px;
                width: 200px;
                height: auto;
                padding: 10px;
                background-color: white;
                -webkit-border-radius: 10px;
                -moz-border-radius: 10px;
                border-radius: 10px;
                -webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
                -moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
                box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
                pointer-events: none;
            }

            #tooltip.hidden {
                display: none;
            }

            #tooltip p {
                margin: 0;
                font-family: sans-serif;
                font-size: 40px;
                line-height: 20px;
            }
            </style>
            <title>Julia Spyrou DECO3100 Assignment 3</title>
            <body>
            <h1>Voting at the Eurovision Song Contest</h1>
            <h3>Mouseover individual chords to view voting relationships</h3>
              <p>NB: These visualisations exclude the countries that did not make the Final and their votes as well</p>
              <button type="button">2016</button>
              <button type="button">2017</button>
              <button type="button">2018</button>
              <div id="tooltip" class="hidden">
                <p>$<span id="value">100</span></p>
              </div>

            <svg width="1000" height="1000"></svg>
            <script src="https://d3js.org/d3.v4.min.js"></script>
            <script>



            var names = ["Ukraine","Spain","Slovenia","Lithuania","Austria","Estonia","Norway","Portugal","UK","Serbia","Germany","Albania","France","Czech Republic","Denmark","Australia","Finland","Bulgaria","Moldova","Sweden","Hungary","Israel","Netherlands","Ireland","Cyprus","Italy"];

            var opacityDefault = 0.7;


            var matrix = [
              [0.1,0,0,0,0,0,0,4,0,0,0,0,4,12,0,0,0,1,15,0,0,7,0,0,2,8], //Ukraine
              [0,0,0,0,0,0,2,14,1,0,6,0,5,0,6,7,0,0,0,0,0,0,0,1,7,0], //Spain
              [5,0,0,0,0,0,0,3,0,8,0,0,2,7,0,0,0,0,0,0,0,4,1,0,0,0], //Slovenia
              [2,0,0,0,0,22,15,6,12,0,8,0,0,1,0,3,0,10,0,7,5,0,7,12,6,0],  //Lithuania
              [7,8,12,15,0,18,16,8,12,4,15,2,7,5,18,5,10,16,3,12,11,13,13,5,2,7], //Austria
              [4,3,5,12,2,0,0,19,6,3,2,4,7,5,7,8,12,2,13,0,0,8,4,7,5,10], //Estonia
              [0,3,5,0,0,6,0,0,5,9,0,0,0,0,8,1,0,0,7,10,7,0,4,0,5,12], //Norway
              [0,0,0,7,0,3,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,2,6,0,0], //Portugal
              [0,0,0,0,0,0,0,0,0,0,1,3,3,0,3,6,0,0,0,0,0,8,0,10,0,6], //UK
              [0,0,0,0,8,0,0,0,0,0,0,3,1,0,0,0,0,7,0,0,0,0,0,0,0,1], //Serbia
              [0,13,4,7,16,6,18,8,3,10,0,14,8,3,24,12,1,6,8,6,1,8,24,16,3,13], //Germany
              [0,0,5,0,9,0,0,10,7,1,0,0,0,6,0,0,0,7,0,0,10,0,0,2,6,12], //Albania
              [19,10,2,15,0,0,8,5,0,0,0,12,0,0,2,0,9,5,0,5,0,6,0,4,4,0], //France
              [14,14,11,8,15,5,4,0,5,5,8,1,4,0,6,4,5,11,6,3,8,12,6,7,13,0], //Czech Republic
              [11,0,7,10,5,8,10,2,2,4,3,0,2,7,0,10,13,0,0,12,24,0,8,2,0,12], //Denmark
              [2,0,0,0,0,0,6,0,1,0,7,0,10,0,12,0,0,0,7,8,7,7,0,0,0,0], //Australia
              [0,0,0,0,0,12,0,0,4,0,0,0,0,3,0,4,0,0,0,9,0,6,0,0,0,0], //Finland
              [0,5,0,0,6,7,0,7,14,2,0,14,0,5,1,0,10,0,11,6,0,1,0,10,12,0], //Bulgaria
              [6,0,6,0,0,1,0,6,4,6,0,2,6,6,0,7,0,8,0,0,2,10,0,1,10,10], //Moldova
              [6,2,12,12,8,5,13,0,2,12,12,4,5,8,11,12,8,2,0,0,1,10,8,0,12,1], //Sweden
              [1,0,3,2,3,3,0,0,0,12,2,0,0,2,0,0,8,5,2,0,0,3,2,0,4,0], //Hungary
              [22,22,1,7,19,0,7,2,17,9,11,6,24,22,3,18,19,14,22,17,16,0,15,13,10,9], //Israel
              [8,0,7,3,1,1,9,0,0,7,5,0,6,0,5,0,2,0,0,1,8,0,0,3,0,0], //Netherlands
              [0,6,0,4,8,0,1,3,13,0,15,7,1,14,4,12,2,1,0,4,3,0,4,0,0,5], //Ireland
              [4,20,14,7,1,12,7,5,8,10,9,20,3,8,6,7,7,15,13,16,7,2,11,17,0,8], //Cyprus
              [5,10,10,7,10,7,0,14,0,14,12,24,10,2,0,0,10,6,8,0,6,5,7,0,15,0], //Italy

            ];

            var svg = d3.select("svg"),
                width = +svg.attr("width"),
                height = +svg.attr("height"),
                outerRadius = Math.min(width, height) * 0.4 - 100,
                innerRadius = outerRadius - 20;

            var formatValue = d3.formatPrefix(",.0", 1e3);

            var chord = d3.chord()
                .padAngle(0.05)
                .sortSubgroups(d3.descending);

            var arc = d3.arc()
                .innerRadius(innerRadius)
                .outerRadius(outerRadius);

            var ribbon = d3.ribbon()
                .radius(innerRadius);

            var color = d3.scaleOrdinal()
                .domain(d3.range(4))
                .range(["#4B5320", "#50C878", "#98FB98", "#679267","#2E8B57","#043927", "#0B6623","#9DC183","#708238", "#C7EA46", "#3F704D","#00A86B","8F9779"]);

            var g = svg.append("g")
                .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
                .datum(chord(matrix));

            var group = g.append("g")
                .attr("class", "groups")
              .selectAll("g")
              .data(function(chords) { return chords.groups; })
              .enter().append("g");


            group.append("path")
                .style("fill", function(d) { return color(d.index); })
                .style("stroke", function(d) { return d3.rgb(color(d.index)).darker(); })
                .attr("d", arc)
                .on("mouseover", fade(5))
                .on("mouseout", fade(5));

            function fade(opacity) {
              return function(d, i) {
              svg.selectAll(".ribbons path")
                  .filter(function(d) { return d.source.index != i && d.target.index != i; })
                  .transition()
                 };
            }

            group.append("text")
              .each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
              .attr("dy", ".35em")
              .attr("class", "titles")
              .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
              .attr("transform", function(d) {
                if (outerArcs = 0) {
                  width = 10;
                }
                return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
                + "translate(" + (outerRadius + 10) + ")"
                + (d.angle > Math.PI ? "rotate(180)" : "");
              })
              .text(function(d,i) { return names[i]; });

            g.append("g")
                .attr("class", "ribbons")
              .selectAll("path")
              .data(function(chords) { return chords; })
              .enter().append("path")
                .attr("d", ribbon)
                .style("fill", function(d) { return color(d.target.index); })
                .style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); });

            // Returns an array of tick angles and values for a given group and step.
            function groupTicks(d, i) {
              var k = (d.endAngle - d.startAngle) / d.value;
              return d3.range(0, d.value,0.2).map(function(v, i) {
                return {
                  angle: v * k + d.startAngle,
                  label: i*100 % 5 ? null : v,
                  id: i
                };
              });
            }

            svg.selectAll("path")
                .on("mouseover", function(d) { //event when we mouseover
                  d3.select(this) //this gives us acess to the particular rectange in the selection
                    .transition()
                    .duration(100)
                    .attr("fill", "rgb(170, 255, 44)"); //hover colour
                    var h = 20;
                  //get tooltip location
                  var xPosition = parseFloat(d3.select(this).attr("x"));
                  var yPosition = parseFloat(d3.select(this).attr("y")) / 100 + h / 7;

            svg.selectAll("path")
                .transition()
                .style("opacity", 0.1);
              d3.select(this)
                .transition()
                    .style("opacity", 1);
                  //update tooltip location and value
                  console.log(d.source.index);
                  d3.select("#tooltip")
                    .style("left", xPosition + "100px")
                    .style("top", yPosition + "3000px")
                    //.select("#value")
                    .text(names[d.source.index] + " received " + d.source.value + " points from " + names[d.target.index]);

                  //use hidden css to hide and show the tooltip
                d3.select("#tooltip").classed("hidden", false);
                })
                .on("mouseout", function(d) { //reset fill on mouse out
                d3.select(this)
                  .transition()
                  .duration(250)
                  .attr("fill", "rgb(0," + Math.round(d+60) + ",0)");
                //hide tooltip
                d3.select("#tooltip").classed("hidden", true);
                });

             function fade(opacity) {
               return function(g, i) {
                 svg.selectAll("g.chord ribbons")
                     .filter(function(d) {
                       return d.source.index != i && d.target.index != i;
                     })
                   .transition()
                     .style("opacity", opacity);

               };
             }
            </script>

If anyone has a working solution for this that would be awesome, cheers :)

Upvotes: 1

Views: 907

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38201

To start I'm going to mention why it is important to keep your selections straight. For example, here:

 group.append("path")
      ...
      .on("mouseover", fade(5))
      .on("mouseout", fade(5));

You apply listeners for mouse events, but below you (and you might be trying to capture the ribbons, but I'm not sure), you over-write both of these listeners with:

   svg.selectAll("path")
      .on("mouseover", function(d) { ...

And through a more complex method (which doesn't follow a usual pattern), you also overwrite the listener for mouseout. An event listener can only be assigned once for a particular element. And as we need to have different listeners for both groups (outer arcs) and ribbons, we need to change this approach.

The good news is that this can result in much shorter and clearer code. To reuse your ribbon selection, I've created a variable to hold it called ribbons, the variable group will continue to hold the outer arcs.

I'm going to break out the three primary events I see you trying to do:

  1. Unhighlighting everything

This should be pretty straightforward, we can use a function such as:

function showAllRibbons() {
    ribbons.style("opacity",1)
}
  1. Highlighting a single path on mouse event:

This is also pretty straightforward, we hide everything else and show the element that this points to:

function highlightOneRibbon() {
    ribbons.style("opacity",0.1);
    d3.select(this).style("opacity",1);
}
  1. Highlight all ribbons that have a source or target in a particular outer arc.

This is the most challenging, it helps to see the data structure of each ribbon, which has properties for the id of the two anchors located at: d.source.index and d.target.index. Now using the index of each outer arc in the selection group we can do a simple filter for those ribbons that meet a basic criteria:

function highlightRibbons(d,i) {
    ribbons.style("opacity",0.1); // set all relatively transparent
    // fix the ones that need to be shown:
    ribbons.filter(function(r) {
        if(r.target.index == i || r.source.index == i) return r;
      })
      .style("opacity",1);

Of course you could get a bit fancier, filter those that don't meet the criteria to fade them out, filter those that do to fade in.

This function is some more evidence why you need distinct event listeners for both ribbon and arc, as this function won't work with d3.selectAll("path").on("mouseouver,highlightRibbons) because our indexes might be wrong (as we are selecting the ribbons too, but this function also doesn't make sense with the ribbons).


Ok, so. Let's strip out some of your code that uses d3.selectAll("path") to manipulate elements that already exist (about line 247 onwards?) as this is causing us some grief, while we are at it, as the event listeners that call the function fade are overwritten we aren't using that, let's drop that function too.

As a result I'm loosing the tooltip interactivity, but that seemed to have some issues that were independent of the question (but again, separate listeners are likely required for group and ribbon so that the tooltip has relevant information based on the type of feature it references).

Here's a quick demo of the result with a bit of added flair for showing the group (transitions) (because snippets loathe larger amounts of code it seems, I've trimmed the tooltip stylings and the buttons etc as they are not functional in or used in this demonstration anyways):

var names = ["Ukraine","Spain","Slovenia","Lithuania","Austria","Estonia","Norway","Portugal","UK","Serbia","Germany","Albania","France","Czech Republic","Denmark","Australia","Finland","Bulgaria","Moldova","Sweden","Hungary","Israel","Netherlands","Ireland","Cyprus","Italy"];

            var opacityDefault = 0.7;

            var matrix = [
              [0.1,0,0,0,0,0,0,4,0,0,0,0,4,12,0,0,0,1,15,0,0,7,0,0,2,8], //Ukraine
              [0,0,0,0,0,0,2,14,1,0,6,0,5,0,6,7,0,0,0,0,0,0,0,1,7,0], //Spain
              [5,0,0,0,0,0,0,3,0,8,0,0,2,7,0,0,0,0,0,0,0,4,1,0,0,0], //Slovenia
              [2,0,0,0,0,22,15,6,12,0,8,0,0,1,0,3,0,10,0,7,5,0,7,12,6,0],  //Lithuania
              [7,8,12,15,0,18,16,8,12,4,15,2,7,5,18,5,10,16,3,12,11,13,13,5,2,7], //Austria
              [4,3,5,12,2,0,0,19,6,3,2,4,7,5,7,8,12,2,13,0,0,8,4,7,5,10], //Estonia
              [0,3,5,0,0,6,0,0,5,9,0,0,0,0,8,1,0,0,7,10,7,0,4,0,5,12], //Norway
              [0,0,0,7,0,3,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,2,6,0,0], //Portugal
              [0,0,0,0,0,0,0,0,0,0,1,3,3,0,3,6,0,0,0,0,0,8,0,10,0,6], //UK
              [0,0,0,0,8,0,0,0,0,0,0,3,1,0,0,0,0,7,0,0,0,0,0,0,0,1], //Serbia
              [0,13,4,7,16,6,18,8,3,10,0,14,8,3,24,12,1,6,8,6,1,8,24,16,3,13], //Germany
              [0,0,5,0,9,0,0,10,7,1,0,0,0,6,0,0,0,7,0,0,10,0,0,2,6,12], //Albania
              [19,10,2,15,0,0,8,5,0,0,0,12,0,0,2,0,9,5,0,5,0,6,0,4,4,0], //France
              [14,14,11,8,15,5,4,0,5,5,8,1,4,0,6,4,5,11,6,3,8,12,6,7,13,0], //Czech Republic
              [11,0,7,10,5,8,10,2,2,4,3,0,2,7,0,10,13,0,0,12,24,0,8,2,0,12], //Denmark
              [2,0,0,0,0,0,6,0,1,0,7,0,10,0,12,0,0,0,7,8,7,7,0,0,0,0], //Australia
              [0,0,0,0,0,12,0,0,4,0,0,0,0,3,0,4,0,0,0,9,0,6,0,0,0,0], //Finland
              [0,5,0,0,6,7,0,7,14,2,0,14,0,5,1,0,10,0,11,6,0,1,0,10,12,0], //Bulgaria
              [6,0,6,0,0,1,0,6,4,6,0,2,6,6,0,7,0,8,0,0,2,10,0,1,10,10], //Moldova
              [6,2,12,12,8,5,13,0,2,12,12,4,5,8,11,12,8,2,0,0,1,10,8,0,12,1], //Sweden
              [1,0,3,2,3,3,0,0,0,12,2,0,0,2,0,0,8,5,2,0,0,3,2,0,4,0], //Hungary
              [22,22,1,7,19,0,7,2,17,9,11,6,24,22,3,18,19,14,22,17,16,0,15,13,10,9], //Israel
              [8,0,7,3,1,1,9,0,0,7,5,0,6,0,5,0,2,0,0,1,8,0,0,3,0,0], //Netherlands
              [0,6,0,4,8,0,1,3,13,0,15,7,1,14,4,12,2,1,0,4,3,0,4,0,0,5], //Ireland
              [4,20,14,7,1,12,7,5,8,10,9,20,3,8,6,7,7,15,13,16,7,2,11,17,0,8], //Cyprus
              [5,10,10,7,10,7,0,14,0,14,12,24,10,2,0,0,10,6,8,0,6,5,7,0,15,0], //Italy

            ];

            var svg = d3.select("svg"),
                width = +svg.attr("width"),
                height = +svg.attr("height"),
                outerRadius = Math.min(width, height) * 0.4 - 100,
                innerRadius = outerRadius - 20;

            var formatValue = d3.formatPrefix(",.0", 1e3);

            var chord = d3.chord()
                .padAngle(0.05)
                .sortSubgroups(d3.descending);

            var arc = d3.arc()
                .innerRadius(innerRadius)
                .outerRadius(outerRadius);

            var ribbon = d3.ribbon()
                .radius(innerRadius);

            var color = d3.scaleOrdinal()
                .domain(d3.range(4))
                .range(["#4B5320", "#50C878", "#98FB98", "#679267","#2E8B57","#043927", "#0B6623","#9DC183","#708238", "#C7EA46", "#3F704D","#00A86B","8F9779"]);

            var g = svg.append("g")
                .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
                .datum(chord(matrix));
				
			// Append the groups:
            var group = g.append("g")
                .attr("class", "groups")
              .selectAll("g")
              .data(function(chords) { return chords.groups; })
              .enter().append("g");

            group.append("path")
                .style("fill", function(d) { return color(d.index); })
                .style("stroke", function(d) { return d3.rgb(color(d.index)).darker(); })
                .attr("d", arc)
                .on("mouseover", highlightRibbons)
                .on("mouseout", showAllRibbons)

			group.append("text")
              .each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
              .attr("dy", ".35em")
              .attr("class", "titles")
              .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
              .attr("transform", function(d) {
                if (outerArcs = 0) {
                  width = 10;
                }
                return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
                + "translate(" + (outerRadius + 10) + ")"
                + (d.angle > Math.PI ? "rotate(180)" : "");
              })
              .text(function(d,i) { return names[i]; });	

			// Append the ribbons
            var ribbons = g.append("g")              // save the selection as a variable
                .attr("class", "ribbons")
              .selectAll("path")
              .data(function(chords) { return chords; })
              .enter().append("path")
                .attr("d", ribbon)
                .style("fill", function(d) { return color(d.target.index); })
				.attr("class","ribbon")
                .style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })
				.on("mouseover",highlightOneRibbon)
				.on("mouseout",showAllRibbons);
				
	
			// New Functions:
			// Highlight
			function highlightRibbons(d,i) {
				ribbons.filter(function(r) {
					    if(!(r.target.index == i || r.source.index == i)) return r;
					  })
					  .transition()
					  .style("opacity",0.1)
					  .duration(500);	
     				ribbons.filter(function(r) {
						if(r.target.index == i || r.source.index == i) return r;
					  })
					  .transition()
					  .style("opacity",1)
					  .duration(500);
			}
						
			// Unhighlight
			function showAllRibbons() {
				ribbons
				   //.transition()      // don't use for individual ribbons - transition is too long
				  .style("opacity",1)
				  //.duration(500);     // any visually effective transition will be too long given how quickly the mouse can cross multiple paths
			}
			
			// show a particular ribbon:
			function highlightOneRibbon() {
				ribbons.style("opacity",0.1);
				d3.select(this).style("opacity",1);
			}
			
            // Returns an array of tick angles and values for a given group and step.
            function groupTicks(d, i) {
              var k = (d.endAngle - d.startAngle) / d.value;
              return d3.range(0, d.value,0.2).map(function(v, i) {
                return {
                  angle: v * k + d.startAngle,
                  label: i*100 % 5 ? null : v,
                  id: i
                };
              });
            }
.group-tick line {
              stroke: #000;
            }

            .ribbons {
              fill-opacity: 1;

            }
<svg width="1000" height="1000"></svg>
            <script src="https://d3js.org/d3.v4.min.js"></script>

Upvotes: 2

Related Questions