1

I'm using d3 to display the data from a dataset. The display shows the three elements of a hierarchy:

  • Top level: "Organization-sustaining activities"
  • Middle level: "Extreme Sports"
  • Bottom Level: "Skydiving management"

When the DOM representing the middle level is clicked, the bottom level appears. When the middle level is clicked a second time, the bottom level disappears. This is all good.

The problem is that my users are always tempted to click the bottom level, and doing so disappears the bottom-level. I'd like to make it so that the bottom-level only disappears when the middle-level is clicked.

What I've tried:

I tried putting the event listener on the middle-level text element rather than the div. This led to the error d.key is not a function.

I also tried preceding on.click with div.parentNode and divs2.parentNode but got the message divs2.parentNode is undefined.

Here is my code

var doc = URL.createObjectURL(new Blob([`TooltipInfo Category Function1 Function2
Records relating to the skydiving.  Includes halters, parachutes, and altimeters.<ul><li>For records relating to rock climbing, see <b>rock climbing</b>.</li><li>For travel expenses, see <b>Procurements & Purchasing</b>.</li></ul>Retention:<ul><li>Keep records for seven years from the date of record creation, then send to <mark>archives.</mark></li><li>Keep all other records for seven years from the date of record creation, then destroy.</li></ul> • Skydiving Management Extreme Sports > Organization-sustaining Activities`]))


