JuliaPlots / PlotlyJS.jl

Julia library for plotting with plotly.js
Other
423 stars 78 forks source link

plotlyjs.jl figures not rendering when exported as html #322

Open jdamiba opened 4 years ago

jdamiba commented 4 years ago

Describe the bug

Background: I work at Plotly, and I'm trying to figure out how to better document the plotlyjs.jl library alongside the plotly.jl library.

Issue: Figures I create using plotlyjs.jl in a Jupyter Notebook do not render when exported as .html files using nbconvert because WebIO is not present in the runtime.

My question Is: how can I get the figures I create with plotlyjs.jl in a jupyter notebook to render when exported to .html?

Screenshot:

Screen Shot 2020-04-20 at 2 04 12 PM

Steps to Reproduce: Create a figure using PlotlyJS.jl in a Jupyter Notebook with a Julia 1.4.1 kernel, then use jupyter nbconvert to transform the notebook into an .html file.

Version info

Screen Shot 2020-04-20 at 2 06 14 PM

sglyon commented 4 years ago

Hi @jdamiba thanks for filling out the issue template -- very helpful

I haven't tried this exact use case before...

Let's call in some webIO experts and see if they can help us sort this out

cc @shashi @travigd

nicolaskruchten commented 4 years ago

Thanks @sglyon ! If this cannot be made to work, is there some other known way of going from PlotlyJS.jl-plot-in-a-notebook to HTML?

twavv commented 4 years ago

WebIO doesn't have a way to export to static HTML. This is something we've talked about a bit, but it's hard because WebIO is really designed around interactive use cases (think widgets) which require JS to work.

Looking around at the codebase, it makes pretty heavy use of WebIO, so I'm not sure that it's possible.

What we might be able to do in WebIO would be to have a static_html function that tries to strip out all of the fancier stuff, but I'm not even sure if that would work since PlotlyJS.jl uses WebIO's import system to bootstrap the plot with JS.

@sglyon Does the application/vnd.plotly.v1+json MIME work?

nicolaskruchten commented 4 years ago

So the way this works in our Python integration is that our equivalent of WebIO inlines all of the Javascript in the resulting HTML, so this isn't totally "static" HTML :) This isn't something you've tried tackling in WebIO.jl/PlotlyJS.jl yet?

sglyon commented 4 years ago

Hey @nicolaskruchten thanks for stepping in

We do have a "static" html functionality built into the package. I haven't ever tried to get it set up for use with nbconvert. We can make it happen though!!

Can you describe how your python integration works with both active jupyter sessions and static html exports via nbconvert? Perhaps you utilize the plotly mimetype for jupyterlab (notebook too?) AND include a payload for text/html mimetype that nbconvert uses?

How do you guarantee that the html has plotly.js loaded (perhaps via script tag that points to cdn, or inline minified js?)? How do you ensure you only have one plotly.js load attempt if you have multiple plots in the exported notebook?

Sorry lots of questions, but I want to make sure we get to the bottom of this!

nicolaskruchten commented 4 years ago

I'm going to tag in @jonmmease here to answer the questions above :)

jonmmease commented 4 years ago

Hi @sglyon, these are definitely the right questions! In version 4 of plotly.py, we introduced a renderers framework that lets us support a bunch of different ways to display figures in a variety of contexts. The documentation for this is at https://plotly.com/python/renderers/. Here's the current set of renderers (with a few aliases removed): ['plotly_mimetype', 'notebook', 'notebook_connected', 'colab', 'cocalc', 'databricks', 'json', 'png', 'jpeg', 'jpg', 'svg', 'pdf', 'browser', 'firefox', 'chrome', 'chromium', 'iframe', 'iframe_connected', 'sphinx_gallery']

Renderers can be combined so that multiple representations are produced when a figure is displayed. For example, the default renderer (when no specialized environment is detected) is 'plotly_mimetype+notebook'.

plotly_mimetype

The plotly_mimetype renderer produces the application/vnd.plotly.v1+json bundle and works in environments that include support for rendering the mimetype. These include JupyterLab (with the jupyterlab-plotly extension), nteract, and vscode. In this case, we don't provide the plotly.js bundle as that is the responsibility of the front-end.

notebook and notebook_connected

The notebook renderer is designed to work in the classic Jupyter notebook and the output of nbconvert. The first time the renderer is used to display a figure, it adds an HTML snippet to the notebook that configures require.js to load the plotly.js bundle. notebook does this by inserting the full bundle and so it works offline but adds a few MB to the notebook size. notebook_connected points to a plotly.js CDN so it requires internet access but doesn't increase notebook size.

This require.js initialization is done in the HtmlRenderer.activate method https://github.com/plotly/plotly.py/blob/5b0e8d3ccab55fe1a6e4ba123cfc9d718a9ffc5a/packages/python/plotly/plotly/io/_base_renderers.py#L267-L324.

Then each time a figure is displayed, it accesses plotly.js using requirejs (assuming that the init code was run to register plotly.js with require.js). This is done in the plotly.io.to_html function https://github.com/plotly/plotly.py/blob/5b0e8d3ccab55fe1a6e4ba123cfc9d718a9ffc5a/packages/python/plotly/plotly/io/_html.py#L265-L267.

