Yup, makes sense. Typically, you might do something like this:
const labels = g.selectAll('.label')
.join('g')
.attr('class', 'label')
.attr('transform', d => `translate(${d.x}, ${d.y})`) // assuming you have coordinates
labels.append('rect')
.attr('width', 100)
.attr('height', 100)
labels.append('text')
.text('Hello world!')
.style('fill', 'white') // Can't read black text against a black rectangle, which is the default fill
So here Iâm creating a group tag (âgâ) and appending a rectangle object to that, and then a text object. Both the rectangle and the text object are appended to the group tag, and since the text object is 2nd object appended, it renders on top of the rectangle.
SVG is a bit tricky in that you canât render any object within another object. So, this doesnât work:
svg.append('rect')
.attr('width', 100)
.attr('height', 100)
.append('text')
.text('hello world!')
That wouldnât render the text, because you canât put a text within a rectangle. Itâs a bit annoying, especially for labels, or if you want to, say, put an image within something.
There are a couple of exceptions to this rule, like with textPaths, tspans, and foreignObjects. Iâd say that definitively gets to the more expert (at least 200 level!) SVG usage, so if youâre just starting to learn D3, Iâd wait on getting to those concepts.
I wrote this notebook a while ago which converts a D3 chart to a SVG DOM Tree. I think this can be helpful for you in your study. When working with D3, itâs important to think about what the structure of the visualization in terms of the DOM. Take a look at that notebook and see how the visualization itself is structured. (It might surprise you how some of them are in fact very simple!)