Question

I'm using the line-with-focus chart ( View Finder ) example in nvd3. That means there's 3 or 4 lines ( series ) being drawn on the graph. When i hover over any of the lines I want to get back all the y-values for all lines of that given x-axis position ( for the most part these will be interpolated y-values per line ).

I see in the nv.models.lineWithFocusChart source code that using a callback for the elementMouseover.tooltip event I can get my data's x-value back for the data points on the line.

The closest part of the source code that does what i want is with the interactiveGuideline code for the lineChart examples. However, i don't want to create a <rect> overlay with elementMousemove interaction. I think i can modify this code to filter my data and get each line's y-value, but I'm sure there's an easier way I'm not seeing.

I think I'm on the right track, but just wondering if someone had this need before and found a quicker route than the rabbit hole I'm about jump in.

Thanks for feedback

Was it helpful?

Solution

This is the basic functionality you're looking for, it still needs a bit of finesse and styling of the tooltips. (Right now the tooltip blocks the view of the points...)

Key code to call after the drawing the chart in (for example, within the nv.addGraph function on the NVD3 live code site):

 d3.selectAll("g.nv-focus g.nv-point-paths")
    .on("mouseover.mine", function(dataset){

      //console.log("Data: ", dataset);

      var singlePoint, pointIndex, pointXLocation, allData = [];
      var lines = chart.lines;

      var xScale = chart.xAxis.scale();
      var yScale = chart.yAxis.scale();
      var mouseCoords = d3.mouse(this);
      var pointXValue = xScale.invert(mouseCoords[0]);

      dataset
          .filter(function(series, i) {
            series.seriesIndex = i;
            return !series.disabled;
          })
          .forEach(function(series,i) { 
              pointIndex = nv.interactiveBisect(series.values, pointXValue, lines.x());
              lines.highlightPoint(i, pointIndex, true);

              var point = series.values[pointIndex];

              if (typeof point === 'undefined') return;
              if (typeof singlePoint === 'undefined') singlePoint = point;
              if (typeof pointXLocation === 'undefined')
                pointXLocation = xScale(lines.x()(point,pointIndex));

              allData.push({
                  key: series.key,
                  value: lines.y()(point, pointIndex),
                  color: lines.color()(series,series.seriesIndex)
              });
          }); 


      /*
      Returns the index in the array "values" that is closest to searchVal.
      Only returns an index if searchVal is within some "threshold".
      Otherwise, returns null.
      */
      nv.nearestValueIndex = function (values, searchVal, threshold) {
            "use strict";
            var yDistMax = Infinity, indexToHighlight = null;
            values.forEach(function(d,i) {
               var delta = Math.abs(searchVal - d);
               if ( delta <= yDistMax && delta < threshold) {
                  yDistMax = delta;
                  indexToHighlight = i;
               }
            });
            return indexToHighlight;
      };

     //Determine which line the mouse is closest to.
     if (allData.length > 2) {
            var yValue = yScale.invert( mouseCoords[1] );
            var domainExtent = Math.abs(yScale.domain()[0] - yScale.domain()[1]);
            var threshold = 0.03 * domainExtent;
            var indexToHighlight = nv.nearestValueIndex(
              allData.map(function(d){ return d.value}), yValue, threshold
            );
            if (indexToHighlight !== null)
              allData[indexToHighlight].highlight = true;
                //set a flag you can use when styling the tooltip
      }


      //console.log("Points for all series", allData);

      var xValue = chart.xAxis.tickFormat()( lines.x()(singlePoint,pointIndex) );

      d3.select("div.nvtooltip:last-of-type")
        .html(
          "Point: " + xValue + "<br/>" +

          allData.map(function(point){
            return "<span style='color:" + point.color + 
              (point.highlight? ";font-weight:bold" : "") + "'>" + 
              point.key + ": " + 
              chart.yAxis.tickFormat()(point.value) +
              "</span>";
          }).join("<br/><hr/>")
        );

    }).on("mouseout.mine", function(d,i){ 
          //select all the visible circles and remove the hover class

      d3.selectAll("g.nv-focus circle.hover").classed("hover", false);
    });

The first thing to figure out was which objects should I bind the events to? The logical choice was the Voronoi path elements, but even when I namespaced the event names to avoid conflict the internal event handlers nothing was triggering my event handling function. It seems that a parent <g> event captures the mouse events before they can reach the individual <path> elements. However, it works just fine if instead I bind the events to the <g> element that contains the Voronoi paths, and it has the added benefit of giving me direct access to the entire dataset as the data object passed to my function. That means that even if the data is later updated, the function is still using the active data.

The rest of the code is based on the Interactive Guideline code for the NVD3 line graphs, but I had to make a couple important changes:

  • Their code is inside the closure of the chart function and can access private variables, I can't. Also the context+focus graph has slightly different names/functionality for accessing chart components, because it is made up of two charts. Because of that:

    • chart in the internal code is chart.lines externally,

    • xScale and yScale have to be accessed from the chart axes,

    • the color scale and the x and y accessor functions are accessible within lines,

    • I have to select the tooltip instead of having it in a variable

  • Their function is called with custom event as the e parameter that has already had the mouse coordinates calculated, I have to calculate them myself.

  • One of their calculations uses a function (nv.nearestValueIndex) which is only initialized if you create an interactive layer, so I had to copy that function definition into mine.

I think that about covers it. If there's anything else you can't follow, leave a comment.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top