0

Using d3.v3.min.js and the example D3 Zoomable Plot as reference I built a scatter plot. The most relevant bits are:

var svg = d3.select("#scatter1")
    .append("svg")
    .attr("width", outerWidth)
    .attr("height", outerHeight)
    .attr("viewBox", "0 0 " + (width + margin.left + margin.right) + " " + (height + margin.top + margin.bottom))
    .attr("preserveAspectRatio", "xMidYMid meet")
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

var objects = svg.append("svg")
    .classed("objects", true)
    .attr("width", width)
    .attr("height", height);

objects.selectAll(".dot")
    .data(values)
    .enter().append("circle")
    .classed("dot", true)
    .attr("r", function (d) {
        return 6 * Math.sqrt(2.0 / Math.PI);
    })
    .attr("transform", transform)
    .style("fill", colorVal);

and some needed css:

.dot {
  fill-opacity: .5;
}

.dotHidden {
  fill-opacity: .1;
}

My data values are defined as:

var values = [[x0, y0], [x1, y1], ....];   

with a custom property idx per element i.e.

values[i].idx === i 

Now I'd like to highlight only a subset of the dot elements, namely only those that are in an indexes list e.g. selectedIdx = [4, 8, 10, 38, 90]. How can I do that in the fastest possible way? is there an d3 js idiomatic way to do it? I have attempted to do this below and it does the job to "hide" all the dots but I actually want to hide only the ones not in the selectedIdx.

d3.select("#scatter1 svg")
  .selectAll("circle")
  .attr("class", "dotHidden");

UPDATE: This is a follow up of the question How to get the index of the data element in a histogram on mouseover?

SkyWalker
  • 13,729
  • 18
  • 91
  • 187
  • You can filter the elements not selected like this: `d3.select("#scatter1 svg") .selectAll("circle") .filter(function(d, i){ return selectedIdx.indexOf(i) === -1; }).attr("class", "dotHidden");` (You can also use a dictionary for the selectedIdx to avoid indexOf(i)) or instead of using i, use d.idx if that fits better. – mkaran Jun 07 '17 at 09:57
  • Yes that's a way to do it but not very elegant. Is there a way to directly select only those circles that ocurr in the `selectedIdx` array? – SkyWalker Jun 07 '17 at 10:00
  • 2
    You can use a tertiary operator with indexOf. However, if you don't provide a [MCVE] we can test, it'd be difficult to get a proper solution. Also, have in mind that *elegant* is quite opinion-based. – Gerardo Furtado Jun 07 '17 at 10:14
  • @altocumulus Yep good detective work! :D – SkyWalker Jun 07 '17 at 13:04

2 Answers2

2

Use classed. You need to cycle through all elements only once, then. Plus your code gets more robust because you make sure that dotHidden class is unset for all circles in selectIdx.

d3.select("#scatter1 svg")
  .selectAll("circle")
  .classed("dotHidden", function(d, i) {
    return selectIdx.indexOf(i) === -1;
  });

If you‘re running ES2015 or use a transpiler you can improve the callback:

.classed("dotHidden", (d, i) => selectIdx.indexOf(i) === -1);
undko
  • 927
  • 8
  • 15
  • Good but I wanted something better than `indexOf`I believe there should be a way to tell D3 JS (idiomatically) that I am updating some circles even if having to compute the set difference first. – SkyWalker Jun 07 '17 at 15:32
  • Ah, I see. I suppose `selectIdx` changes over time by some user intreaction while the complete data set (`values`?) doesn‘t - is that right? – undko Jun 08 '17 at 05:58
  • Correct! this is a one way interaction between a histogram (all points inside a bin) and two scatter plots to "highlight" which points belong to the histogram bin. – SkyWalker Jun 08 '17 at 07:06
  • A slightly faster version using http://underscorejs.org/#indexOf is `.filter(function(d, i){return _.indexOf(selectedIdx, i, true) === -1; })` namely it does a binary search i.e. O(n log n) and not a linear search ending in O(n^2), now I am just looking whether I always get the selectedIdx sorted or was mere coincidence ... – SkyWalker Jun 08 '17 at 11:55
1

Once you have the selected data (not the indexes) you can use data binding capabilities of d3.js, in particular selection.exit():

d3.select("#scatter1 svg")
  .selectAll("circle")
  // make sure to reset all circles to normal
  .classed("dotHidden", false);  
  .data(selectedValues)
  // narrow selection to values that are not in selectedValues
  .exit()
  .classed("dotHidden", true);
undko
  • 927
  • 8
  • 15
  • I believe there is possibility to pass a second parameter to `.data(selectedValues)`i.e. the keys to match and using the indexes there would make a faster much more efficient solution than matching by arrays of Double values ... – SkyWalker Jun 08 '17 at 09:40
  • You could in fact define a key function that sets a nonexisting key for every datum not in `selectIdx`, exit selection would be filled with them. Still I‘m not sure how you want to avoid calling `selectIdx.indexOf()`, you could IMO just shift it around. – undko Jun 08 '17 at 11:00