Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tip mark + pointer interaction #1527

Merged
merged 56 commits into from
May 11, 2023
Merged

tip mark + pointer interaction #1527

merged 56 commits into from
May 11, 2023

Conversation

mbostock
Copy link
Member

@mbostock mbostock commented May 6, 2023

This is an alternative to #1304 that decouples the display mark (tip) from interaction (pointer). The tip mark no longer has any built-in interaction and can be used to display static tips. The pointer interaction, meanwhile, handles pointer events and re-renders a downstream mark (often but not always the tip mark) similar to a dynamic select or filter transform. Re-rendering is accomplished using a new render transform mechanism: a render option which allows the specified function to wrap mark.render.

As with the tooltip mark in the previous PR, the tip mark here uses getBBox to compute text metrics so that the path surrounding the tip text exactly fits the text. This requires the tip mark to render (partially) asynchronously, since plots and marks are initially rendered detached; the tip mark using a microtask (Promise.resolve) or requestAnimationFrame to defer rendering. I would prefer to render the tooltip synchronously, but this would require either approximating text metrics or synchronously inserting the Plot into the document at an arbitrary location, so partial asynchronous rendering feels like the least worst option.

To-do, pointer interaction:

  • Find the closest point to the pointer
  • Find the closest point in x (or y), approximately, as for lines and areas
  • Find the closest point across all facets
  • Limit the search radius
  • Click to switch between “sticky” and “free” mode, allowing text selection
  • Click to hide the tooltip (clearing the focused point)
  • Fix sticky modality when there is faceting; all facets should be sticky, or none
  • Fix sticky modality so that if one pointer interaction is sticky, they all are
  • Support x or y not existing; respect frameAnchor
  • Support x1 & y1 and x2 & y2 as alternatives to x and y
  • Support px & py to decouple pointing position from display position
  • Render transforms via the render mark option
  • Do something cleaner than mark._render (pass a next option)
  • Test again on mobile Safari
  • Tests
  • Documentation

To-do, tip mark:

  • Render the tooltip as SVG or HTML, anchored to a point
  • Draw a little connection to the anchor point
  • Implement four tip orientations
  • Option for forcing a specific tip orientation
  • Orient the tooltip relative to the anchor automatically based on position
  • Minimize changes in tooltip orientation
  • Configurable text styles
  • Configurable colors (even channels)
  • Limit the maximum width of the tooltip, clipping long text
  • Show title when tooltip value is truncated
  • Add a lil’ drop shadow
  • Display unscaled, labeled values for all channels in the tooltip
  • Display unscaled facet values, too (index.fx/index.fy)
  • Fix the inferred scale label not shown in the axis (e.g., “Date”)
  • Support x or y not existing; respect frameAnchor
  • Support x1 & y1 and x2 & y2 as alternatives to x and y
  • Display x1-x2 extent for binX (and similar for binY)
  • Display y2-y1 length for stackY( and similar for stackX)
  • Fix z-order of facet axes, which should be underneath facets
  • Fix z-order of the tip mark, which should be drawn atop all facets
  • Fix display of faceted value when the tip mark is not faceted (suppress index.fx/index.fy)
  • Fix display values for dodge initializer
  • Fix display values for hexbin initializer
  • Display inverted hexagon centroids with the hexbin initializer
  • Side anchors (not just corners: top, right, bottom, left) and middle
  • Allow the tip drop shadow to be configured imageFilter (support for the CSS filter attribute) #409
  • Allow the tip corner size to be configured
  • Don’t have the fill and stroke channels (etc.) change the appearance of the mark?
  • Instead of fill and stroke, show a color swatch when a channel is bound to the color scale
  • Apply the title reducer to ariaLabel; use for multi-line tips
  • Tests
  • Ability to reorder, hide, and customize the format of the displayed channel values
  • Documentation

To-do, crosshairs:

  • Crosshairs composite mark
  • Support faceting?
  • Support transforms?
  • Support initializers?
  • Support rule in conjunction with hexbin transform?
  • Tests
  • Documentation

To-do, other:

Optional:

  • Multi-line text, wrapped text, without a bold label
  • For link and arrow, treat {x1, y1} and {x2, y2} as distinct points?
  • Option for controlling where a tooltip appears on a rect/area/etc.?
  • Respect the r channel for hovering large circles?
  • Handle multiple dots in the same position (e.g., click to cycle)?
  • Automatic dark mode (media query or checking computed background color)?
  • Have group, bin, stack transform etc. populate a z channel for the benefit of tooltips?

