IRkernel / repr

String and byte representations for all kinds of R objects
https://irkernel.github.io/docs/repr/
GNU General Public License v3.0
54 stars 34 forks source link

viz. with rbokeh does not work #108

Open smartinsightsfromdata opened 9 years ago

smartinsightsfromdata commented 9 years ago

First of all, I've just installed yesterday the new OS from Apple El Capitan - I mention this in case there is some specific issue related to it.

Alo, this assumes that IRkernel / Jyupiter works with the family of htmlwidgets. So far I've tested two and both don't work.

This is the code

library(rbokeh)
figure() %>% ly_points(cars$speed, cars$dist)

I get the following code at the UI in red, but it is not a major issue as I normally get it also in Studio

In red:

xlim not specified explicitly... calculating...
ylim not specified explicitly... calculating...

Nothing happen in the browser. This is what I get in the console:

[I 16:18:09.307 NotebookApp] Serving notebooks from local directory: /Users/e
[I 16:18:09.307 NotebookApp] 0 active kernels 
[I 16:18:09.307 NotebookApp] The IPython Notebook is running at: http://localhost:8888/
[I 16:18:09.307 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[I 16:18:21.015 NotebookApp] Creating new notebook in 
[I 16:18:21.554 NotebookApp] Kernel started: 8eeb3261-a880-4806-9c3d-6886cde73adb
> IRkernel::main()
[1] "Got unhandled msg_type:" "comm_open"              

Afterward if I do !R --version (no matter if I restart the kernel etc.):

Error in eval(expr, envir, enclos): object 'R' not found
takluyver commented 9 years ago

(the !R --version was to be run in Python to debug the R kernel crashing on start - that's unrelated to this issue)

It looks like rbokeh is trying to open a Jupyter comm, which isn't implemented for the R kernel yet. I'd guess the Bokeh JS is doing something to detect whether it's being used in a notebook, and if so it assumes that it's valid to open a comm. @damianavila, @hafen, does that sound right?

To make it work, either:

  1. Bokeh needs to understand that being in a notebook frontend doesn't automatically mean that comms are supported by the kernel, and if they're not, it should be able to fall back to... whatever else it does when it doesn't have comms.
  2. IRkernel needs to support comms, and rbokeh needs to be adapted to use them.
smartinsightsfromdata commented 9 years ago

I have the impression that this issue is common to all of the family of htmlwidgets.

I've just tried networkD3 with similar results.

hafen commented 9 years ago

Enzo is correct - the rbokeh output is an htmlwidget, so it should render like any htmlwidget. I'd love to see this working in jupyter but unfortunately I don't have much expertise here.

damianavila commented 9 years ago

AFAIK, rbokeh does not try to use comms (but I can be wrong because I did not see the code base for a while), so this is probably related with htmlwidgets...

smartinsightsfromdata commented 9 years ago

@ramnathv @timelyportfolio could you kindly give a piece of advice?

ramnathv commented 9 years ago

For this to work, one would have to write an R package to handle the comms in Jupyter. Rather than each widget trying to write its own comm, I think it makes sense to write such a package, and an S3 method that would invoke it in a notebook context.

ramnathv commented 9 years ago

The following code will save the widget as a local html file

library(htmlwidgets)
library(rbokeh)
x = figure() %>% ly_points(cars$speed, cars$dist)
tf = 'test.html'
saveWidget(x, file = tf, selfcontained = F)

I tried using IRdisplay::display_html to display the html file in the notebook, but was unable to get the path right. How does one refer to local files in a notebook?

takluyver commented 9 years ago

How does htmlwidgets JS communicate with the backend in general?

The "Got unhandled msg_type:" "comm_open" in the console indicates that something on the JS side is trying to open a comm, and I was guessing that that was related, but it may not be.

@ramnathv , if you call display_html(file='...'), it should read the file in the kernel, so paths should be relative to the CWD. If you display <iframe src="...">, I think paths work relative to the directory containing the notebook, which should be the same as CWD if your code doesn't change it.

ramnathv commented 9 years ago

@takluyver currently we have only implemented communication protocols for shiny as the backend. In an R session (outside of Rstudio), it simply opens up an external browser and displays the saved HTML file. I suppose in a notebook context, we can do something different so that the file displays in the notebook (which is what I was trying to do using display_html).

ramnathv commented 9 years ago

@takluyver. The iframe trick works. Directly calling display_html(file = "") does not work, which I think makes sense.

takluyver commented 9 years ago

Are the communications for shiny HTTP requests, websocket messages, or something else? How practical would it be to do similar communications patterns over Jupyter comms? And would packages like rbokeh need changes to run within Jupyter, or would htmlwidgets abstract all the differences away?

(display_html(file = "...") ought to read the file and display the contents, but it will drop in into the existing page in a div, which may not work if the HTML in question is for an entire page. But this is probably irrelevant now.)

ramnathv commented 9 years ago

Shiny uses websockets. I think it is very feasible for us to implement comm protocols for Jupyter in htmlwidgets, thereby abstracting things away for packages that depend on it. This was the approach I was suggesting earlier.

As for the comm protocol, we need to know how to do two things. One, how to load js/css dependencies, and two, insert the relevant html div into the notebook. I think I know how to do (2). So (1) is where we would need help.

Another point of note, is how do notebooks handle redundancies in dependencies. So let us say I have one widget that has loaded jquery 1.10.1. Suppose another widget attempts to load jquery 1.11.1, can the notebook prevent duplicates? The iframe approach will take care of this since it sandboxes the pages, but I was wondering if the notebook architecture had robust dependency resolution mechanisms.

smartinsightsfromdata commented 8 years ago

@ramnathv I think I've missed this<iframe src="..."> trick. How does it work?

ramnathv commented 8 years ago

Here is how you would embed a htmlwidget as an iframe.

library(htmlwidgets)
library(rbokeh)
x = figure() %>% ly_points(cars$speed, cars$dist)
tf = 'myfigure.html'
saveWidget(x, file = tf, selfcontained = F)
IRdisplay::display_html(paste("<iframe src=' ", tf, " ' width = 100% height = 400"))

Note that I used selfcontained = F, since the notebook was unable to find the version of pandoc that comes installed with RStudio. You can set it to TRUE if pandoc is seen by the notebook.

flying-sheep commented 8 years ago

i really hate tempfiles.

let’s do this instead:

IRdisplay::display_html(toHTML(x))

and if it somehow need to be isolated, let’s use the isolated flag like we do for SVG

smartinsightsfromdata commented 8 years ago

While @ramnathv code works (adding "/>" at the end), I don't seem to be able to make to work @flying-sheep suggestion to use:

IRdisplay::display_html(toHTML(x))

toHTML is not a function available in any of the packages in my space (htmlwidgets, rboketh etc.). I also tried tools::toHTML and XML::toHTML but I get errors.

Which toHTML have you been using?

flying-sheep commented 8 years ago

oh, sorry, it’s not exported. but it has an exported alias:

IRdisplay::display_html(as.tags(x))
smartinsightsfromdata commented 8 years ago

@flying-sheep I can get to it with

IRdisplay::display_html(htmlwidgets:::as.tags.htmlwidget(x))

but I get:

Error in prepare_content(isbinary, data, file): Data needs to be a character vector

Realising that the toHTML referred to above refers to the internal function in htmlwidgets, I tried also:

IRdisplay::display_html(htmlwidgets:::toHTML(x))

But I get the same (not surprising as as.tags.htmlwidget calls toHTML)

Error in prepare_content(isbinary, data, file): Data needs to be a character vector
flying-sheep commented 8 years ago

it’s in 0.5. update htmlwidgets and you’ll have as.tags

this should work:

display_html(as.character(as.tags(x)))

or we can do sth. smart with htmltools::renderTags:

renderTags returns a list with the following variables:

head

An HTML string that should be included in <head>.

singletons

Character vector of singleton signatures that are known after rendering.

dependencies

A list of resolved htmlDependency objects.

html

An HTML string that represents the main HTML that was rendered.

flying-sheep commented 8 years ago

here’s a dumb, ad-hoc version that works at least for DiagrammR.

this displays how useful a dependency mechanism for jupyter notebooks would be :laughing:

library(DiagrammeR)
library(htmltools)
library(htmlwidgets)
library(repr)

repr_html.htmlwidget <- function(w) {
    tags <- renderTags(as.tags(w))

    deps <- ''

    for (dep in tags$dependencies) {
        if (!is.null(dep$script)) {
            f <- file.path(dep$src$file, dep$script)
            deps <- sprintf('%s\n<script>// %s\n%s</script>', deps, f, readChar(f, file.info(f)$size))
        }
        if (!is.null(dep$stylesheet)) {
            f <- file.path(dep$src$file, dep$stylesheet)
            deps <- sprintf('%s\n<style>/* %s */\n%s</style>', deps, f, readChar(f, file.info(f)$size))
        }
    }

    paste(deps, tags$html, '<script>HTMLWidgets.staticRender()</script>', sep = '\n');
}

g <- grViz("
  digraph {
    layout = twopi
    node [shape = circle]
    A -> {B C D} 
  }")

display_html(repr_html(g))
ramnathv commented 8 years ago

A dependency specification mechanism would be awesome. The wrapper function by @flying-sheep is a good start. However, it may not work always, since it simply inlines js/css assets, without paying attention to additional dependencies that these files might import. Writing a wrapper function that invokes pandoc to do the inlining and then embeds the html fragment as opposed to the whole page, should be easy to write.

I would still recommend the iframe route, in the absence of a dependency handling mechanism. It sandboxes the page and ensures that it displays exactly the way it should. One can also inline an entire iframe using the srcdoc argument, which works with most modern browsers. I have an ongoing PR in the htmlwidgets repo that adds robust iframe support, which we can take advantage of here.

tverbeke commented 8 years ago

The DiagrammeR example of @flying-sheep works in a fresh session if I change the last line to

IRdisplay::display_html(repr_html(g))

For the OP's question, this

library(htmlwidgets)
library(rbokeh)
x = figure() %>% ly_points(cars$speed, cars$dist)
IRdisplay::display_html(as.character(htmlwidgets:::as.tags.htmlwidget(x)))

will not display anything (but a white rectangle) with the following sessionInfo():

R version 3.2.2 (2015-08-14)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 14.04.3 LTS

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] rbokeh_0.2.3.2    htmlwidgets_0.5.1

loaded via a namespace (and not attached):
 [1] lattice_0.20-33 digest_0.6.8    IRdisplay_0.3   grid_3.2.2     
 [5] repr_0.4        jsonlite_0.9.17 magrittr_1.5    evaluate_0.8   
 [9] stringi_0.5-5   uuid_0.1-1      hexbin_1.27.1   IRkernel_0.5   
[13] tools_3.2.2     stringr_1.0.0   maps_3.0.0-2    yaml_2.1.13    
[17] base64enc_0.1-2 rzmq_0.7.7      htmltools_0.2.6
flying-sheep commented 8 years ago

Writing a wrapper function that invokes pandoc to do the inlining and then embeds the html fragment as opposed to the whole page, should be easy to write.

very inefficient. we should either push the dependency management or code some simple hack to fix this.

maybe it can be done with a simple helper that searches for script tags with ids, e.g.: <script id="d3-3.5.6" src="*"/>, and inserts them, then calls some optional JS onload.

we should be able to chain-load or parallelly-load them, which is easy with promise code.

// event to promise
const once = (emitter, event) =>
  new Promise((resolve, reject) =>
    emitter.addEventListener(event, resolve))

function load_script(id, url) {
  const existing = document.getElementById(id)
  if (existing)
    return Promise.resolve(existing)

  const script = document.createElement('script')
  script.setAttribute('id', id)
  script.setAttribute('src', url)
  document.head.appendChild(script)
  return once(script, 'load')
}

function load_dependencies(deps, callback) {
  let promise = null
  //sequentially load all deps in the array
  if (Array.isArray(deps)) {
    promise = load_dependencies(deps.shift())
    if (deps.length > 0) promise = promise.then(load_dependencies(deps))
  //parallelly load all deps in the object
  } else {
    promise = Promise.all(Object.keys(deps).map(id => load_script(id, deps[id])))
  }
  if (callback) promise = promise.then(callback)
  return promise
}

//example: parallelly load underscore and d3,
//after both are finished, load react (makes no sense but whatever)
load_dependencies([
  {
    '_-1.8.3': 'http://...',
    'd3-3.5.6': 'http://...',
  },
  { 'react-1.4.0': 'http://' }
])

missing from above code

jankatins commented 8 years ago

I think the

> IRkernel::main()
[1] "Got unhandled msg_type:" "comm_open"

comes form somethign different, I get that as well on any kernel startup... Will open a new issue for that...