Plotly:双击放大图中的空白 space 会在重置轴时选择新点

Plotly: double-clicking on empty space in a zoomed-in plot selects new point(s) as it resets axes

我有一个散点图,其中点密集地聚集在一起。放大其中一些后,双击空 space 重置轴,这就是我想要的。但在许多情况下,它也 select 是一个新点——这不是我想要的。毕竟我双击空space。我无意 select 提出新观点。

问题似乎是双重的。首先,each double click is also registered as a single click。其次,单击是在 post-axis-reset 坐标处注册的——而不是我双击时实际看到的坐标。 post-axis-reset 坐标映射到标绘点,即使我在放大时单击空 space。我该如何解决这个问题?

这是一个最小的插图。 Plotly 图是从 R 生成的,但这似乎并不重要:

library(plotly)
x <- c(rnorm(3000, 0, 3), rnorm(1000, 0, 0.2))
y <- c(rnorm(3000, 0, 3), rnorm(1000, 0, 0.2))
groups <- rep(c("a", "b", "c", "d"), 1000)
myData <- highlight_key( data.frame(x, y, groups), ~groups )
myPlot <- plot_ly(
  x = ~x, y = ~y,
  color = ~groups,
  data  = myData)
highlight(myPlot, color = "red")

This animated GIF 显示了代码创建的图形,它也说明了问题。

问题已经 noted before。但我似乎无法通过在双击时抢占默认的点击 select 功能或使用任何其他策略来解决它。我尝试过的一些事情:

  1. 触发plotly_doubleclick事件后,更改存储的JSON数据,使x>highlight>on为空。然后用Plotly.newPlot().

  2. 重新绘制
  3. 触发plotly_doubleclick事件后,使用remove.listener()禁用plotly_click事件。但是当 plotly_doubleclick 被触发时,这个策略似乎已经来不及起作用了:单击 (plotly_click) 事件已经被触发了。

  4. 更改布局 > 排序从 "traces first" 到 "layout first"

  5. 检测到双击时将 plotly_click 事件处理程序通知 return false。 (我使用 this methodplotly_click 事件处理程序检测激活它的点击是否是双击的一部分。)这个策略可能适用于点击图例,但它似乎不起作用点击情节本身。

None 这行得通。但我认为一定有解决办法——有吗?

有解决办法。它需要 (a) 覆盖默认的单击行为,以及 (b) 补充默认的双击行为。在这两种情况下,我们都需要编写自定义事件处理程序。

人们似乎倾向于通过引入一些延迟来区分单击和双击,以确保任何给定的单击既不是双击中的第一次也不是最后一次。这是合理的,但是当在这样的应用程序中使用时,延迟是显着的:单击标绘点后,在突出显示该点之前会有明显的滞后。出现延迟是因为单击 (plotly_click) 事件处理程序正在等待以确保触发它的单击不是双击的一部分。

幸运的是,我们不需要在此应用程序中引入延迟。关键是要意识到完全区分单击和双击是不必要的。我们只需要确保触发 plotly_click 的点击不是双击中的第二次点击。为什么我们只需要检查这种情况,我不确定。但这就足够了,我们可以检查这个条件,而不会在突出显示过程中引入任何显着的延迟。

这是完成这项工作的代码。在 R:

library(plotly)
x <- c(rnorm(3000, 0, 3), rnorm(1000, 0, 0.2))
y <- c(rnorm(3000, 0, 3), rnorm(1000, 0, 0.2))
groups <- rep(c("a", "b", "c", "d"), 1000)
myData <- data.frame(x, y, groups)
myPlot <- plot_ly(
  x = ~x, y = ~y,
  color = ~groups,
  data  = myData)
myPlot$elementId <- "myPlot"
myPlot <- highlight(myPlot, on = NULL, off = "plotly_doubleclick") 
onRender(myPlot, readLines("onRender.js"))

其中 "onRender.js" 是

function singleClickHandler (data, el, COLORS_TRACE, OPACITY_START, OPACITY_DIM) {
  let t0 = Date.now();

  // If the triggering click wasn't the second click in a double click...
  if ((t0 - doubleClickTime) > interval) {
    highlightTrace(data, el, COLORS_TRACE, OPACITY_START, OPACITY_DIM);
  }
}


function highlightTrace (data, el, OPACITY_START, OPACITY_DIM) {
  // We want clicking on a point to "highlight" that point and all other  
  // points in the trace -- by dimming the points in all -other- traces.

  const numTraces = el.data.length;              // total # of traces in plot
  const traceNum  = data.points[0].curveNumber;  // number of clicked trace

  // Initialize array with one element for each trace
  let traceOpacity = new Array(numTraces).fill().map( () => OPACITY_DIM );

  // Set only the clicked-on trace to have normal (relatively high) opacity
  traceOpacity[traceNum] = OPACITY_START;

  // Restyle
  Plotly.restyle("myPlot", { "marker.opacity": traceOpacity } );
}


function onRender (el) {

  // Get opacity of first mark in first trace when figure is first displayed
  const OPACITY_START = el._fullData[0].marker.opacity;
  const OPACITY_DIM   = 0.2;

  // Set timing
  interval = 1000;  // two clicks within 1 second (1000 ms) is a double click
  doubleClickTime = 0;

  // Wrap the singleClickHandler() event handler in onSingleClick(). We do 
  // this so we can pass both event info ("data") and other objects to 
  // singleClickHandler(). 
  var onSingleClick = (data) => singleClickHandler(data, el, OPACITY_START, OPACITY_DIM);
  el.on('plotly_click', onSingleClick);

  el.on('plotly_doubleclick', function (d) {      
    doubleClickTime = Date.now();    
    Plotly.restyle("myPlot", { "marker.opacity": OPACITY_START } );
  });
}


onRender