observablehq / plot

A concise API for exploratory data visualization implementing a layered grammar of graphics
https://observablehq.com/plot/
ISC License
4.42k stars 178 forks source link

A pointer interaction that handles click differently? #1832

Open Fil opened 1 year ago

Fil commented 1 year ago

When building an interactive app, we might want:

  1. a pointer that only sets the value when we click
  2. a pointer that doesn't stick when we click
mbostock commented 1 year ago

Maybe we can call it the “clicker” interaction in homage to TLOU. 😂

yurivish commented 1 year ago

I've been using Plot to build early product prototypes recently and something along these lines would have been extremely useful (though this use case is admittedly stretching the boundaries of "exploratory data visualization" a bit!)

A bit of context in case it's useful: one of the prototypes explored ways to browse the results of a set of benchmarks/performance experiments. The prototype looks like this:

image

The top "chart" shows a grid of cells, where each cell represents a specific set of experimental parameters. The charts below correspond to detailed information about the selected cell in the top chart.

The interaction I wanted was that the user would click on a cell in the top chart to explore the associated experiments, with the secondary charts appearing only once a cell had been clicked.

I couldn't figure out a way to distinguish hover from click, though, so I ended up doing some lower-level event handling to maintain a variable that held the "last-pointed-at" element as a way to model a sticky hover selection – otherwise, moving the cursor away from the cells would result in a very visible reflow as the associated charts disappeared.

I would have liked to be able to show a tip in the top chart on hover, but not show the associated charts until the user clicks on a cell, ie. to distinguish a hover selection from a sticky selection. The tip represents a first level of detail, and the associated charts represent a second, much greater level of detail (akin to a page navigation).

A related question that came up was how to persist the pointer state across re-renders of the chart. The prototype had a few range sliders that all of the visualizations depended on. Since these included the top chart (which held the selection state), the selection would be reset whenever the user interacted with any of the sliders. Because I was storing the "last-pointed-at" element separately, I was able to keep the associated charts on screen as the user slid the sliders, but the selection state in the top chart (ie. the tooltip indicating the selected experiment) would disappear and I couldn't figure out how to bring it back without introducing dependency cycles.

(One thing I tried was setting the value of the top plot and triggering an input event, but that did not seem to cause the tips to show up -- maybe because I was setting the value of the figure element and not the svg itself, though?)

Fil commented 1 year ago

Another example https://observablehq.com/d/7e5f1eec1354f1a2

ezralee commented 1 year ago

We are using Plot as our visualization library to embed within an in house "production" app and being able to disable "click-to-stick" would be great for things like: Initial click on figure expands to full screen view -> optionally click to stick to highlight data point -> download SVG.

vorbei commented 1 year ago

+1 For click only pointer. We are using Plot for interactive viz app, and selected value will trigger the "list detail data" function, we do hope to set "click" as a stable interaction. We need to request data base on the selected value, and it should not be be triggered too often. The hover to point is nice for realtime data exhibition, but not very ideal for this scenario.

tannerlinsley commented 11 months ago

If it's helpful to anyone, current @nozzleio, we are listening to pointer events and grabbing the plot.value, then using that value in any registered click/hover callbacks on the entire plot. Blah blah blah, here's some code that might inspire?

useStrictEffect(() => {
    if (plotEl) {
      plotEl.innerHTML = ''
      plotEl.append(plot)
    }

    let registeredOnLeave = false

    const onTipHoverCb = () => {
      if (!registeredOnLeave) {
        registeredOnLeave = true
        const onLeave = e => {
          if (!plotEl.contains(e.target)) {
            getOnTipHover()?.(undefined!)
            registeredOnLeave = false
            document.removeEventListener('mousemove', onLeave)
          }
        }
        document.addEventListener('mousemove', onLeave)
      }
      getOnTipHover()?.(plot.value as T)
    }

    const onTipClickCb = (e: any) => {
      if (plot.value) getOnTipClick()?.(plot.value as T, e)
    }

    plot.addEventListener('mousemove', onTipHoverCb)
    plot.addEventListener('click', onTipClickCb)

    return () => {
      plot.removeEventListener('mousemove', onTipHoverCb)
      plot.removeEventListener('click', onTipClickCb)
    }
  }, [getOnTipClick, getOnTipHover, width, height, plot, plotEl])
aboveyunhai commented 8 months ago

When building an interactive app, we might want:

  1. a pointer that only sets the value when we click
  2. a pointer that doesn't stick when we click

The first option or any form of value exposing really leaves us the opportunity to do whatever we want.

The current default of click to stick (which adds point-event: none to some elements) actually blocking any custom events after click. If possible, just pass something to us or expose the click event to user to handle/overwrite.

jordan-calderwood-reify commented 7 months ago

Hello! I would love to be able to disable the default click to stick behavior. We are trying to replace our app's current visualization library with Plot but the sticky behavior doesn't play nicely with our desired onClick behavior.

I see a PR to this effect https://github.com/observablehq/plot/pull/1979/files. Is this still a viable solution? Anything I can do to help?

mbostock commented 6 months ago

A quick workaround for this is that you can use event.stopPropagation on pointerdown events to prevent the pointer interaction from becoming sticky.

Another approach is to emit a pointerleave event that causes the pointer event to hide and clear the sticky bit.

const pointerdowned = (event) => {
  const pointerleave = new PointerEvent("pointerleave", {bubbles: true, pointerType: "mouse"});
  event.target.dispatchEvent(pointerleave);
};
jessiesanford commented 2 months ago

@mbostock I have tried using event.stopPropagation on my chart pointerdown events but it seems to only trigger when I click in the whitespace of the chart, when I click on a chart element (such as a dot, bar, or line) the pointerdown event is not fired. Do you have any other insight into this issue or workarounds?