The basic pattern you need to use are nested selections -- for each line, there are multiple circles. It's easier to do the lines and circles separately, lines and g
elements first:
var join = svg.selectAll("g")
.data(lineData);
// Enter
join.enter()
.append("g")
.append("path")
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
// Update
join.select("path")
.attr('d', line);
join.exit().remove();
The code is basically the same as yours, except that the appended g
elements aren't saved in a separate selection and the exit selection is handled by removing the elements. Now the circles, along the same lines:
var circles = join.selectAll("circle")
.data(function(d) { return d; });
circles.enter()
.append('circle')
.attr("r", 10)
.attr('fill', 'blue');
circles.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); });
circles.exit().remove();
The first line here is the nested selection -- for each element in the array that denotes the line, we want a circle. Note that this is operating on the update selection of the g
elements. This is ok because the elements in the enter selection are merged into the update selection when the g
elements are appended. That is, even though we only handle the update selection, any newly-appended elements are included in this.
After that, we handle the selections as usual. The enter selection has elements appended, the update selection sets the coordinates, the exit selection removes elements. All the magic happens in that first line, where we tell D3 to, for each g
element at the top level, bind each point from the line to any circles underneath.
Complete example here.