2
//add circles with price data
svgContainer.selectAll("circle")
  .data(priceData)
  .enter()
  .append("svg:circle")
  .attr("r", 6)
  .style("fill", "none")
  .style("stroke", "none")
  .attr("cx", function(d, i) {
    return x(convertDate(dates[i]));
  })
  .attr("cy", function(d) { return y1(d); })

//add circles with difficulty data
svgContainer.selectAll("circle")
  .data(difficultyData)
  .enter()
  .append("svg:circle")
  .attr("r", 6)
  .style("fill", "none")
  .style("stroke", "none")
  .attr("cx", function(d, i) {
    return x(convertDate(dates[i]));
  })
  .attr("cy", function(d) { return y2(d); })

In the first half, circles with price data are added along the relevant line in the graph chart. Now I want to do the same with the second half to add circles with different data to a different line. However, the first circles' data are overwritten by the second circles' data, and the second circles never get drawn.

I think I have a gut feeling of what's going on here, but can someone explain what exactly is being done and how to solve the problem?

possible reference:

"The key function also determines the enter and exit selections: the new data for which there is no corresponding key in the old data become the enter selection, and the old data for which there is no corresponding key in the new data become the exit selection. The remaining data become the default update selection."

4 Answers 4

1

First, understand what selectAll(), data(), enter() do from this great post.

The problem is that since circle element already exists by the time we get to the second half, the newly provided data simply overwrites the circles instead of creating new circles. To prevent this from happening, you need to specify a key function in data() function of the second half. Then, the first batch of circles do not get overwritten.

//add circles with price data
svgContainer.selectAll("circle")
  .data(priceData)
  .enter()
  .append("svg:circle")
  .attr("r", 6)
  .style("fill", "none")
  .style("stroke", "none")
  .attr("cx", function(d, i) {
    return x(convertDate(dates[i]));
  })
  .attr("cy", function(d) { return y1(d); })

//add circles with difficulty data
svgContainer.selectAll("circle")
  .data(difficultyData, function(d) { return d; }) // SPECIFY KEY FUNCTION
  .enter()
  .append("svg:circle")
  .attr("r", 6)
  .style("fill", "none")
  .style("stroke", "none")
  .attr("cx", function(d, i) {
    return x(convertDate(dates[i]));
  })
  .attr("cy", function(d) { return y2(d); })
Sign up to request clarification or add additional context in comments.

2 Comments

If you take this approach you would want to add the key function to both the first and second half if you ever intend to update the page with new data.
I do not see why this answer has been chosen as the 'best'. Just use groups (as others have suggested). Using groups is also intuative when you think of the hierarchical structure of the DOM model. Using key function approach here is (in my view) a hack that makes the code more difficult to read at a later stage.
1

you can append the circles in two different groups, something like:

//add circles with price data
svgContainer.append("g")
      .attr("id", "pricecircles")
      .selectAll("circle")
      .data(priceData)
      .enter()
      .append("svg:circle")
      .attr("r", 6)
      .style("fill", "none")
      .style("stroke", "none")
      .attr("cx", function(d, i) {
        return x(convertDate(dates[i]));
      })
      .attr("cy", function(d) { return y1(d); })

//add circles with difficulty data
svgContainer.append("g")
  .attr("id", "datacircles")
  .selectAll("circle")
  .data(difficultyData)
  .enter()
  .append("svg:circle")
  .attr("r", 6)
  .style("fill", "none")
  .style("stroke", "none")
  .attr("cx", function(d, i) {
    return x(convertDate(dates[i]));
  })
  .attr("cy", function(d) { return y2(d); })

if the circles are in different groups they won't be overwritten

Comments

1

I had the same question as OP. And, I figured out a solution similar to tomtomtom above. In short: Use SVG group element to do what you want to do with different data but the same type of element. More explanation about why SVG group element is so very useful in D3.js and a good example can be found here:

https://www.dashingd3js.com/svg-group-element-and-d3js

My reply here includes a jsfiddle of an example involving 2 different datasets both visualized simultaneously on the same SVG but with different attributes. As seen below, I created two different group elements (circleGroup1 & circleGroup2) that would each deal with different datasets:

var ratData1 = [200, 300, 400, 600]; 
var ratData2 = [32, 57, 112, 293];

var svg1 = d3.select('body')
            .append('svg')
              .attr('width', 500)
              .attr('height', 400);

var circleGroup1 = svg1.append("g");
var circleGroup2 = svg1.append("g");

circleGroup1.selectAll("circle")
    .data(ratData1)
     .enter().append("circle")
    .attr("cy", 60)
    .attr("cx", function(d, i) { return i * 100 + 30; })
    .attr("r", function(d) { return Math.sqrt(d); });

circleGroup2.selectAll("circle")
    .data(ratData2)
  .enter()
  .append("circle")
  .attr("r", function(d, i){
        return i*20 + 5;
    })
    .attr("cy", 100)
    .attr("cx", function(d,i){ return i*100 +30;})
    .style('fill', 'red')
    .style('fill-opacity', '0.3'); 

Comments

0

What is happening is that you are:

In the FIRST HALF:

  1. getting all circle elements in the svg container. This returns nothing because it is the first time you're calling it so there are no circle elements yet.
  2. then you're joining to data (by index, the default when you do not specify a key function). This puts everything in the priceData dataset in the "enter" section.
  3. Then you draw your circles and everything is happy.

then, In the SECOND SECTION:

  1. You are again selecting generically ALL circle elements, of which there are (priceData.length) elements already existing in the SVG.
  2. You are joining a totally different data set to these elements, again by index because you did not specify a key function. This is putting (priceData.length) elements into the "update section" of the data join, and either:
    • if priceData.length > difficultyData.length, it is putting (priceData.length - difficulty.length) elements into the "exit section"
    • if priceData.length < difficultyData.length, it is putting (difficulty.length - priceData.length) elements into the "enter section"
      1. Either way, all of the existing elements from the first "priceData" half are reused and have their __data__ overwritten with the new difficultyData using an index-to-index mapping.

Solution?

  • I don't think a key function is what you are looking for here. A key function is way to choose a unique field in the data on which to join data instead of index, because index does not care if the data or elements are reordered, etc. I would use this when i want to make sure a single data set is correctly mapped back to itself when i do a selectAll(..).data(..).

  • The solution I would use for your problem is to group the circles with a style class so that you are creating two totally separate sets of circles for your different data sets. See my change below.

    • another option is to nest the two groups of circles each in their own "svg:g" element, and set a class or id on that element. Then use that element in your selectAll.. but generally, you need to group them in some way so you can select them by those groupings.

//add circles with price  data
svgContainer.selectAll("circle.price")
    .data(priceData)
    .enter()
    .append("svg:circle")
    .attr("class", "price")
    .attr("r", 6)
    .style("fill", "none")
    .style("stroke", "none")
    .attr("cx", function(d, i) {
        return x(convertDate(dates[i]));
    })
    .attr("cy", function(d) { return y2(d); })

//add circles with difficulty data
svgContainer.selectAll("circle.difficulty")
    .data(difficultyData)
    .enter()
    .append("svg:circle")
    .attr("class", "difficulty")
    .attr("r", 6)
    .style("fill", "none")
    .style("stroke", "none")
    .attr("cx", function(d, i) {
        return x(convertDate(dates[i]));
    })
    .attr("cy", function(d) { return y2(d); })

Using this method, you will always be working with the correct circle elements for the separate datasets. After that, if you have a better unique value in the data than simply using the index, you can also add a custom key function to the two .data(..) calls.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.