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.
Thanks!
1 Like
Update…
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:
- Create the desired svg with the math elements un-centered.
- 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.
- 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:
d3
.select(math_uncentered)
.selectAll("g.pretty-math")
.each(function (d) {
let bbox = this.getBBox();
d3.select(this).attr(
"transform",
`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!
Thanks!
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
svg
.selectAll("circle")
.data(somemath)
.join("circle")
.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 selection
s 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