Search code examples
reactjsd3.jshoverdc.jscrossfilter

Use DC.js Canvas Scatter Hover Information to Update Another Chart


I would like to hover over a DC.js canvas chart, and use the hovered point to apply changes to another chart (I.e. display the information about that point in another chart).

The complications:

  • It's a canvas plot I'll be hovering
  • The whole thing needs to be done within the context of a virtual DOM

I'm guessing it will be a renderlet like this, but it's rather tricky as it's a canvas plot.

I'm assuming the fact that it's canvas will require me to use a hidden element, like in this block

I've put a Stackblitz up where the required behavior would be hovering the orange and green plot, and the other scatter is has some (any for this example) property changed for the respective group point


Solution

  • Cool! I'm glad you figured this out and published your answer. This will be helpful to others.

    Your instinct is right not to do expensive initialization inside an inner loop.

    It's fine to do work in the renderlet handler; that will get fired when the chart is rendered or redrawn, and that is when the data changes, so it makes sense.

    However, it's not a good idea to to an expensive calculation like calculating a quadtree inside of the mousemove listener, because that's interactive.

    It looks like the speed is completely fine, but I have a decent iMac, and the performance might be troublesome on a low-end machine.

    I would structure the code like this:

      eventScatterChart.on("renderlet", c => {
        // 0
        const xScale = c.x();
        const yScale = c.y();
        const data = c.group().all() // 1
            .filter(({value}) => value) // 2
            .map(({key}) => key); // 3
        // 4
        const overlayPos = c.select('.overlay').node().getBBox();
        const tree = d3
          .quadtree()
          .extent([
            [xScale.invert(overlayPos.x), yScale.invert(overlayPos.y)],
            [xScale.invert(overlayPos.width), yScale.invert(overlayPos.height)]
          ])
          .addAll(data);
    
        c.select(".overlay").on("mousemove.eventID", function(event) {
          // 5
          const cursorX = d3.pointer(event)[0];
          const cursorY = d3.pointer(event)[1];
    
          const closestPoint = tree.find(
            xScale.invert(cursorX),
            yScale.invert(cursorY)
          );
          console.log(closestPoint);
        });
      });
    
    1. The renderlet is a good place to do initialization. I also found that the x and y scale were not defined until the chart was drawn.

    2. Fetch the data from the group, not the dimension, so that it observes filters on other dimensions.

    3. When a point is "filtered out" there will still be an entry but its value is 0, so remove those. Hovering over hidden points should select the closest visible point.

    4. The key of the group is an array with x, y as first elements, since you defined the dimension key as

       const dimension = ndx.dimension(d => [
         d.changePointTime,
         d.photonCountsMean,
         d.transitionDirection === -1 ? "BG" : "Signal"
       ]);
      
    5. Initialize the quadtree exactly as before.

    6. Now the mousemove event handler is really simple and only doing lookups. Should be pretty efficient!

    I found that it didn't help CPU usage (still pegs one processor), but the numbers were flying by much faster in the browser console. If you want to further improve performance, you could throttle the mousemove events. (A browser can produce hundreds per second.)

    Fork of your stackblitz.