Fixes #4.
Fixes #443.

@mbostock
Copy link
Member Author

mbostock commented May 7, 2023

Here’s an example of the facet z-order problem with this approach:

Screenshot 2023-05-07 at 9 46 04 AM

Another interesting problem is that each facet now listens independently for pointer events, so it’s possible to get multiple tooltips from adjacent facets! I’m not sure if this is an urgent problem, but we could certainly add some cross-facet coordination to prevent this from happening. (It can be rare depending on the chosen maxRadius, and it might be useful in some cases.)

Screenshot 2023-05-07 at 9 47 18 AM

There’s also a funny problem with the sticky implementation now, in that you can have multiple marks driven by the pointer interaction, and the sticky modality is independent for each mark (based on g.contains(event.target)). That means you can click to make one pointer-driven mark sticky while another pointer-driven mark remains free. This also feels like a bit of an edge case, but perhaps we could figure out how to make all the pointer-driven marks for a given plot share the same modality.

@mbostock
Copy link
Member Author

mbostock commented May 7, 2023

Crosshairs!

Screen.Recording.2023-05-07.at.10.01.41.AM.mov

@mbostock mbostock mentioned this pull request May 7, 2023
41 tasks
@mbostock mbostock changed the title tip mark tip mark + pointer interaction May 7, 2023
@mbostock
Copy link
Member Author

mbostock commented May 7, 2023

I reorganized the to-dos and would like to proceed with this direction instead of #1304. There are a few things still to figure out here, but I think the biggest two are (1) more coordination of the pointer interaction, such that (1a) only one point can be focused at a time across facets, and (1b) if there are multiple pointer interactions they are required to be defined consistently; and (2) some mechanism that lets the tip mark draw atop other facets.

One possibility for (2) is that we transpose the hierarchy of marks and facets, such that z-order is preserved across facets, rather than only within facets. And it’s possible that (1b) isn’t a requirement at all…

@mbostock
Copy link
Member Author

mbostock commented May 8, 2023

Fun experiment using color-mix to apply the stroke channel to the drop-shadow filter:

Screen.Recording.2023-05-08.at.9.16.50.AM.mov

@mbostock mbostock marked this pull request as ready for review May 8, 2023 18:40
@mbostock mbostock requested a review from Fil May 8, 2023 18:40
@mbostock
Copy link
Member Author

mbostock commented May 8, 2023

There are a couple things still to-do above, but I think this is ready for an earnest look.

@tophtucker
Copy link
Contributor

easy to add to auto 😅 #1532

Screen.Recording.2023-05-09.at.6.50.41.PM.mov

@mbostock
Copy link
Member Author

mbostock commented May 10, 2023

I would like to get this released soon. I feel the biggest blocking issue is the shorthand syntax, and more specifically, how we might drive the tip mark from an existing mark’s channels rather than duplicating a mark’s definition for the purpose of applying tips. Repeating the channel specifications definitely works, as the tests demonstrate, but it can be difficult to use particularly in conjunction with Plot’s many implicit behaviors (e.g., implicit stack transform, implicit tuples). My hope is we can somehow describe that the tip mark is derived from another mark, and that, somehow, the tip mark can therefore “inherit” the channels from this other mark (including even channels created by an initializer).

Related to this, the tip mark is unusual in that channels control the contents of the tip and also affect the appearance of the tip. For example, if you’re applying the tip to a mark that uses the fill channel, you might want to display the value in the tip; but, you probably don’t want to apply that color as the fill of the tip itself. We ideally want these channels to be independent: the values that we display in the tip should be configurable separately from the aesthetics of the tip.

So maybe the tip mark doesn’t inherit the channels of some other mark, but instead has a way of reading the channels of another mark (and maybe there’s a restriction that the two marks have the same data and faceting, so they’re parallel)? Perhaps the context that is passed to mark.render could expose a method for reading the channels of another mark?

I’ll experiment with this tomorrow.

@mbostock
Copy link
Member Author

Color swatches! 🟦 🟧 🟥

Screen.Recording.2023-05-10.at.12.14.00.PM.mov

@mbostock mbostock mentioned this pull request May 10, 2023
@mbostock
Copy link
Member Author

