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

Initial support for linked views and animation #554

Merged
merged 284 commits into from
Nov 17, 2016

Conversation

jcheng5
Copy link
Collaborator

@jcheng5 jcheng5 commented Apr 19, 2016

Lots of work still needed here, but it works enough that you can get a sense of what I'd like to see happen here. This is similar to other demos that have existed in the past, but unlike my previous demos it now uses plotly, not just ggplot2/ggvis/base graphics; and unlike @cpsievert's previous demos (to my knowledge) it now does client-side linked brushing between two distinct plotly plots.

Live demo:
https://beta.rstudioconnect.com/jcheng/shiny-crosstalk/

  • Decide on API for specifying Crosstalk key and group (or should both parameters be bundled into a single crosstalk param?)--this should ideally be consistent across all Crosstalk-compatible htmlwidgets (Update: key/group are attributes of the crosstalk::SharedData object which plot_ly()/ggplotly() will require for enabling linked views)
  • Need to be able to programatically clear the selection box on a plotly.js plot when another plot in the same group activates its brush (including non-plotly plots) (Update: I worked around this for now by removing the selection box myself.)
  • I'm not sure I understand all the possibilities around multiple traces and/or subplots, could use a brain dump on these things from @cpsievert
  • Client-side linking can use plotly_selecting and you get a really nice experience--but to make it not overwhelm the server we need to use plotly_selected right now. Maybe should formalize this in Crosstalk and have both "fast" and "slow" subscription models.

cc @jjallaire, @ramnathv

@hafen, I'm hoping to look more closely at rbokeh next.

@timelyportfolio
Copy link
Collaborator

The demo does not work for me on Windows 7/Chrome Version 49.0.2623.110. Am I misunderstanding? I'll dig into the code and update Chrome. There are no JavaScript console errors.

@ramnathv
Copy link

The brushing has no effect for me either. Again, no js errors in console.

@jcheng5
Copy link
Collaborator Author

jcheng5 commented Apr 19, 2016

Sorry, forgot to mention, you have to click the little dotted box outline in the plotly toolbar first to put it in selection mode instead of zoom mode. I couldn't figure out how to make this the default.

@timelyportfolio
Copy link
Collaborator

That makes sense, and I thought I tried, but I guess I didn't try hard enough. It is working.

@cpsievert
Copy link
Collaborator

cpsievert commented Apr 19, 2016

@jcheng5 plot_ly() %>% layout(dragmode = "select")

@jcheng5
Copy link
Collaborator Author

jcheng5 commented Apr 21, 2016

Updated the demo to default to select mode, and I'm clearing selection boxes now (by hacking on the DOM directly, can clean this up if plotly.js adds an API for this--I need to file an issue).

@cpsievert
Copy link
Collaborator

@jcheng5 is it possible for me to add commits to this pull request? I just tried:

git fetch origin pull/554/head:crosstalk
git checkout crosstalk
git add *
git commit -m "add skip_on_pull_request()"
git push origin crosstalk:joe/feature/crosstalk

but it just created this new branch.

If it isn't possible, that'd be great if you could send a pull request from the ropensci remote (you should be a collaborator now).

@jcheng5
Copy link
Collaborator Author

jcheng5 commented Apr 26, 2016

Ok @cpsievert, I added you.

@cpsievert
Copy link
Collaborator

@jcheng5 I like what you've done in 7ec2ec3 to allow different "sets" of traces. As you probably know, the event_data() infrastructure uses the source argument in plot_ly() which currently specifies plot-level groupings. I really like this finer grained approach, so I'll start moving in that direction (by the way, since crosstalk has a .set() method, I think we should use a name other than set).

Before anyone puts more work in, let me outline my vision for how linked selections could work more generally (feel free to chime in @etpinard, @alexcjohnson, @timelyportfolio):

