0

I've some code inside a selection.join() pattern:

const nodeWidth = (node) => node.getBBox().width;

const toolTip = selection
    .selectAll('g')
    .data(data)
    .join(
      (enter) => {

        const g = enter
          .append('g')

        g.append('text')
          .attr('x', 17.5)
          .attr('y', 10)
          .text((d) => d.text);
       
        let offset = 0;
        g.attr('transform', function (d) {
          let x = offset;
          offset += nodeWidth(this) + 10;
          return `translate(${x}, 0)`;
        });

        selection.attr('transform', function (d) {
          return `translate(${
            (0 - nodeWidth(this)) / 2
          },${129.6484} )`;
        });
      },

      (update) => {
        
        update
          .select('text')
          .text((d) => d.text);

        let offset = 0;
        update.attr('transform', function (d) {
          let x = offset;
          offset += nodeWidth(this) + 10;
          return `translate(${x}, 0)`;
        });

        selection.attr('transform', function (d) {
          return `translate(${
            (0 - nodeWidth(this)) / 2
          },${129.6484} )`;
        });
      }
    );

as you can see, in the enter and update section I need to call a couple of functions to calculate several nodes transformations. In particular, the code stores in the accumulation var offset the length of the previous text element. This properly spaces text elements (ie, text0 <- 10 px -> text1 <- 10 px -> ...).

As you can see, the "transform functions" in the enter and update section are identical. I'm trying to define them just in one place and call them where I need. E.g.,

   (update) => {
     update.attr('transform', foo);

     selection.attr('transform', bar);
    }

However, I cannot refactor the code this way because it looks like I cannot pass in neither the offset value nor this to the function passed to attr().

Is there a way to do it?

EDIT:

As per Gerardo Furtado's hint (if I got it right), you can define foo as follows:

const foo = function(d, i, n, offset) {
          let x = offset;
          offset += nodeWidth(n[i]) + 10;
          return `translate(${x}, 0)`;
}

then in the selection.join¡ you have to call foo this way:

   (update) => {
     let offset = 0;
     update.attr('transform', (d, i, n) => foo(d, i, n, offset));

    }

However, refactoring this way, offset is ever equal to 0. A possibile solution here: https://stackoverflow.com/a/21978425/4820341

floatingpurr
  • 7,749
  • 9
  • 46
  • 106
  • You use `selection.attr("transform"` for both the update and the enter: since you use this twice, you overwrite the transform set the first time. Also, as your offset value starts at 0 for the update and the enter selections, your update should be overtop your enter - am I missing something there? (eg: are you never entering new elements after the initial enter?) – Andrew Reid Jun 25 '21 at 15:51
  • 1
    Regarding the `this`, it's being passed, check again. Regarding `offset` that has nothing to do with D3, that's a Javascript feature: the scope of a function depends on where it is defined, not where you call it. – Gerardo Furtado Jun 26 '21 at 04:56
  • @GerardoFurtado yep, indeed the challenge is using the same function without repeating it – floatingpurr Jun 26 '21 at 08:27
  • @floatingpurr that's not a challenge at all, just pass `offset` as an argument. – Gerardo Furtado Jun 27 '21 at 09:41
  • @GerardoFurtado I might be wrong, but I cannot pass the offset this way inside attr(). If you have a working solution, please submit your answer. Happy to accept it. Thanx! – floatingpurr Jun 27 '21 at 10:25
  • 1
    It's just `update.attr('transform', (d, i, n) => foo(d, i, n, offset));`, and then `selection.attr('transform', (d, i, n) => foo(d, i, n, offset));`, etc. Inside `foo` you get `this` as `n[i]` and `offset` as `offset`, obviously. – Gerardo Furtado Jun 27 '21 at 10:47
  • Thanks @GerardoFurtado. Tried this way but I cannot modify the offset value in the scope where foo is called (more in the original post) – floatingpurr Jun 28 '21 at 16:01

1 Answers1

0

Have a look at Function.prototype.bind().

const doSomething = (d) => {
    return `translate(${
        (0 - nodeWidth(this)) / 2
    },${129.6484} )`;
}

Calling the function inside (enter) and (update)

selection.attr('transform', doSomething.bind(d));

This way the function gets executed in the current scope.

I guess this is what you are looking for. Please be aware that I could not test my code!

Wolfgang
  • 876
  • 5
  • 13
  • Working this way I get 'd is not defined'. Also the offset is not managed anymore. – floatingpurr Jun 25 '21 at 14:43
  • @floatingpurr sorry, my fault. cal() will actually call the function in the scope, you have to use .bind() which returns a function in the scope, because selection.attr() needs a function, not the return value. I updated my code. – Wolfgang Jun 25 '21 at 14:49
  • The offset makes this more complicated. This is why I chose the other duplicated method. You may have to introduce another function including the offet, but without testing this I may not be able to find a solution. Sorry. – Wolfgang Jun 25 '21 at 14:58
  • 1
    Bind will also result in d being undefined. selection.attr() already passes `d` (current datum) and `this` (current node) (and also passes other parameters) to the supplied function with Function.apply(), there is no need to use bind or call. The offset is the challenge. – Andrew Reid Jun 25 '21 at 15:03
  • @Wolfgang, no worries. However, still getting 'd is not defined' – floatingpurr Jun 25 '21 at 15:03
  • @AndrewReid, I did not know that `this` is passed. I believed that selection.attr() passes just `d`, and `i`(ie., datum and index) – floatingpurr Jun 25 '21 at 15:08
  • 1
    @floatingpurr,, `this` within the supplied function refers to the current node ([docs](https://github.com/d3/d3-selection#selection_attr)) of course `this` will be different when using an arrow function. I'll take a closer look at the question in a bit. – Andrew Reid Jun 25 '21 at 15:13