parallel-coordinates icon indicating copy to clipboard operation
parallel-coordinates copied to clipboard

How to highlight when mouse-over ?

Open rodrigob opened this issue 11 years ago • 24 comments

I am a new user of d3js and parallel coordinates, could you point me to an example on how to highlight a curve when touched by the mouse ?

rodrigob avatar Nov 20 '13 01:11 rodrigob

That's not available yet, since the library doesn't use SVG paths.

Two possibilities to add it:

  • SVG rendering could be added
  • Close curves could be found with a calculation similar to this intersection brushing. http://bl.ocks.org/syntagmatic/5441022

syntagmatic avatar Nov 20 '13 01:11 syntagmatic

What would it take to add such SVG rendering ?

rodrigob avatar Nov 20 '13 01:11 rodrigob

It would take quite a bit of refactoring, the Canvas element is tightly bound to the library.

I have no plans to do this work soon, but perhaps you could work from one of the D3 SVG parallel coordinates examples, like this one:

http://bl.ocks.org/mbostock/3709000

syntagmatic avatar Nov 20 '13 01:11 syntagmatic

Thank examples looks exactly like what I am looking for, but then that would mean not using d3.parcoords.js, right ?

rodrigob avatar Nov 20 '13 01:11 rodrigob

Yes, just use d3.js with that example.

I'll leave this issue open, since someone may address it eventually. Especially searching for the closest point in data, rather than hovering over a path. Then the rendering environment doesn't matter.

syntagmatic avatar Nov 20 '13 01:11 syntagmatic

Hi @syntagmatic - I'm also looking for a similar functionality. Maybe I can start writing the part for finding the closest point. I'm not sure how can I extract the data for the curves but if you show me how to get the curves I can start looking for finding the closest point to mouse.

EDIT: I think the link that you shared above is a good place to start: http://bl.ocks.org/syntagmatic/5441022

mostaphaRoudsari avatar Feb 19 '15 18:02 mostaphaRoudsari

Note: in many cases it's not just closest point, but closest points. E.g. look at this example: http://exposedata.com/parallel/. At some mouse locations you many lines will cross. I would suggest to highlight them all.

Additionally, it would be nice to have some kind of sensitivity parameter in the range [0..1] to allow for highlighting points which are not exactly under the mouse cursor. This will be more user friendly in cases where the plot is not dense at all.

bbroeksema avatar Feb 20 '15 07:02 bbroeksema

@mostaphaRoudsari That's right, the intersection brushing is a good place to start. You could think of a hover as making a small line segment as it goes, and that math will find all intersecting polylines.

@bbroeksema implemented intersection brushing as an interactive mode: https://github.com/syntagmatic/parallel-coordinates/blob/master/src/brushes/2D.strums.js

Example here. Change brush mode to 2D-strums.

syntagmatic avatar Feb 20 '15 10:02 syntagmatic

Hi @bbroeksema and @syntagmatic. Thank you for links. Here is a working sample.

It does work for single or multiple lines and I also added a tooltip that you can turn it off. Also I put a tolerance for detection. The only issue is that JavaScript is not my best and I couldn't really add it to your source code. I made compute_centroids public so I can find the points and from there it was easy to figure out. I left comments in the code in places that I think it can be improved. If you guys give me some hints I will be happy to add it to your code at some point.

image

image

Also this is the project that I really needed it for. It's slightly different as highlight is assigned to click and it has also a grid table. Thanks for your work. It's pretty useful.

mostaphaRoudsari avatar Feb 22 '15 07:02 mostaphaRoudsari

Thanks Mostapha. I started refactoring your example to see how it worked.

screen shot 2015-02-21 at 11 46 11 pm

syntagmatic avatar Feb 22 '15 07:02 syntagmatic

This library is really neat and useful! Can't wait for mouseOver to be implemented in the master source code - thank you all!

shakedk avatar May 07 '16 17:05 shakedk

@mostaphaRoudsari This is awesome. I'd love to experiment with it. I'm trying to figure out...besides making compute_centroids public, and using a slightly modified compute_centroids method, what other changes did you make? I'm trying to modify the newer version of d3.parcoords so I can experiment with this, but not sure what else needs changed. Thank you!

jmgelman avatar Jul 12 '16 01:07 jmgelman

@mostaphaRoudsari Thank you! I have the same problem as @jmgelman Can you help us? Need only public compute_centroids.

m1neral avatar Jul 22 '16 08:07 m1neral

@syntagmatic Do you have any plans to include this functionality in the master branch any time soon?

tbadams45 avatar Jul 25 '16 17:07 tbadams45

It has been a while since I made this changes and I don't remember all the details. You can run a git differ between the codes to see the differences.

In general if you want to spend some time to integrate it into parallel coordinates I think you should try to add it to the main script without making compute_centroids public.

Also be aware of another limitation of the method which is reported by Jianlong Zhou. It won't work if one smooth the curves. The math is written for straight lines.

I am using d3.parcoords.js to visualize my data. I found your example with mouse over highlighting enhancement is very impressive. I am trying to use your example in my data: http://bl.ocks.org/mostaphaRoudsari/b4e090bb50146d88aec4

However, I found that your example does not support the d3.parcoords.js's option "smoothness", even I set different smooth values and statements as follows to set up smooth curves, it does not work: // smoothness d3.select("#smoothness").on("change", function() { d3.select("#smooth").text(this.value); graph.smoothness(this.value).render(); });

mostaphaRoudsari avatar Jul 27 '16 18:07 mostaphaRoudsari