In plotly.js, most (if not all) trace types support a constant opacity attribute, but only a few support a marker.opacity array. If we want to abstract linked selections to more trace types (and events), we could do something like:

  1. On "selection" (i.e., on "plotly_selected"/"plotly_click"/"plotly_hover"):
    (a) Use Plotly.restyle() to reduce the existing opacity of any relevant traces (any traces with the same set group).
    (b) Take the full trace data of relevant traces (from graphDiv._fullData), subset their (x/y/z) values according to the selected keys, and Plotly.addTrace() with showscale/showlegend set to false. And since we're using data from graphDiv._fullData, we shouldn't need to worry about where to anchor the traces (i.e., subplots should automatically be supported).
  2. Upon "deselection" (i.e., on "plotly_deselect"/"plotly_doubleclick"):
    (a) Use Plotly.restyle() to return to the original opacity.
    (b) Use Plotly.deleteTrace() to delete the "selection trace".

We could then allow users to define the "selection"/"deselection" events and the opacity multiplier from plot_ly() (or ggplotly()).

@jcheng5
Copy link
Collaborator Author

jcheng5 commented Apr 27, 2016

I agree that set is not a good name. What would you think about having a single parameter named crosstalk that you can pass an options object to? There could be constructor for the options object in the crosstalk package.

plot_ly(..., crosstalk = ct_opts(group = "groupA", key = ~rownames(.)))

plot_ly(..., crosstalk = ct_opts(group = "groupA", key = ~subjectID))

@jcheng5
Copy link
Collaborator Author

jcheng5 commented Apr 27, 2016

I don't really understand the subplots vs. _fullData, is there anywhere I can look to grok what _fullData is for?

Would the approach you outlined (adding a separate trace when something is selected, and decreasing opacity on the original trace) work for a pie chart? How about a stacked bar or area chart (if those even make sense for linked brushing)?

@cpsievert
Copy link
Collaborator

And since we're using data from graphDiv._fullData, we shouldn't need to worry about where to anchor the traces (i.e., subplots should automatically be supported).

Sorry, I was mistaken, using graphDiv._fullData over x.data wouldn't buy us anything extra here. The difference is essentially that graphDiv._fullData supplies defaults -- http://codepen.io/cpsievert/pen/ZWqQpe

@cpsievert
Copy link
Collaborator

I suppose for something like pie charts, overlaying the filtered data wouldn't work, but would require overlaying all the data and hiding graphical elements with "transparent" (or similar) like this.

Another problem is that, for some trace types (e.g., heatmap), key should really be a matrix (i.e., multidimensional array). Here is an example using event_data() and here is how I access key values under the hood. Would this sort of thing fundamentally conflict with what you have in mind for crosstalk?

How about a stacked bar or area chart (if those even make sense for linked brushing)?

I'd have to think about this, but as you show in your example (in the initial comment), it is certainly useful to overlay bars/densities related to the selection (which can only be done client-side by adding another trace). This gets into the realm of non 1-to-1 mappings in the source view <-> data <-> target view pipeline, which seems like a huge messy problem, but it'd be awesome if one day something like this could replace your example.

subplot(
  ggplot(mtcars, aes(wt, mpg)) + geom_point(),
  ggplot(mtcars, aes(wt, disp)) + geom_point(),
  ggplot(mtcars, aes(cyl)) + geom_bar(position = "identity"),
  crosstalk = ct_opts(key = row.names(mtcars))
)

Layering selection traces would also have the advantage of supporting a sequence of selections which is super useful for comparisons, for example:

pedestrians

That being said, I don't want to open pandora's box and prevent progress from being made. Let's just focus on reducing the opacity of non-selected points for now, and leave trace layering for another pull request.

@jcheng5
Copy link
Collaborator Author

jcheng5 commented May 4, 2016

@cpsievert Speaking of Pandora's box, I'm now looking at adding rudimentary client-side filtering support (for plots without aggregated data). It doesn't look to me like Plotly.restyle will be helpful for fully hiding points; I tried setting opacity to 0, but the hover tooltips still show up.

