How to get screen x,y coords of clicked point in 3D scatter?

I have a custom control (a couple of nested divs with text, a button to perform an action and a pointy indicator) that I’d like to use in-place of the standard hover text in a 3D scatter chart. I want to emphasise the point being highlighted (and ultimately have a click action too), so I use a semi-transparent overlay div to dim the canvas, then place my control and a replacement circle indicator on top. All of the behaviour and appearance and ability to precisely position it is now as I’d like it, but I don’t have access to the screen coordinates of the point I want to label/highlight.

The ‘point’ object delivered by a click event describes the data (series number, point number within series) but tells me nothing about the on-screen position of the point. This must be known somewhere, for the default hover to correctly place the hoverinfo. Given I know a point, and the coordinates in data-space, how do I ask Plotly for the screen coordinates?

(I believe there’s a family of functions for the 2D charts for mapping to/from screen/data space - I’m hoping there’s a 3D equivalent, at least for the data->screen mapping in 3D charts).

Looking at the sources, I can see tantalising glimpses of what I think I need, but I can’t quite get it working:

plotly.js/src/plots/gl3d/project.js contains some matrix multiplication (but how to access it, and where to get the camera transform from?)

plotly.js/src/plots/gl3d/scene.js has some code that looks like it’s positioning the standard hover:

var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate);
...
Fx.loneHover({
    x: (0.5 + 0.5 * pdata[0] / pdata[3]) * width,
    y: (0.5 - 0.5 * pdata[1] / pdata[3]) * height,
    ...

I think elem._fullLayout.scene._scene.glplot.cameraParams is what I need as the first parameter to project(...), and selection.dataCoordinate is presumably just [x,y,z,1]. I’ve just copy/paste reimplemented project(...) in my own code, not entirely successfully…

Working from the 3D example (https://plot.ly/javascript/3d-scatter-plots/), I’ve added xformMatrix(m,v) and project(camera,v) and then:

var cameraParams = elem._fullLayout.scene._scene.glplot.cameraParams,
    point = [-2,4,-4,1],    // A point on the 'floor' of the default projection
    transformed = project(cameraParams, point);

var rect = elem._fullLayout.scene._scene.container.getBoundingClientRect(),
    width = rect.width,
    height = rect.height;

var coords = {
    x: (0.5 + 0.5 * transformed[0] / transformed[3]) * width,
    y: (0.5 - 0.5 * transformed[1] / transformed[3]) * height,
}

Unfortunately that 0.5 + 0.5 * transformed[0] / transformed[3] term evaluates to a value > 1.0, and so the computed x value is outside the view canvas.

I feel like the solution is tantalisingly close, but I can’t spot where the error is.

Now it looks as if there’s another transformation that I’m missing. With the mouse hovering over a point that reports the expected x, y, z in the default hoverinfo, the values stored in elem._fullLayout.scene._scene.glplot.selection.dataCoordinate differ.

The factor by which they differ varies with each axis, but seems consistent per-axis.

e.g.
For point A, .selection.dataCoordinate reports (-0.0547 -0.4067, 0.4289) when hoverinfo reports (-0.197, -1.694, 1.324), factors of (3.60, 4.17, 3.09)
For point B, .selection.dataCoordinate reports (-0.116, 0.593, 0.076) when hoverinfo reports (-0.418, 2.472, 0.234), also factors of (3.60, 4.17, 3.09)

I did wonder whether that was in some way related to the aspect ratios of the view, but that reports (1.0024, 1.1599, 0.859). What is this mysterious scaling factor?

I Chris, I came accross the same problem and I could solve it, although it is a hack that maybe works for me but not for you. Anyway, I’ll share it with you, Hope it works for you, buddy.

I “beauttified” the minimized version of the plotly.min.js library and on line 92268 I found what I was looking for (probably this line number won’t work for you but I gave you just in case it does). You have to find a function as follows in plotly’s code:

t.glplot.onrender = function(t) {
...
  var E = {
    points: [S]
  };
  t.fullSceneLayout.hovermode && f.loneHover({
    ...
  }),
  p.buttons && p.distance < 5 ? r.emit("plotly_click", E) ...
}

Pay attention to the “.glplot.onrender = function(” part, that’s what you need to look for, and then check that it has an object defined with a “E = {points: [S]}” and after that a call to “.loneHover”, followed by an ".emit(“plotly_click”," (as shown above, ignoring variable names, they may be different in your code, like those t, p, r, f, E and S).

Once you have found this spot the only thing you need to do is to add two new fields (“x” and “y”) to your “E” object, like follows:

var E = {
    points: [S],
    x: (.5 + .5 * m[0] / m[3]) * a + 10,
    y: (.5 - .5 * m[1] / m[3]) * o + 70
};

Once you’ve done that, your “plotly_click” even will receive, in addition to the “points” (x, y, z) values, the x and y screen coordinate, as shown below:

myPlot.on('plotly_click', function(data){
  var point = data.points[0],
      point_screen_x = data.x,  // <- - - these two guys are what you need!
      point_screen_x = data.y;  // <- |
      ...
});

Well, that’s it! hope it helps you, my friend. Happy hacking! :slight_smile:

1 Like