-
Notifications
You must be signed in to change notification settings - Fork 185
brush, pointer #721
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
base: main
Are you sure you want to change the base?
brush, pointer #721
Conversation
From the screen capture my feeling is that this is a different system than the one implemented for brush. With brush, the interaction mark shows the selected region, and is not responsible for highlighting the selected data points. Here the interaction mark shows the individual selected dots. We should aim for a consistent approach (brush, lasso, pointer) and just show the selection region as an overlay of the corresponding shape. The task of highlighting the selected marks is the same in all cases, and would be handled by the dot mark instead, maybe with the suggested Rather than mode, could we use pointer, pointerX and pointerY? Conversely, would we prefer to remove brushX brushY and have only one brush with a mode option? (I think I like the mode approach better, if it works consistently.) |
Consistency is generally a good thing, but it’s worth thinking this through. I find it helpful to think of two forms of selection: the geometric selection and the logical selection. The geometric selection describes the area—some region of the plane—that is selected. For a brush that is an axis-aligned rectangular region defined by x1, x2, y1, and y2. For a lasso it is some complex polygon or set of polygons. For a pointer it might be a circle around the mouse, or an infinitely tall or wide region for one-dimensional pointing. Or, it might be a buffered region of a given radius around the path traveled by the pointer (in the case of the more persistent “spray paint” pointing that I’d like to implement). In some cases, say if we select the n points that are closest to the pointer, there might not be a simple definition of the geometric selection! The logical selection describes which data are selected. In Plot this is represented exclusively by a subset of the mark’s index (node[Plot.selection]). This is the value that the plot exposes to the rest of the notebook, so you can e.g. show a table or a coordinated visualization. The logical selection, in a sense, is what matters to the user. It’s what the user seeks to control through interaction. The geometric selection is the means to that end: how to indicate that selection (conveniently). So, showing the logical selection, perhaps in addition to the geometric selection, is usually a good thing because it provides more direct feedback as to the effect of the user’s input. In the case of a brush, the geometric selection is persistent: after brushing a rectangular region, you can drag that rectangular region around. We could, possibly, do this behavior with the lasso as well, where you draw an arbitrary polygon, and then you can pick up and move that polygon around to change your selection. But I don’t think we want or need that. Instead, my expectation with a lasso is that the geometric selection (the polygon) is ephemeral: it dissolves as soon as I lift the pointer, and I’m left only with a logical selection as a set of selected points. Similarly when I’m using the pointer, if we want to support a persistent selection where I can click and drag to select some points near the mouse, and then shift-click and drag to select some other points, I’d rather just see the logical selection. I don’t think I want a visual representation of a buffered region around the path of the pointer while it was down. The challenge with showing the logical selection is that, unlike the geometric selection, we may not know an appropriate visual representation. If interaction marks only understand that you’re selecting infinitesimal points in x and y, then we could draw circles around those points, say, but that might not be a great representation say when you’re selecting circles of varying radii, or bars, images, rects, etc. But, maybe that’s fine? Maybe it just needs to convey a hint that you’ve selected something, even if it’s not perfect. The goal of Plot is just to get you something “good enough”, quickly. As we’ve discussed, I like the idea of an internal selection channel maintained by Plot and some mechanism for marks to express style overrides when selected. Maybe it’s even possible for Plot to have defaults so that all marks have a default selected state. But there remain a variety of technical and design challenges to get this to work, and we’ve already merged the brush mark and I would like to add pointer, lasso, and perhaps others. Perhaps when we add selection styles in the future, we change the interaction marks so that they don’t have a visual representation of the logical selection? Or at least a way to turn it off.
It’s useful to control separately (1) whether the points to be selected are defined in x or y or both and (2) whether the selection region around the pointer is defined in x or y or both. As I showed in the video, the points are defined in x and y, but I only want to select in x. I think this will be a common pattern when there is an independent and dependent variable (e.g., time and temperature, respectively). Also, the separate constructors (pointer, pointerX, and pointerY) don’t just set the mode, they also provide convenient defaults for x and y: maybeTuple if both x and y are specified, and otherwise identity if only one of x and y is specified. I think we want to maintain this pattern for consistency with other marks in Plot. The defaults are setup so that you won’t have to think about it most of the time, hopefully. If we want the brush to show the logical selection, we’ll need to take the mode option implemented here and implement that for brush, too. |
Here’s my other idea for a visual representation of the logical selection. The first video is mode xy, the second mode x. pointer-whiskers2.movpointer-whiskers.movIt looks kinda neat, but I don’t think it will work as well when we make the pointer selection persistent using click-and-drag (and letting you point to multiple areas with shift-click-and-drag, or deselect with option-click-and-drag). And also this style wouldn’t work with the brush, where the position of the pointer isn’t relevant. And, perhaps most importantly, while looking cool it does put most of the emphasis on the pointer location which is already visible with the pointer anyway. But leaving it here for the record. 🙂 |
Maybe a different interpretation of your comment is that pointer means mode = xy, pointerX means mode = x, and pointerY means mode = y. So, you’d never specify the mode option (or perhaps you could specify the mode option with pointer, but we’d expect you to use pointerX or pointerY instead). However, unlike Plot’s other similar constructors such as dotX and dotY, you’d be allowed to specify a y channel for pointerX, or an x channel for pointerY. Maybe that’s confusing? I think maybe that‘s confusing but it’s not a strong preference so let me know what you prefer. |
I'm confused by your remark since dotY does accept both x and y (it's just defaulting y to identity, and x to undefined) ;-) Anyway, yes, I'd be inclined to use pointer as 2d (mode=xy), pointerY as mode=y and pointerX as mode=x, and in that case there is no need to expose "mode" as an option. In terms of visual feedback, I don't like the circle much since it moves in sporadic jumps, whereas the link version is a bit less jarring as it's never static (it's still very jumpy though); but this can probably be smoothed out with a (quick) transition, a less aggressive shape and color, I don't know. link + circle would work too, the link easing the visual movement, but stopping before it actually occludes the target. I think i'd use it most often to select the (n = 1) closest element to the pointer rather than a whole range, but it's more gut feeling than any experience with this type of selection mechanism. My hunch is that with a large n the flock of lines will tend to occlude a majority of the selected dots, and the ones that are not occluded will be those farthest from the pointer, which seems a bit opposite to the goal. |
Oh, whoops. I got confused because it wasn’t always that way; prior to 5b81573 dotX forced y to be null, and dotY forced x to be null. So, does this mean brushX should allow y, and brushY should allow x? 🤔 Lines 81 to 87 in 551f53a
|
I think the difference between brushX(… {y}) and brush(… {y}) would be if the brush highlights the logical selection; in the first case brushX(… {y}) the brush is horizontal, but the highlighted values are dots positioned in x and y. In the second case it is a 2-d brush. |
2d13b7c
to
f05fa14
Compare
Okay, I think I got it in the latest commit. Now brushX and brushY allow you to specify y and x, respectively. And pointerX and pointerY similarly only provide defaults for mode and x and y, respectively, while allowing you to specify y and x respectively. |
I’ve implemented persistent selection (click and drag, and shift-click and drag). Let me know what you think, @Fil! |
Some open tasks:
The last task also applies to the brush—it’ll require some core changes, not just changes to pointer. The problem is that mark.data isn’t the same as the data returned by the transform. We have to decide whether we’re selecting from the original (untransformed) data or, perhaps more likely, selecting from the transformed data (e.g., selecting bins). |
|
Based on our earlier discussion, it is behaving as expected. (Or, it’s not clear what you mean by “breaks”—I see that brushX in your example is a functional 2D brush.) In your example you are specifying x and y, so brushX behaves the same as brush. The only difference between brush, brushX, and brushY is the defaults for x and y. If you want one-dimensional brushing: Plot.brushX(data, {x: "culmen_depth_mm"}) Or: Plot.brush(data, {x: "culmen_depth_mm"}) If you want two-dimensional brushing: Plot.brush(data, {x: "culmen_depth_mm", y: "culmen_length_mm"}) |
The case I found "broken" is when you want the data to be a 2-d scatterplot, and the brush to be on X only. This (in my mind) is what you want when you say brushX(data, {x, y}). It's also what's currently implemented in main. Since we're not highlighting the logical selection, it's true that we can use brush(data, {x}) instead. Maybe I just need to adapt my mental model… but for now I don't see what is the purpose of brushX and brushY anymore, if the mode is driven by the x and y options. 🤔 |
It’s the same as dotX and dotY. The only purpose is to specify different defaults. Lines 95 to 101 in 94d69a5
The brush doesn’t support a mode option currently. We could add one for symmetry with the other marks, but it’s a little redundant because brush doesn’t need the x and y channels in the other modes. Meaning if we did, you’d be passing in (and computing) the y channel with mode = x, but you wouldn’t be using it for anything. Though of course we could set the unused channels to null in the constructor I guess… |
I've reverted my commit and updated my mental model! |
c905213
to
2112eab
Compare
I’ve made a few small improvements (to handle band scales, to improve the display of one-dimensional pointing using rules instead of circles, and to change the default n to 1). I’m currently thinking that when you use brush or pointer with a transform, that you’re still selecting the source (input) data, i.e., the data prior to aggregation; you won’t be selecting e.g. bins. |
bf42c54
to
f106ee4
Compare
} | ||
if (!selectionEquals(this[selection], S)) { | ||
this[selection] = S; | ||
this.dispatchEvent(new Event("input", {bubbles: true})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how much of a "public" api your wanting to expose in these events... but access to the selected data indexes or the range of the data is needed. e.g.
this.dispatchEvent(new CustomEvent("input", {bubbles: true, detail: {selected: S, range}}));
where range is something like if(X && x.invert) range = extent.map(x.invert);
Then in the listener, you can use the indexes for filtering right away or show the textual range to to the user.
Without access to the indexes used, callers only can view the plot.value
. This only has what was selected. It does not provide what criteria was used for the selection, nor the data left out.
Hey guys – any update on this? I saw this demo on observable (not sure if it ever got released) but would be keen to see this feature in a recent version on NPM 🚀 |
Related #4. Like the new brush mark, except whatever is near the pointer is considered selected. It defaults to searching in a radius around the pointer in x and y, but you can configure it to search only along a single dimension, too.
pointer-demo.mov
TODO
Draw little whiskers connecting the pointer to the selected pointsFun, but no.… and maybe option-dragging removes from the current selection?Maybe in the future.