One brute-force option I have is to essentially just re-run renderValue, should I just do that?

@cpsievert
Copy link
Collaborator

I'm pretty sure the brand spanking new filter transforms will be helpful for fully hiding/removing points.

Personally, I find highlighting (rather than fully removing) to be more useful, so this isn't a huge issue for me, at least initially.

I'm currently flirting with the idea of manipulating the DOM directly with Plotly.d3, rather than using Plotly.restyle()/Plotly.addTrace(), which is actually how brushing currently works in plotly.js. I'll probably start another pull request in the next few days so we can easily see/discuss the pros/cons of the two approaches.

@jcheng5
Copy link
Collaborator Author

jcheng5 commented May 4, 2016

@cpsievert The transform plugins look cool. It looks to me, though, like it's basically a more declarative way to transform data on the way into the plot--I could just do that transformation myself. The part I'm less sure about is how to trigger the plot to redraw when the filtering changes, short of calling Plotly.newPlot.

Aaaand I just discovered Plotly.redraw. Never mind. :)

@jcheng5
Copy link
Collaborator Author

jcheng5 commented May 4, 2016

@cpsievert I was able to get everything I needed (AFAICT) with Plotly.redraw. I lifted one particularly sketchy technique out of the filter example from the Plotly.js plugins branch--the part where they iterate over the entire data object looking for things that look like arrays, and subsetting them. (I'm not pushing the filtering stuff to this branch just yet, it's a big mess that I'm just starting to clean up now that it's breathing.)

Just a word of warning, in case you have local changes based off this branch: I had to completely blow up and refactor the existing code to get this working.

The JS code for the htmlwidget binding is getting big enough that we should perhaps start considering using modules and unit tests, and ES2015 (or TypeScript if you prefer). If you're up for that in principle, I can implement it (maybe not this week but soon).

@cpsievert
Copy link
Collaborator

I do have local changes, but I was anticipating conflicts and started a new branch, so no worries.

BTW, I ditched the idea of manipulating the DOM with D3js. I'm back to what I outlined here. I could definitely use some help with filtering, so I'm looking forward to seeing what you have thus far!

I agree that we'll need some JS unit tests, but I don't have much experience with this, any opinions on this @timelyportfolio? Should we aim for consistency with the plotly.js approach?

@timelyportfolio
Copy link
Collaborator

timelyportfolio commented May 5, 2016

@cpsievert, you read my mind. I think consistency with CommonJS modules and the jasmine/karma testing from plotly.js would be ideal. Perhaps, @etpinard could help us decide whether or not to use ES2015 modules instead of CommonJS. I worry that TypeScript, while nice, would be much different from the current plotly.js approach.

I am trying to think through the best way to run these tests from R. @jcheng5, any advice on this?

@jcheng5
Copy link
Collaborator Author

jcheng5 commented May 5, 2016

Oh, I wasn't actually thinking we'd run the tests from R. I was thinking we'd just run the JS tests from the command line (as well as browserify).

Jasmine/karma is fine.

@timelyportfolio
Copy link
Collaborator

timelyportfolio commented May 5, 2016

@cpsievert, @etpinard should we tie in continuous with CircleCI? This is probably premature, since we need tests first :)

@cpsievert
Copy link
Collaborator

If at all possible I'd prefer to keep the entire testing suite on TravisCI

// Preserve the original data. We'll subset based off of this whenever
// filtering is (re)applied.
this.origData = JSON.parse(JSON.stringify(graphDiv.data));

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jcheng5 just getting back to this now after a major overhaul of subplot()...can't wait to dig in and help out!

First question: how come we need JSON.parse(JSON.stringify()) here? To verify it's valid JSON?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks to me like the purpose of this is a deep copy.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's a bit questionable to do it this way as it loses any functions, Date objects, and any other custom objects that aren't JSON, but it didn't look to me like the .data property would have any of that anyway...?

@cpsievert
Copy link
Collaborator