d3.tsv(doc)
  .row(function(d) {
    return {
      University: d.University,
      TooltipInfo: d.TooltipInfo,
      Searchterms: d.Searchterms,
      Category: d.Category,
      Function1: d.Function1,
      Function2: d.Function2,
      MaxRetentionRounded: d.MaxRetentionRounded,
      ModifiedRetention: d.ModifiedRetention
    };
  })
  .get(function(error, data) {

    var div = d3.select("body").append("div")
      .attr("class", "tooltip")
      .style("opacity", 0)

    var height = 150,
      width = 300;


    var nest = d3.nest()
      .key(function(d) {
        return d.Function2;
      })
      .key(function(d) {
        return d.Function1;
      })
      .key(function(d) {
        return d.Category;
      })
      .entries(data);


    var height = 80,
      width = 150;


    var divs = d3.select(".container")
      .selectAll(null)
      .data(nest)
      .enter()
      .append("div")
      .attr("class", "innerdiv");

    divs.append("p")
      .html(function(d) {
        return d.key;

      });



    var divs2 = divs.selectAll(null)
      .data(function(d) {
        return d.values;
      })
      .enter()
      .append('div')
      .attr("class", "first")
      .style("cursor", "pointer")
      .on("click", function(d, i) {
      const curColour = this.childNodes[1].attributes["height"].nodeValue;
      if (curColour == '0px') {
        d3.selectAll(this.childNodes).attr("height", "20px");
      } else if (curColour == '0') {
        d3.selectAll(this.childNodes).attr("height", "20px");
      } else {
        d3.selectAll(this.childNodes).attr("height", "0px");

      }
    });

    divs2.append("text")
      .attr('class', 'label1')
      .attr('x', 0)
      .attr('y', 0)
      .style("font-size", "21px")
      .text(function(d) {
        return d.key;
      })


    var svgs2 = divs2.selectAll(null)
      .data(function(d) {
        return d.values;
      })
      .enter()
      .append('svg')
      .attr("class", "second")
      .attr("height", 0)
      .attr("width", function(d) {
        return String(d3.select(this).value).length * 31.5
      })
    svgs2.append("text")
      .attr('class', 'label2')
      .attr('x', 10)
      .attr('y', 17)
      .style("font-size", "14px")
      .text(function(d) {
        return d.key;
      })
      .attr('text-anchor', 'start')
      .style("cursor", "pointer")
      .on("mouseover", function(d, i) {
        div.transition()
          .duration(200)
          .style("opacity", .9);
        div.html(d3.select(this).datum().values[0].TooltipInfo)

      })
      .on("mouseout", function(d) {
        div.transition()
          .duration(500)
          .style("opacity", 0);
      });

  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js"></script>

<div class="container"></div>

UPDATE: I've tried putting a 'Stop Propagation' on the childnode:

.on("mouseover", function(event) {
      event.stopPropagation();
      div.transition()
        .duration(200)
        .style("opacity", .9);
      div.html(d3.select(this).datum().values[0].TooltipInfo)

But it seems to stop the child's action (tooltip appearing) rather than the parents action (child disappearing).

UPDATE#2: stopPropagation doesn't seem to apply to mouseover, only to click. The following gives the behaviour I want (but I still need to figure out how to disappear the tooltip):

.on("click", function() {
      event.stopPropagation();
      div.transition()
        .duration(200)
        .style("opacity", .9);
      div.html(d3.select(this).datum().values[0].TooltipInfo)
oymonk
  • 427
  • 9
  • 27

2 Answers2

1

When you click on your bottom level SVG the browser tries to find a suitable handler for the resulting click event starting at the bottommost element at the mouse (more generally speaking, the pointer) position. If no handler is found on that element the browser will traverse the DOM tree upwards checking any enclosing—i.e. parent—elements for a registered handler until one handler is found or the root element has been reached. This process is called event bubbling and, if you are not familiar with it, you might want to spend some time digging into this concept as it will help to understand many misconceptions when it comes to JavaScript event handling. There are numerous resources to be found covering this topic, e.g.:

To stop the click event from bubbling up to the mid-level element causing it to toggle the bottom level element's visiblity you need to register a click handler on the bottom level element itself. In that handler you can use the event's stopPropagation method to prevent any further bubbling of the event.

.on("click", () => d3.event.stopPropagation());

Doing so, the handler of the mid-level element will not get executed if you click on the bottom level element while it is still reachable if you click on the mid-level element itself.

Have a look at the following working demo:

var doc = URL.createObjectURL(new Blob([`TooltipInfo Category Function1 Function2
Records relating to the skydiving.  Includes halters, parachutes, and altimeters.<ul><li>For records relating to rock climbing, see <b>rock climbing</b>.</li><li>For travel expenses, see <b>Procurements & Purchasing</b>.</li></ul>Retention:<ul><li>Keep records for seven years from the date of record creation, then send to <mark>archives.</mark></li><li>Keep all other records for seven years from the date of record creation, then destroy.</li></ul> • Skydiving Management Extreme Sports > Organization-sustaining Activities`]))


d3.tsv(doc)
  .row(function(d) {
    return {
      University: d.University,
      TooltipInfo: d.TooltipInfo,
      Searchterms: d.Searchterms,
      Category: d.Category,
      Function1: d.Function1,
      Function2: d.Function2,
      MaxRetentionRounded: d.MaxRetentionRounded,
      ModifiedRetention: d.ModifiedRetention
    };
  })
  .get(function(error, data) {

    var div = d3.select("body").append("div")
      .attr("class", "tooltip")
      .style("opacity", 0)

    var height = 150,
      width = 300;


    var nest = d3.nest()
      .key(function(d) {
        return d.Function2;
      })
      .key(function(d) {
        return d.Function1;
      })
      .key(function(d) {
        return d.Category;
      })
      .entries(data);


    var height = 80,
      width = 150;


    var divs = d3.select(".container")
      .selectAll(null)
      .data(nest)
      .enter()
      .append("div")
      .attr("class", "innerdiv");

    divs.append("p")
      .html(function(d) {
        return d.key;

      });



    var divs2 = divs.selectAll(null)
      .data(function(d) {
        return d.values;
      })
      .enter()
      .append('div')
      .attr("class", "first")
      .style("cursor", "pointer")
      .on("click", function(d, i) {
      const curColour = this.childNodes[1].attributes["height"].nodeValue;
      if (curColour == '0px') {
        d3.selectAll(this.childNodes).attr("height", "20px");
      } else if (curColour == '0') {
        d3.selectAll(this.childNodes).attr("height", "20px");
      } else {
        d3.selectAll(this.childNodes).attr("height", "0px");

      }
    });

    divs2.append("text")
      .attr('class', 'label1')
      .attr('x', 0)
      .attr('y', 0)
      .style("font-size", "21px")
      .text(function(d) {
        return d.key;
      })


    var svgs2 = divs2.selectAll(null)
      .data(function(d) {
        return d.values;
      })
      .enter()
      .append('svg')
      .attr("class", "second")
      .attr("height", 0)
      .attr("width", function(d) {
        return String(d3.select(this).value).length * 31.5
      })
    svgs2.append("text")
      .attr('class', 'label2')
      .attr('x', 10)
      .attr('y', 17)
      .style("font-size", "14px")
      .text(function(d) {
        return d.key;
      })
      .attr('text-anchor', 'start')
      .style("cursor", "pointer")
      .on("mouseover", function(d, i) {
        div.transition()
          .duration(200)
          .style("opacity", .9);
        div.html(d3.select(this).datum().values[0].TooltipInfo)

      })
      .on("mouseout", function(d) {
        div.transition()
          .duration(500)
          .style("opacity", 0);
      })
      .on("click", () => d3.event.stopPropagation());

  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js"></script>

<div class="container"></div>
altocumulus
  • 21,179
  • 13
  • 61
  • 84
-1

The answer is a combination of three different elements:

  1. Change your event listener from a mouseover to a click. This allows you to..
  2. Add stopPropagation on the child div.
  3. Once you've gotten rid of mouseover, you can no longer use 'mouseout' to turn off the tooltip. You'll need a conditional in your on.click function. See below for full example.

var doc = URL.createObjectURL(new Blob([`TooltipInfo Category Function1 Function2
Records relating to the skydiving.  Includes halters, parachutes, and altimeters.<ul><li>For records relating to rock climbing, see <b>rock climbing</b>.</li><li>For travel expenses, see <b>Procurements & Purchasing</b>.</li></ul>Retention:<ul><li>Keep records for seven years from the date of record creation, then send to <mark>archives.</mark></li><li>Keep all other records for seven years from the date of record creation, then destroy.</li></ul> • Skydiving Management Extreme Sports > Organization-sustaining Activities`]))


d3.tsv(doc)
  .row(function(d) {
    return {
      University: d.University,
      TooltipInfo: d.TooltipInfo,
      Searchterms: d.Searchterms,
      Category: d.Category,
      Function1: d.Function1,
      Function2: d.Function2,
      MaxRetentionRounded: d.MaxRetentionRounded,
      ModifiedRetention: d.ModifiedRetention
    };
  })
  .get(function(error, data) {

    var div = d3.select("body").append("div")
      .attr("class", "tooltip")
      .style("opacity", 0)

    var height = 150,
      width = 300;


    var nest = d3.nest()
      .key(function(d) {
        return d.Function2;
      })
      .key(function(d) {
        return d.Function1;
      })
      .key(function(d) {
        return d.Category;
      })
      .entries(data);


    var height = 80,
      width = 150;


    var divs = d3.select(".container")
      .selectAll(null)
      .data(nest)
      .enter()
      .append("div")
      .attr("class", "innerdiv");

    divs.append("p")
      .html(function(d) {
        return d.key;

      });



    var divs2 = divs.selectAll(null)
      .data(function(d) {
        return d.values;
      })
      .enter()
      .append('div')
      .attr("class", "first")
      .style("cursor", "pointer")
      .on("click", function() {
        const curColour = this.childNodes[1].attributes["height"].nodeValue;
        if (curColour == '0px') {
          d3.selectAll(this.childNodes).attr("height", "20px");
        } else if (curColour == '0') {
          d3.selectAll(this.childNodes).attr("height", "20px");
        } else {
          d3.selectAll(this.childNodes).attr("height", "0px");

        }
      }, false);

    divs2.append("text")
      .attr('class', 'label1')
      .attr('x', 0)
      .attr('y', 0)
      .style("font-size", "21px")
      .text(function(d) {
        return d.key;
      })

    var firstClick = 1;

    var svgs2 = divs2.selectAll(null)
      .data(function(d, e) {
        return d.values;
      })
      .enter()
      .append('svg')
      .attr("class", "second")
      .attr("height", 0)
      .attr("width", function(d) {
        return String(d3.select(this).value).length * 31.5
      })
    svgs2.append("text")
      .attr('class', 'label2')
      .attr('x', 10)
      .attr('y', 17)
      .style("font-size", "14px")
      .text(function(d) {
        return d.key;
      })
      .attr('text-anchor', 'start')
      .style("cursor", "pointer")
      .on("click", function() {
        event.stopPropagation()
        if (firstClick % 2 === 1) {
          div.transition()
            .duration(200)
            .style("opacity", .9)
          div.html(d3.select(this).datum().values[0].TooltipInfo)

          console.log(firstClick);
        } else {
          div.style("opacity", 0)
        }
        firstClick++;
      })

  })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div class="container"></div>
oymonk
  • 427
  • 9
  • 27