Looks good to me! LGTY, @Fil? 🚢

I suggest we followup with documentation (including the TypeScript interfaces).

@Fil
Copy link
Contributor

Fil commented May 11, 2023

Playing with it, and everything's working great.

Still unclear to me:

  • how to fully control the tip's contents (we mentioned ariaLabel, label, details…)
  • when and how do we introduce a mark's tip option
  • how we fix the z-order of marks, so that tip marks (esp. auto-generated by the tip option) are always on top of other marks

@mbostock
Copy link
Member Author

Are you saying those are blockers? I suggest we figure those out later as there are many possible solutions and I don’t think we’ll get stuck. (The tipDotFilter test demonstrates one strategy for putting the tips last even when there are multiple tips.)

@Fil
Copy link
Contributor

Fil commented May 11, 2023

Definitely not blockers. Let's rock :)

@mbostock mbostock merged commit 60be56d into main May 11, 2023
@mbostock mbostock deleted the mbostock/tip branch May 11, 2023 20:38
@tannerlinsley
Copy link

Ooo la la 😍 testing asap

@net
Copy link

net commented May 11, 2023

Thank you for not binding the tooltip position to the mouse position. I've always hated the aesthetic of tooltips moving around with the mouse. No other UI tooltips follow the mouse, and plots' shouldn't either. ❤️

@yurivish
Copy link
Contributor

yurivish commented May 11, 2023

I am unreasonably excited about these! Great work.

Just tried them out and noticed an interesting consequence of the current rules, mentioning it in case it helps decide whether to keep this behavior or maybe change it:

image

You can cause the same data field to appear multiple times in the tooltip if more than one channel is driven by it. The above picture is the result of evaluating

Plot.dot([{ x: 1 }], {
  x: "x",
  y: "x",
  tip: true
}).plot({ width: 100, height: 100 })

using the tip-option branch.

Notebook link

@mbostock
Copy link
Member Author

Yes, @yurivish. The tip can’t currently tell which channels are redundant. The tip has access to the materialized channel’s array of values, but not the channel’s definition. Even if it had access to the definition, if in your example you wrote the definitions as x: (d) => d.x and y: (d) => d.x it would still “look” like two different definitions.

Instead of trying to detect such a case, the current plan is to give the user control over which channels appear in the tip; by default we’ll try to show (almost) everything. If the problem of redundant channels occurs more often than expected then we can try to detect the common forms, but I’d rather not add the complexity if possible.

@yurivish
Copy link
Contributor

Ah, yeah, that's a very good point. Makes sense – thanks for explaining!

@tannerlinsley
Copy link

Any way we could get an @next npm tag release? This needs to get into Nozzle asap.

@mbostock mbostock mentioned this pull request Aug 9, 2023
6 tasks
chaichontat pushed a commit to chaichontat/plot that referenced this pull request Jan 14, 2024
* tip mark

* render transform!

* pointer interaction

* port mbostock/tooltip fixes

* improved tip & pointer

* simplify

* better facets; stable anchor

* px, py; crosshairs

* crosshairs composite mark

* crosshair singular

* transpose facets and marks

* prefer top-left

* only pass index.f[xyi] if faceted

* [xy][12]; don’t apply stroke as text fill

* tip + hexbin test

* optimize faceting by swapping transforms

* renderTransform instead of _render

* only one pointer across facets

* suppress other facets when sticky

* prevent duplicate ARIA when faceting

* isolate state per-pointer

* fix crash with one-dimensional tip

* if px, default x to null; same for py

* tidier crosshair options

* use channel label if available

* only separating space if named

* crosshair initializer fixes

* tidier crosshair options

* remove to-do

* tip + dodge test

* cleaner facet translate

* crosshair text using channel alias

* preTtier

* fix transform for [xy][12]

* p[xy] precedence

* pointer comments

* tip textAnchor

* more tip options

* more tip options, comments

* bandwidth offset

* fix for multi-facet, multi-pointer

* fix dimensions

* tip side anchors

* tipped helper

* raster nearest

* color swatch; fix f[xy]; no tip aesthetic channels

* multi-line, summary ariaLabel

* tidier formatting

* tidier crosshair

* project p[xy], too

* centroid test

* geoCentroid test

* shorthand extra channels

* no pointer-specific state

* revert Mark interface change

* remove dead code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Crosshairs Tooltips (hover to read values).
6 participants