@jcheng5 with "rstudio/crosstalk@joe/feature/filter", in non-shinyMode, there is a jQuery conflict (Uncaught TypeError: $ is not a function) deriving from this line. Any ideas how to best handle this @timelyportfolio?

@timelyportfolio
Copy link
Collaborator

timelyportfolio commented May 20, 2016

The easiest, but I'm not sure best way, is to add the jQuery dependency with rmarkdown::html_dependency_jquery(). @jcheng5, do we expect crosstalk to require jQuery? If so should we add it to the crosstalk::dependencies list?

@ramnathv
Copy link

One idea I had a long while back, but never got around to doing it is a htmlwidgetlibs package that purely consists of commonly used dependencies like jquery, d3, bootstrap, lodash etc. It could have a single function that simply adds a list of dependencies to the widget. To have this mechanism work, we would have to tweak things so that dependencies can be specified from another package in widget.yaml. Just throwing this out there. If some of you believe it is a good idea, I can flesh out some more details.

@jjallaire
Copy link

I've done this already to some extent within rmarkdown, right now here is
what is there:

html_dependency_jquery
html_dependency_jqueryui
html_dependency_bootstrap
html_dependency_font_awesome
html_dependency_ionicons
html_dependency_tocify

These could of course easily be moved to a new package called perhaps
"htmldependencies" ?

We'd also need as Ramnath pointed out a syntax for referencing these in
widget.yaml.

On Fri, May 20, 2016 at 11:45 AM, Ramnath Vaidyanathan <
[email protected]> wrote:

One idea I had a long while back, but never got around to doing it is a
htmlwidgetlibs package that purely consists of commonly used dependencies
like jquery, d3, bootstrap, lodash etc. It could have a single function
that simply adds a list of dependencies to the widget. To have this
mechanism work, we would have to tweak things so that dependencies can be
specified from another package in widget.yaml. Just throwing this out
there. If some of you believe it is a good idea, I can flesh out some more
details.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#554 (comment)

@timelyportfolio
Copy link
Collaborator

timelyportfolio commented May 20, 2016

@jjallaire, I use the rmarkdown set of dependencies and shiny bootstrap dependencies all the time, so it is a very common pattern for me to

attachDependencies(tagList(htmlwidget),list(rmarkdown::html_dependency_jquery())

@ramnathv
Copy link

@jjallaire i like the name htmldependencies. We could also call it htmlwidgetdeps to make it more explicit.

On the question of syntax for referring to these, I was thinking

dependencies:
  - name: jquery
    package: htmlwidgetdeps
    version: 

By default, a user need not specify version in which case it will resolve to the latest version of jquery. However, a user can also refer to a specific version of jquery to avoid surprises.

This would call for a little bit of change in htmlwidgets, where the presence of a package attribute in the spec will trigger the new logic.

@jjallaire
Copy link

I'd call it htmldependencies rather than htmlwidgetdeps because these can
also be used by:

  1. Custom R Markdown formats that want to use these dependencies
  2. Other HTML producing functions that aren't htmlwidgets

On Fri, May 20, 2016 at 12:04 PM, Ramnath Vaidyanathan <
[email protected]> wrote:

@jjallaire https://github.com/jjallaire i like the name htmldependencies.
We could also call it htmlwidgetdeps to make it more explicit.

On the question of syntax for referring to these, I was thinking

dependencies:

  • name: jquery
    package: htmlwidgetdeps
    version:

By default, a user need not specify version in which case it will resolve
to the latest version of jquery. However, a user can also refer to a
specific version of jquery to avoid surprises.

This would call for a little bit of change in htmlwidgets, where the
presence of a package attribute in the spec will trigger the new logic.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#554 (comment)

@cpsievert cpsievert changed the title Another attempt at crosstalk integration Initial support for linked views and animation Nov 17, 2016
@cpsievert cpsievert merged commit 51e159b into plotly:master Nov 17, 2016
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.

7 participants