repositioning objects according their BBox dimensions

This question is a follow up to this one: getting width of element created by `tex2svg`

Once you’ve rendered an SVG and used d3 plus getBBox to get the height and width of some elements in the SVG, how can you then use that data to modify positions in the SVG? I’ve applied the standard selection.join() pattern to the problem and failed.

I suspect that the problem is that I don’t actually understand selection.join()?

I’ve laid out the question and shown my failed attempt in this notebook.


1 Like


I think the problem is rooted in cell reactivity.

Because…First I’m creating a cell that renders an svg containing MathJax math. Then I’m creating another cell that reads the position of the math in the previous cell, and then adjusts the position accordingly. But then of course that causes the position in the first cell to update, which in turn causes the second cell to run again in response to the new position…ad infinitum…Who knows where that math is going to end up!

So how to fix this?

The only thing I can come up with is really gross:

  1. Create the desired svg with the math elements un-centered.
  2. Run code in a second cell that collects the x and y positions and the widths and heights of the math elements from the svg created in (1). Store those values in a variable.
  3. Re-create the same svg again in a third cell, but this time feed it the data collected from (2) to center the math in this (second version of) the svg.

This is gross because it requires you to create and display duplicates of the same svg (except one is janky because its math is uncentered).

Is there a better way???

@mcmcclur I’d love your perspective on this.

Hi @stuvjordan-uroc - Sorry for the delay; I’m travelling and only just now found a moment.

I’m not sure why you’re computing the bboxes in one cell, then re-rendering the whole image in another. I think you can just compute then, modify. Something like so:

  .each(function (d) {
    let bbox = this.getBBox();
      `translate(${d.x - bbox.width / 2} ${d.y - bbox.height / 2})`

You also could also yield the result then modify it all in one cell. Both approaches are illustrated in this notebook

1 Like

@mcmcclur I knew you’d come through on this! Thank you!!!

FWIW, could you explain one detail about how the “yield in one cell” method you demonstrate in your notebook works?

Specifically, midway down the cell you do yield svg.node(). I know (from your fantastic response to another forum post) that this transforms the D3 selection svg into instructions to the browser/DOM resulting in html rendered in the cell. (Of course, correct me if I’m wrong about that!)

But I don’t understand why the block of code immediately after the yield (i.e. pretty_math.each…etc.) works. In my brain, the variable pretty_math at that moment in the code points to a D3 selection. And in my brain, any given selection is not the same thing as DOM nodes returned by selection.node() nor is it the same thing as the actually-rendered HTML resulting from yield selection.node(). So how would the correct information about the rendered widths and heights of the math nodes be returned when the function called by each does this.getBBox()? At that point in the code, this (I think) points to the the selection prettymath, not to the nodes returned by prettymath.node() or the HTML rendered into the cell by the previous lines.

Now, as I write all that out, I guess the answer must be something like…“rendering a selection with return selection.node() or yield selection.node() has the side-effect of updating the data referenced by the selection. Specifically, when you return selection.node(), d3 renders the selection, collects all the data about the rendered html (such as the now-populated objects returned by node.getBBox()) and updates the object in memory pointed to by selection with that new data.

Is that correct? What’s the full and correct story about how return selection.node() or yield selection.node() alters selection?

Feel free to just point me to a book or documentation I should read!


1 Like

Yes, that’s exactly right. They’re connected, though, so that changes in one are reflected in the other. In particular, the process of rendering DOM elements by the browser sets certain attributes of those elements (like size and position), which then affects the selection. I honestly don’t know the internal details well enough to elaborate beyond that, though.

I guess you’re saying here pretty much what I said above. The one thing that I would say, though, is that the phrase “data referenced by the selection” typically refers specifically to the data bound to the selection by the data method. That is, when we write

  .attr("cx", (d) => d.x)

we’ve bound the array somemath to the selection. That’s what allows us to specify attributes via a function, as in d => d.x in the specification of the "cx" attribute.

1 Like

ah…yes, thanks! Better to say “return selection.node() and yield selection.node() have the side effect of updating selection with any relevant (hopefully!) information about the rendered nodes”!

And now that I think about it…I suppose this is the case for any changes that occur in the DOM no matter the source. I.e. whenever the DOM changes, no matter the source of those changes, all selections pointing to any parts of the DOM are updated accordingly. DOM changes induced by yield or return selection.node() are just a special case! That automatic bi-direction information (not “data”!) flow between DOM and selections is a foundational aspect of D3’s behavior.

(No need to reply unless I’m wrong about any of that!)

1 Like