Open RyanMullins opened 2 years ago
Proposed API:
selection.onClick((e: MouseEvent, s: SpriteView, d: Datum, index: number, arry: Datum[]) => {
/* ... */
});
selection.onMouseover((e: MouseEvent, s: SpriteView, d: Datum, index: number, arry: Datum[]) => {
/* ... */
});
selection.onMouseout((e: MouseEvent, s: SpriteView, d: Datum, index: number, arry: Datum[]) => {
/* ... */
});
Often, users won't need all the parameters. I suspect that usually it'll be just the first couple that get used (the Event, the SpriteView and/or the Datum).
API Rationale:
on()
method that takes a string parameter, having specific functions makes it clear which events are supported. See https://github.com/d3/d3-selection#selection_onon()
method takes a listener to which it passes the Event, the Datum, and sets the DOM object to this
. Instead of overloading this
, passing in the SpriteView makes the listener more consistent with other Selection callbacks.each()
, attr()
, etc.) do pass the index
and the array of bound data, D3's on()
method does not. By passing the index
and the bound data array, we make it easier for users to work backwards from the event to their own data.Concerns:
mouseover
and mouseout
events because these are not native DOM elements. Rather, Selection will need to listen for mousemove
. So while the e
parameter passed to callbacks will be an instance of MouseEvent
, it will not match the method name called on the Selection.onBind()
callback should be invoked for each mouse event like it currently is for other lifecycle events. I'm leaning towards 'no' because the data is not being re-bound at this time.bind()
call is asynchronous and may be long-running. It's possible that a user's interaction may happen while callbacks affecting that Sprite/Datum are queued. Not sure how to handle this.Open to discussion.
Things I support...
.onClick(...)
etc. naming convention for the API over the generic .on()
methodSpriteView
, index, and bound array to the callback, in addition to the event and datum..onBind()
callback in response to these events; these APIs should focus on outbound state communication, not internal updates/changes.Things I could use a bit more clarity on...
.onMouseover()
and .onMouseout()
somewhat difficult to follow for a MegaPlot Selection
.
Selection
all get mapped to the same canvas
element, which doesn't inherently afford per-datum mouse event associations, and even if per-datum events were implemented, the elements in a Selection
cannot independently update their state (nor can a SpriteView
)..onMouseout(...)
would be accompanied by an .onMouseover(...)
, each requiring a redraw, and those would be handled sequentially in client code, leading to 2x the redraws per mousemove
(at whatever the debounced rate)..onMousemove(...)
is the right name, but maybe .onHover(...)
better conveys the difference in intent between this behavior and .onClick(...)
? However, elements in a MegaPlot Selection all get mapped to the same canvas element, which doesn't inherently afford per-datum mouse event associations,
Yes, the underlying DOM events would be mousemove
events on the canvas or its container element. The Megaplot mouseover
/mouseout
would be synthetic events.
Synthetic events are not uncommon even in the native browser implementation. For example, a doubleclick
is a synthetic event which arises from two click
s. A click
arises from mousedown
followed by mouseup
. On mobile, even mousedown
/mouseup
are synthetic, arising from touchstart
/touchend
respectively.
even if per-datum events were implemented, the elements in a
Selection
cannot independently update their state (nor can aSpriteView
).
The current Selection
implementation does not permit access to single data points. However, this is only a design choice. The underlying Sprite
objects which are bound to data could be manipulated independently, and that's what I'm proposing with the .onMouseover()
and .onMouseout()
methods.
My concern is that having both of these events could potentially induce many (possibly expensive) redraws of the canvas. My intuition in the worst case scenario is that every
.onMouseout(...)
would be accompanied by an.onMouseover(...)
, each requiring a redraw, and those would be handled sequentially in client code, leading to 2x the redraws permousemove
(at whatever the debounced rate).
Draw calls are batched and run every animation frame while there's something to draw. Setting onMouseout()
/onMouseover()
callbacks would cause redraws to occur eventually, but only at most one per animation frame.
A slightly more expensive operation than drawing is the update, which flashes the new values into the data texture to be used during draw frames. This operation is also batched to reduce the frequency of CPU/GPU data shuttling.
The most expensive operation is the reading of the hit test results back from its output texture. Debouncing/throttling the native mousemove
events would reduce hit testing and invocations of the onMouseover()
/onMouseout()
callbacks.
Could we combine these into a single method, to try to prevent excess redraws? I'm not sure that
.onMousemove(...)
is the right name, but maybe.onHover(...)
better conveys the difference in intent between this behavior and.onClick(...)
?
Yes, they could be combined into a single onHover()
callback (but not to prevent redraws, as previously discussed).
In that case, the implication is that when the user stops hovering the sprite, it should revert to its pre-hover state. Or, more accurately, revert to animating toward the target state. For animation duration (TransitionTimeMs
), the post-hover reversion should probably copy the duration of the onHover()
to be symmetrical.
selection.onClick((e: MouseEvent, s: SpriteView, d: Datum, index: number, arry: Datum[]) => {
/* ... */
});
selection.onHover((e: MouseEvent, s: SpriteView, d: Datum, index: number, arry: Datum[]) => {
/* ... */
s.TransitionTimeMs = 300; // Used for both hover and reversion.
});
Note however that we're getting further from the DOM API by doing this. In CSS, there's a :hover
pseudo-selector, but there is no hover
event. To implement hover in native DOM/JavaScript, one must use a pair of mouseover
/mouseout
handlers and implement one's own reversion logic. I can only speculate why. My guess would be that in the DOM, keeping track of all the old states in order to revert would be too costly. This isn't a problem for CSS, because the cascading style can be recomputed. It's also not a big problem for us because we can make a copy of a relatively tight block of memory (a small range out of a big Float32Array
) to capture all the properties of the sprite.
Addendum: looking at the DOM API some more, it seems like mouseenter
/mouseleave
may be slightly more appropriate than mouseover
/mouseout
semantically, since the enter/leave pair do not bubble, and neither would our synthetic events.
MegaPlot provides a basic
Scene.hitTest(x, y)
API that returns the full list of Sprites at that location. This requires API consumers to write essentially the same boilerplate code, shown below and also in the debugging demo:A common use case for interactive visualization is to get the top-most Sprite in response to a
click
ormousemove
event and update that Sprite's state to indicate that it is selected of hovered. This could be done directly withSprite.update()
as in the debugging demo, or via a more circuitous path in a coordinated, linked visualization context, e.g., through LIT's centralized Selection and Focus services.MegaPlot could add convenience APIs for
.click()
,.mousemove()
, etc. that only focus on the single-sprite use case, attaching the event listener to the<canvas>
element it injects into the DOM. This would allow consumers to define more complex behavior via the.hitTest()
API is they care about something other than the top-most Sprite at the location.