integrating vega
Opened this issue · 3 comments
I think vega (not vegalite) is the right level of abstraction for what we are trying to do here. Like vegalite it uses a json spec for defining graphics but has a much lower level API for defining event streams and responding to them.
vega spec
{
"$schema": "https://vega.github.io/schema/vega/v4.json",
"width": null,
"height": null,
"signals": [],
"data": [],
"scales": [],
"axes": [],
"marks": []
}
The basic vega spec maps somewhat organically to our ideas surrounding the plot_tibble
.
Evaluated plot data that results from a graphics pipeline can be inserted as a row oriented json into the spec at runtime, likewise marks, axes, and scales can be parsed into the spec via our aesthetic mappings (multiple layers can be generated via the details determined by the plot data.),
I currently can mostly render a few basic plots statically using this approach via the vegalite::from_spec()
function.
Signals are an interesting feature of the vega api , they are reactive variables that respond to input event streams. As an example here's a signal that defines a basic brush on the x axis:
{
"name": "brushX",
"on": [
{
"events": "mousedown",
"update": "[x(), x()]"
},
{
"events": "[mousedown, window:mouseup] > window:mousemove!",
"update": "[brushX[0], clamp(x(), 0, width)]"
}
]
}
In this case the signal is composed of two events, one that initializes the brush on mouse down as an array of size 2, and then updates the second value of the array after a mousemove and mouseup, while ensuring the range of the second element is within the boundaries of the plot area.
Since the brush is reactive we can then draw it as rectangle mark by encoding the xmin and xmax aesthetics should update in response to the streams.
"marks": {
"type": "rect",
"encoding": {
"update" : {
"x" : {"signal" : "brushX[0]"},
"x2" : {"signal" : "brushX[1]"},
"y" : {"value" : 0},
"y2": {"value": "height"}
}
}
}
A question is how to best represent the creation of signals via our control_
verbs, since they are not realized until graph is rendered, how can we best represent something like control_drag() %>% draw_rect()
in our plot tibble? Maybe as an empty reactive data frame with the columns corresponding to the signal array? Should a user then have to specifiy a new visualise
call to be explicit about their intention of how to draw the brush?
mtcars %>%
visualise(x = mpg, y = hp) %>%
draw_points() %>%
control_drag_x() %>%
visualise(xmin = brushX0, xmax = brushX1) %>%
draw_rect(...)
In a way this is more explicit then what we have discussed before... It does bring up one question though - is the data outputted from a brush a valid aesthetic in the way we ordinarily think of them or should something 'special' be associated with a control?
Essentially we could have a very compressed set of signals that are initialized with control_*
so we can limit the possible signals provided by vega.
View api
Instead of embedding the data into the json spec, we can programmatically insert it at run time using the View API, all we need to do is have a reference to the name of the data object from R as a field in the json spec - this requires a bit of hackage on the htmlwidget side.
Pushing everything to down vega's runtime
Another possibility is to have our graphics pipeline transpiled into a vega spec.
There's a pretty extensive array of ops available to aggregate/filter etc - could we map these to dplyr verbs?
integrating with shiny
See here for examples of sending data back and forth between shiny and js:
Since the View API allows signals to be modified/updated/listened to at runtime we can use that to send events back to Shiny. See here for details:
https://vega.github.io/vega/docs/api/view/#view_addSignalListener
Some good thoughts here.
It sounds like you're proposing for the control_
elements to emit a dataset, which makes sense. I'm not sure why control_drag()
would emit variables named after the brush. Wouldn't they be something like x_start
, y_start
, x_cur
, etc? Also not sure why we would need an X axis specific control_drag_x()
.
That's fair - those names make sense to me. If you wanted to just drag the brush along the x-axis and have a box that covers the entire y-axis you could just set ymin = min(aes_y); ymax = max(aes_y) to draw rect rather than having different control events.