Problem
On the pragmatic side, your update function doesn't work as expected because each update you append two new circles to each g entered or updated with selectAll().join():
function updatea (){
// join g's
var miao = svg.selectAll('g.aaa').data(a).join('g')
.classed('aaa',true)
.attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
// append two circles to each g entered or updated:
miao.append('circle').attr('r', 10).attr('fill','red');
miao.append('circle').attr('r', 10).attr('cx', d=>d.end).attr('fill','red');
}
If you inspect the page you'll see two new circles appended each update. You're never updating the circles that are already on the page, just the translate on the g.
On the more theoretical side, you are unclear if your approach is most appropriate for binding data to complex symbols.
Solution
In proposing a solution let's consider the theoretical side first.
D3 was designed with data binding in mind. Normally in D3 you want to bind one datum per element (or one datum per element with a single child in each level of nesting). Where data is grouped and each datum is represented with multiple children, we would often see a second, nested, selectAll().data().join() statement.
However, if your visualization uses a symbol that is always comprised of the same set of child elements, then we don't need to do a nested join. In fact we do not need to in order to stay true to the data binding philosophy in D3: we'll bind one datum to one symbol (symbol in the data visualization sense).
This is the approach I'll propose here.
Rationale
This approach has advantages depending on situation, for example, there may be cases where the symbol's elements share parts of the datum (as in your case where d.start and d.end are both used to set the position of one of the sub-components) - splitting the datum into a new data array would be unnecessarily cumbersome. Changes in the symbol's representation/behavior/etc may require different parts of the datum as well, in which case it doesn't make sense to split the parent datum up.
Also, another reason why the proposed approach is attractive is that if you break the datum into smaller sub-components by using a nested selection:
svg.selectAll("g").data(data).enter().append("g")
.selectAll("circle").data(function(d) { return [d.start,d.end]; })
...
Or by flattening your array:
svg.selectAll("g").data([data[0].start,data[0].end,data[1].start,...])
...
It isn't as clear what child datum corresponds to what property when entering/updating your elements or what even what child datum corresponds to what parent datum. But also, say you dislike the symbol and now want a circle and rect, or two circles and a rect, then you need to substantially adjust the above approaches (perhaps by creating a fancy enter function that returns different types of shapes depending on index or on some identifier that tells you what symbol sub-component the datum corresponds to).
I believe attempting to create one unique datum per element is not ideal in this case, which is why I'm advocating for one datum per symbol.
Implementation
So, let's do one datum per symbol (where the symbols have child elements). This should be fairly easy to implement, I'll go over a simple method to do this here:
We can create the symbol in the join's enter function, and update it in the update function:
function updatea (){
a[0].start += 10*Math.sin(t);
a[0].end += 10*Math.cos(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
First, it's important to note, even though you've likely noticed, the parent datum is passed to child elements when using selection.append().
In the enter function passed to selection.join() We enter the g, style it as appropriate. Then we add the symbol sub-components and set their initial properties.
In the update function we update the overall position of the symbol and then the sub components.
Nothing occurs outside the join method in this case.
I cannot fork your observable without creating another account somewhere, so I'll just make a snippet of your example:
const svg = d3.select("body").append("svg")
.attr("width", 600)
.attr("height", 150);
var a = [{'start': 100, 'end': 200},{'start':100, 'end':200}];
var t = 0;
function updatea (){
a[0].start += 5*Math.sin(t);
a[0].end += 5*Math.cos(t);
a[1].start += 5*Math.cos(t);
a[1].end += 5*Math.sin(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
}
updatea();
setInterval(updatea, 100)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>