require.js is available in the classic notebook and in the default output of nbconvert. People do run into problems sometimes though when they include the nbconvert results in a custom template that doesn't include, or isn't compatible with, require.js.

colab and databricks

Some Jupyter-like environments don't support the plotly mimetype or require.js. In these cases we have custom renderers tailored for the environemnt. colab, for example, displays the results of each notebook cell in a self-contained html document in an iframe. So in that case we output a full html document and use a script tag to pull plotly.js from a CDN. Databricks provides their own custom displayHTML function that needs to be used instead of the IPython.display mechanism, and so we have a custom renderer for that environment.

Hope that's helpful! It's not a simple problem unfortunately, and there may be simpler solutions than what we've figured out over the years.. Let me know if you have any other questions on what we're doing.

sglyon commented 4 years ago

Thanks all!

That is very helpful info. I will not be able to work on this too much in the short run, but would be happy to support anyone who is brave enough to take it on.

Thanks

akdor1154 commented 4 years ago

I think Plots.jl can already basically do this with the basic plotly backend, I'm gonna try and strip it out and create a MWE over PlotlyJS.

akdor1154 commented 4 years ago

Cool so @sglyon I have basic functionality working with only the PlotlyBase package.

import UUIDs
function plotly_html_body(plt)
    uuid = UUIDs.uuid4()
    html = """
        <div id=\"$(uuid)\"></div>
        <script>
        PLOT = document.getElementById('$(uuid)');
        require(['https://cdn.plot.ly/plotly-latest.min.js'], function(plotly) {
            plotly.plot(PLOT, $(string(plt.data)), $(string(plt.layout)));
        }, function(err) { console.error(err); PLOT.innerText = err; PLOT.classList.add("error", "output_stderr") } )
        </script>
    """
    html
end

Base.show(io::IO, ::MIME"text/html", p::PlotlyBase.Plot) = write(io, plotly_html_body(p))

This nearly works. Unfortunately, it looks like you are overriding IJulia.display_dict in PlotlyBase, and preventing if from detecting that it can now render text/html. If I add in a sneaky

Base.delete_method(@which IJulia.display_dict(plot))

Then this results in working plots in jupyter notebooks, that continue to work on static html exports, with none of PlotlyJS required at all (which is great in my case because I don't care for WebIO, Blink etc.).

Is there any chance you could consider removing the display_dict override in PlotlyBase.jl?

nicolaskruchten commented 4 years ago

Sidenote: it's neat to see the layering work being done here, and it mirrors certain aspects of our Python system, where we have plotly.io.show( <bare dict representation of figure> ) as well as fig.show() which internally calls the former :)

akdor1154 commented 4 years ago

Is there any chance you could consider removing the display_dict override in PlotlyBase.jl?

@sglyon ping on this :) I'm happy to submit a PR, just want to check that it would be accepted as it makes PlotlyBase.jl possible to use directly in jupyter notebooks, which might be a change in project direction. (imo a useful one, but I'm not the author of these packages!)

sglyon commented 4 years ago

HI @akdor1154 sorry for delay -- haven't had time to look at this one yet.

I will hopefully find time soon and will ping you when I do

alexlenail commented 3 years ago

What's the verdict on how to get plotly plots to render both in jupyter lab and exported HTML, when using PlotlyJS directly? (rather than using Plots; plotlyjs())

Naively attempting to follow @akdor1154's suggestion, I get an error:

MethodError: no method matching display_dict(::PlotlyJS.SyncPlot)
Closest candidates are:
  display_dict(::Plot) at /Users/alex/.julia/packages/PlotlyBase/GDbp9/src/PlotlyBase.jl:110

Which results from deleting that method.

alexlenail commented 3 years ago

@sglyon ?

PrParadox commented 5 months ago

@sglyon

Working Solution:

using PlotlyJS

p = plot(scatter(x=1:10, y=1:10));
io = PipeBuffer()
PlotlyBase.to_html(io, p.plot, full_html=false)
HTML(read(io, String))

This shows the plot in Jupyter lab itself properly. After using nbconvert, make sure to add <script src="https://cdn.plot.ly/plotly-2.3.0.min.js"></script> in <head> of html file produced by nbconvert (can be automatized with a template file). This is to ensure that plotly-2.3.0.min.js script is loaded before the plot. Otherwise you will see a blank page. Another alternative is to make everything local with include_plotlyjs I presume.

1 - Copy

The produced plot is fully interactive.

2 - Copy

cserteGT3 commented 5 months ago
using PlotlyJS

p = plot(scatter(x=1:10, y=1:10));
io = PipeBuffer()
PlotlyBase.to_html(io, p.plot, full_html=false)
HTML(read(io, String))

This does not work for me. Nothing is shown in Jupyterlab and nothing after converting with nbconvert and copying the mentioned line in the html file. Edit: just revisited and I see the plot in Jupyterlab, but the page is blank in the converted html. I'm on julia v1.10.3, and:

[7073ff75] IJulia v1.24.2
[f0f68f2c] PlotlyJS v0.18.13

@PrParadox Do you have any idea, what could be the issue? (I included the plotly script in the final html.)

AUK1939 commented 2 months ago

@cserteGT3

I had the same issue. Make sure you add the script tag at the top of the head section

<head><meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> 
<title>My Notebook</title>
<script src="https://cdn.plot.ly/plotly-2.9.0.min.js"></script>