@syntagmatic What do you think about my PR? This is the minimum that is needed from library to highlight lines on chart and render tooltips in intersection points (the algorithm @mostaphaRoudsari , but with solid refactoring). Maybe need to add something?

m1neral avatar Jul 27 '16 19:07 m1neral

Thanks @m1neral, I've merged the request

syntagmatic avatar Aug 03 '16 21:08 syntagmatic

Hey @syntagmatic have the changes been merged yet? Or should we stick with Mostapha's implementation. http://bl.ocks.org/mostaphaRoudsari/b4e090bb50146d88aec4

foxbat07 avatar Aug 07 '16 21:08 foxbat07

@mohithingorani Merged the change here to expose compute_real_centroids: https://github.com/syntagmatic/parallel-coordinates/pull/318

syntagmatic avatar Aug 08 '16 05:08 syntagmatic

@mostaphaRoudsari the working example does not work failing to find dependent files. It is late follow up but wondering if someone can help me to get this capability please. I really need this clickable capability for my project. Thank you!

lee1043 avatar Sep 17 '18 21:09 lee1043

Note for future followers: @mostaphaRoudsari fixed the working example. Thank you @mostaphaRoudsari

lee1043 avatar Oct 08 '18 21:10 lee1043

As of Sep 2020, this example fails with: Uncaught TypeError: d3.parcoords is not a function. Is anyone aware of a working example of this code?

onejgordon avatar Sep 06 '20 20:09 onejgordon

@onejgordon, see here for a different option: https://observablehq.com/@vega/vega-lite-parallel-coordinates

mostaphaRoudsari avatar Sep 29 '20 14:09 mostaphaRoudsari

I needed to make this example to work. But Only the hover functionality (without the points).

I will share the functions that I used modified to work with d3 v6 and latest version of parcoord es. Maybe someone else needs it.

Sorry I didn't reproduce the exact example, but my code is a little different (and more complex), and I couldn't find an playground with the parcoords set up in order to put it there.

But replacing the functions below in the example should work (if any error comes up, removing the code regarding the points might be required, I am not sure since I deleted that part in my code).

Here is my code:

  //for highlight when hover over line
  let timeOut;
  //add hover event
  d3.select("#parallel-coordinates-chart-container svg")
    .on("mousemove", event => {
      timeOut && clearTimeout(timeOut);
      timeOut = setTimeout(highlightLineOnClick(event), 100);
    })
    .on("mouseout", () => {
      timeOut && clearTimeout(timeOut);
      parcoord.unhighlight();
    });

  const getActiveData = () => {
    if (parcoord.brushed() != false) return parcoord.brushed();
    return parcoord.data();
  }


  // Add highlight for every line on click
  function getCentroids() {
    const margins = parcoord.margin();
    const brushedData = parcoord.brushed().length ? parcoord.brushed() : data;

    return brushedData.map(d => {
      const centroidPoints = parcoord.compute_real_centroids(d);
      return centroidPoints.map(d => [ d[ 0 ] + margins.left, d[ 1 ] + margins.top ]);
    });
  }

  const highlightLineOnClick = (mouseClick) => {
    let clicked = [];
    // let clickedCenPts = [];

    let clickedData = getClickedLines(mouseClick);

    if (clickedData && clickedData[ 0 ].length != 0) {

      clicked = clickedData[ 0 ];
      // clickedCenPts = clickedData[ 1 ];

      // highlight clicked line
      parcoord.highlight(clicked);

    }
  };


  const findAxes = (testPt, cenPts) => {
    // finds between which two axis the mouse is
    let x = testPt.offsetX;
    let y = testPt.offsetY;

    // make sure it is inside the range of x
    if (cenPts[ 0 ][ 0 ] > x) return false;
    if (cenPts[ cenPts.length - 1 ][ 0 ] < x) return false;

    // find between which segment the point is
    for (let i = 0; i < cenPts.length; i++) {
      if (cenPts[ i ][ 0 ] > x) return i;
    }
  }

  const getClickedLines = (mouseClick) => {
    let clicked = [];
    let clickedCenPts = [];

    // find which data is activated right now
    let activeData = getActiveData();

    // find centriod points
    let graphCentPts = getCentroids(activeData);

    if (graphCentPts.length == 0) return false;

    // find between which axes the point is
    let axeNum = findAxes(mouseClick, graphCentPts[ 0 ]);
    if (!axeNum) return false;

    graphCentPts.forEach(function (d, i) {
      if (isOnLine(d[ axeNum - 1 ], d[ axeNum ], mouseClick, 2)) {
        clicked.push(activeData[ i ]);
      }
    });

    return [ clicked, clickedCenPts ]
  }

  function isOnLine(startPt, endPt, testPt, tol) {
    // check if test point is close enough to a line
    // between startPt and endPt. close enough means smaller than tolerance
    let x0 = testPt.offsetX;
    let y0 = testPt.offsetY;
    var x1 = startPt[ 0 ];
    var y1 = startPt[ 1 ];
    var x2 = endPt[ 0 ];
    var y2 = endPt[ 1 ];
    var Dx = x2 - x1;
    var Dy = y2 - y1;
    var delta = Math.abs(Dy * x0 - Dx * y0 - x1 * y2 + x2 * y1) / Math.sqrt(Math.pow(Dx, 2) + Math.pow(Dy, 2));
    if (delta <= tol) return true;
    return false;
  }

berci-i avatar May 07 '21 10:05 berci-i