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

前端 未结 3 1878
感情败类
感情败类 2021-02-05 23:29

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 funct

相关标签:
3条回答
  • 2021-02-05 23:55

    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.

    0 讨论(0)
  • 2021-02-06 00:08

    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);
      
      }
      
    0 讨论(0)
  • 2021-02-06 00:11

    Let's see what this is defined as for each of your approaches:

    // console.log(this) in inline function:
    <svg width="960" height="960"> 
    
    // console.log(this) in function called from inline function:
    Window → file:///fileName.html
    

    this is set by how a function is called. D3 conveniently sets this to be the DOM element being manipulated by using .apply on the function passed to selection.attr(), selection.on() etc. However, it doesn't do this for functions called within the function passed to selection.attr(), selection.on(), etc.

    We can see that this is indeed the DOM element if we log this in the function passed to selection.on(). If this is not explicitly set, it will be the window (unless using strict mode, then it will be undefined). We can see in the nested function, this is indeed the window.

    Altocumulus's answer and Gerardo's answer avoid the this issue altogether, additionally you could also pass this as some regular argument to the function (this pattern is seen in some examples). But if you want to simply be able to copy and paste the code from the inline function to some separately defined function you can use apply, which will preserve this as the element being modified:

    d3.select("body")
      .append("svg")
      .attr("width", 960)
      .attr("height", 960)
      .on('mousemove', function() {
         var mouse = d3.mouse(this);
         console.log("inline: ", mouse[0]);
         someFunction.apply(this);
    });
    
    function someFunction() {
      var mouse = d3.mouse(this);
      console.log("not inline: ", mouse[0]);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

    If you needed to pass parameters to your function along with this, you can still use apply:

    someFunction.apply(this,[parameterA,parameterB,...]);
    function someFunction(parameterA,parameterB) { }
    

    d3.select("body")
      .append("svg")
      .attr("width", 960)
      .attr("height", 960)
      .on('mousemove', function() {
         var mouse = d3.mouse(this);
         console.log("inline: ", mouse[0]);
         someFunction.apply(this,[mouse[0],mouse[1]]);
    });
    
    function someFunction(x,y) {
      var mouse = d3.mouse(this);
      console.log("not inline: ", mouse[0],x,y);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

    However, this inline function calling other functions may just be extra work. If you are only calling a function in the inline function, then just pass the called function to selection.on() directly, this preserves this without any extra steps as d3 will apply the expected value to it (it also still gives you access to the datum and index if needed):

    d3.select("body")
      .append("svg")
      .attr("width", 960)
      .attr("height", 960)
      .on('mousemove', someFunction)
    
    function someFunction() {
      var mouse = d3.mouse(this);
      console.log(mouse[0]);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

    Don't place the brackets on the function in this case, we don't want to return the result of the function, we want to use the function itself.


    I've use apply (Function.prototype.apply()) in my examples, but you can also use call (Function.prototype.call()), as Altocumulus notes below. The use of call is quite similar. If you aren't passing any parameters to the function and only want to preserve this, the usage is the same: someFunction.apply(this) / someFunction.call(this). But, if passing parameters, call doesn't use an array for the parameters:

    d3.select("body")
      .append("svg")
      .attr("width", 960)
      .attr("height", 960)
      .on('mousemove', function() {
         var mouse = d3.mouse(this);
         someFunction.call(this,mouse[0],mouse[1]); // first parameter will be `this` for someFunction, rest of parameters follow
    });
    
    function someFunction(x,y) {
      var mouse = d3.mouse(this);
      console.log(mouse[0],x,y);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

    0 讨论(0)
提交回复
热议问题