Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.select(this) works on mouseover, but not on function called in mouseover

I am new to javascript and currently struggling with selecting the this object while trying to do a d3 selection. I've made the following example, with a function I'm calling, and an on mousemove event:

function changeFont() {
  d3.select(this)
    .attr('font-size', '2em')
}

...
.on('mousemove', function() {
  var mouse = d3.mouse(this);
  var xVal = mouse[0];

  // this would work, but not when its called in a function
  // d3.select(this)
  //  .attr('font-size', '2em')

  // this works
  d3.select(this)
   .attr("opacity", 1)

  // this doesnt
  changeFont()
});

In my main script not shown here, I am organizing my code by writing functions that handle each of the mousemove, mouseover, etc. effects. However because of these functions, I am running into this problem where I can't do d3.select(this) inside of that mouseover function... Any thoughts on what I should be doing differently?

Should I pass this as a parameter to my changeFont() function? Or should I access this in a different way?

Thanks!

like image 613
Canovice Avatar asked May 03 '18 17:05

Canovice


2 Answers

Just for completeness, since this question has already two very good answers: you can avoid the confusion with this if you use the third and second arguments combined. That's something that even D3 developers forget eventually.

In several D3 methods, the current DOM element is just the current index of the nodes' group. So, in the anonymous function...

.on("mouseover", function(_, i, n) {

... this is just n[i], which you can just pass to the other functions. The _ here corresponds to the first argument, the datum: I'm using _ just to follow the convention that shows this argument is not used.

The nice thing about this approach is that you can even (for whatever reasons you have) use arrow functions:

d3.select("body").selectAll(null)
  .data(["foo", "bar", "baz"])
  .enter()
  .append("p")
  .text(String)
  .on("mouseover", (_, i, n) => {
    changeFont(n[i])
  });

function changeFont(element) {
  d3.select(element).style("font-size", "2em")
}
<script src="https://d3js.org/d3.v5.min.js"></script>

Of course, you can't get the DOM element using this inside the arrow function.

like image 95
Gerardo Furtado Avatar answered Sep 20 '22 17:09

Gerardo Furtado


Although Andrew's answer might be the best fit if you take the question literally, I would like to add my two cents to it. Your real problem does not seem to be to get a hold of this, but to repeatedly get access to that element to apply you manipulations. Since fiddling around with this can be a pain in JavaScript it might be worth taking a slightly different approach by directly passing the selection instead. This will also improve performance as there is no need to re-select this over and over again.

First, let us slightly refactor your changeFont() function to accept a selection object.

function changeFont(selection) {
  selection
    .attr('font-size', '2em');
}

Note, how this makes the function more generally applicable as it does not make any assumptions about the selection passed into it. It could be your d3.select(this), a selection containing multiple elements or any other D3 selection object. Additionally, you do not need to preserve the previous this scope.

There are basically two ways of calling this function.

  1. The obvious one will directly pass the selection as an argument when calling the function:

    const d3This = d3.select(this);
    changeFont(d3This);
    
  2. Fortunately, there is a more elegant way of doing it by resorting to D3's own selection.call() which even allows for method chaining if you need to do multiple calls on the same selection.

    function changeFont(selection) { selection.attr("font-size", "2em"); }
    function changeFill(selection) { selection.attr("fill", "limegreen"); }
    function changeOpacity(selection) { selection.attr("opacity", "0.1"); }
    
    // ...
    .on("mouseover", function() {
      // Call the functions for this element.
      d3.select(this)
        .call(changeFont)
        .call(changeFill)
        .call(changeOpacity);
    
      // Instead, you could also apply the same pattern to all texts.
      d3.selectAll("text")
        .call(changeFont)
        .call(changeFill)
        .call(changeOpacity);
    
    }
    
like image 40
altocumulus Avatar answered Sep 21 '22 17:09

altocumulus