d3 to create paths based on separate array of data

I am trying to create svg paths with d3 and trying to understand how can I ask d3 to create paths based on separate data, notebook

For example, here I have fixed data for path Value and two separate sets as index1 and index2. I want to create paths based on these, meaning I want to create 11 paths altogether, whereas the code creates only 8 paths.

My best guess is, d3 is probably overwriting the first 3 paths from index1 when it starts working on path2 as the first 3 paths were already available in the DOM.

How can I ask d3 to ignore the first 3 paths that are available in the DOM and start from scratch for index2? I don’t want to combine index1 and index 2. What is a d3 way to reach the desired output?

As an alternative, I can do this. But is this the only way?

index2.forEach(
    (a) => {
        svg.append('path')
            .attr('class', () => `ln${a}`)
            .attr('d', pathVal)
            .attr('stroke', () => { return `hsla(${Math.random() * 360} , ${((Math.random() + Math.random()/2)*100).toFixed(2)}%, 50%, 1)`; })
            .style('transform', () => { return `translateY(${a*multiplier}px)` })
            .attr('stroke-width', '2')
    }
)
1 Like

There are a few things I’d suggest. First off, an Observable tip: having one cell modify the contents of another is going to lead to trouble as you get more complex. In my notebook below I converted your cell to return an SVG element directly instead of having a separate “output” cell.

On to my recommended solutions. First, you could simply combine the two data arrays into one call to data. That way d3 doesn’t have a chance to overwrite the existing lines the second time. The core of that is a line like this.

d3.selectAll("path").data([...index1, ...index2]);

This side steps the problem though. To understand what’s going on here, lets look at what join does. Here is some documentation from selection.join / D3 | Observable

If the joining selection isn’t empty—as on subsequent iterations of the loop above— selection.join appends entering elements and removes exiting elements to match the data!

So in other words, the second time you call join, d3 will modify the existing elements you selected to match the new data set. To do this you would need to have different selectors for the two data sets. You could do this in several ways, including by putting each set into it’s own <g> element. In my notebook, I gave each line a class, either index1 or index2 and update the selectAll calls to only select elements with that particular class. Since the second selectAll won’t select the existing index1 elements, .join() won’t remove them to match the data.

2 Likes

Thanks for this.

Is it kindly possible to share the following

Oh, apologies, I linked to your notebook, not mine. Here’s the right link

2 Likes

Many thanks. It is educational to me and I like the 2nd approach as I can separately apply different transitions per index should I choose to.

Many thanks again.

I am stunned by this that we can distinguish the selector like this.

It does not have to be svg.selectAll('path.index1') or svg.selectAll('path.index2').
It could simply be anything - svg.selectAll('foo') or svg.selectAll('bar'). As far as I know, that d3.selectAll takes a valid CSS selector.

@mythmon do you know why it works as above?

svg.selectAll('foo')
    .data(index1)
    .join('path')
    //.classed('index1', true)
    .attr('class', (d) => `ln${d}`)
    .attr('d', pathVal)
    .attr('stroke', () => { return `hsla(${Math.random() * 360} , ${((Math.random() + Math.random()/2)*100).toFixed(2)}%, 50%, 1)`; })
    .style('transform', (d, i) => { return `translateY(${d*multiplier}px)` })
    .attr('stroke-width', '2')

svg.selectAll('bar')
    .data(index2)
    .join('path')
    //.classed('index2', true)
    .attr('class', (d) => `ln${d}`)
    .attr('d', pathVal)
    .attr('stroke', () => { return `hsla(${Math.random() * 360} , ${((Math.random() + Math.random()/2)*100).toFixed(2)}%, 50%, 1)`; })
    .style('transform', (d, i) => { return `translateY(${d*multiplier}px)` })
    .attr('stroke-width', '2')

This technically works for this case, but wouldn’t work if you try and update the visualization over time. One of the powers of d3’s selection model and the join function is that you can update the data over time and change the visualization to match by simply calling this same code again. That only works however if the selectAll actually selects the previous elements.

In this case however, the elements you add won’t match the selector. That’s ok for a one shot visualization. What happens is that svg.selectAll('foo') selects nothing (because no element has that tag), and then join appends elements for every data entry. Those elements don’t need to match the original selection (they should, but nothing enforces that). Then you are able to style the recently added elements since they are implicitly selected.

Sorry for stretching this one as I am trying my best to understand the side-effects before I turn this into a production code.

Will that not be exactly the same if I use svg.selectAll('path.index1'), cause it will not return anything as well or it would be significantly different than svg.selectAll('foo')

The result in this specific case won’t be different, since you are only rendering it once. To see the difference, consider this specific example from the join docs:

It needs to be able to reselect the previous elements so it can remove some of them, and update others. If it used a bogus selector like “foo”, then it could never reselect the already existing items, and it would simply add more and more line every time.

1 Like

Another possibility is to append a separate g element for each group of data and selectAll within that group:

1 Like

Anyone coming back to this link in the future
BogusSelectorSideEffect Gerardo Furtado explains it very well.

2 Likes