PumasAI / QuartoNotebookRunner.jl

MIT License
61 stars 8 forks source link

PlotlyJS not working for HTML output #129

Closed MHellmund closed 3 months ago

MHellmund commented 4 months ago

Problem: test.qmd:

---
engine: julia
---

```{julia}
using PlotlyJS

plot(rand(10,4))


`$ quarto render test.qmd --to html`
load `test.html` in browser  ==>  *no plot, error in javascript console:* `require not defined`

 - Solution A:  change "require" to "cdn" here: https://github.com/PumasAI/QuartoNotebookRunner.jl/blob/main/src/worker.jl#L815

- Solution B: Quarto has this mechanism: https://github.com/quarto-dev/quarto-cli/blob/main/src/core/jupyter/widgets.ts#L102-L105 but at the moment this is not triggered by the julia engine.
MichaelHatherly commented 4 months ago

Could you provide the test.html output that's failing, I've not managed to reproduce that myself @MHellmund.

MHellmund commented 4 months ago

test.zip produced as described above with quarto 1.5.32, julia 1.10.3, julia env includes PlotlyJS.jl 0.18.13

MichaelHatherly commented 3 months ago

@jkrumbiegel I think option B above is perhaps the right approach here.

MichaelHatherly commented 3 months ago

@MHellmund the workaround for now (if you've not done it already) would of course just be to include requirejs in the notebook frontmatter

include-in-header:
    text: |
        <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js" integrity="sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
MHellmund commented 3 months ago

OK, thank you. I'm trying to understand what quarto does.

In Jupyter, output display cells using javascript ("jupyter widgets") have their own mimetypes: https://github.com/quarto-dev/quarto-cli/blob/main/src/core/mime.ts#L20-L23

A cell with this mimetype triggers the 'solution B' mechanism via https://github.com/quarto-dev/quarto-cli/blob/main/src/core/jupyter/widgets.ts#L25-L43

IJulia already has a mimetype for Plotly: https://github.com/JuliaLang/IJulia.jl/blob/master/src/display.jl#L37-L41

Is this the way to go?

jkrumbiegel commented 3 months ago

I did some more exploration:

In Jupyter, output display cells using javascript ("jupyter widgets") have their own mimetypes: https://github.com/quarto-dev/quarto-cli/blob/main/src/core/mime.ts#L20-L23

That system exists, and we in principle have access to it, because our notebook object runs through the same function in quarto-cli that the jupyter backend uses, called juptyerToMarkdown:

https://github.com/quarto-dev/quarto-cli/blob/c001669480ce8cd0756eb49b74b11bd222077bcc/src/core/jupyter/jupyter.ts#L731

But the mechanism that plotly hits when I render a plot in a python jupyter notebook is not that using the jupyter widget mime type. Rather, quarto has some special code to detect if there's a plotly plot in a cell, as text/html mime type:

https://github.com/quarto-dev/quarto-cli/blob/c001669480ce8cd0756eb49b74b11bd222077bcc/src/core/jupyter/widgets.ts#L188-L196

I checked the ipynb output of a python notebook, and the first cell with a plotly plot has two html outputs. One with the require.js setup code, and one with the actual plot

---
keep-ipynb: true
---

```{python}
import plotly.express as px
import plotly.io as pio
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", 
                 color="species", template="simple_white")
fig.show()
fig.show()

<img width="1070" alt="image" src="https://github.com/PumasAI/QuartoNotebookRunner.jl/assets/22495855/8f3dc210-9690-407d-960a-31fb9ee142aa">

When I show the same plot a second time, that cell doesn't carry the setup code a second time, so there's something checking whether the first plotly plot is being rendered

<img width="737" alt="image" src="https://github.com/PumasAI/QuartoNotebookRunner.jl/assets/22495855/558c4312-c8cc-4182-80e1-db88045d8058">

The quarto special-casing code then makes the first setup code block end up in the `<head>`. You can see the setup code at the top, at the end of the `<head>` section, and the other `text/html` block with the plot at the bottom, in the `<body>`.

<img width="778" alt="image" src="https://github.com/PumasAI/QuartoNotebookRunner.jl/assets/22495855/10d11535-0226-4d4f-86ba-80f8d88f8f17">

If I just add that setup code manually in a cell above, it still doesn't work though:

<img width="935" alt="image" src="https://github.com/PumasAI/QuartoNotebookRunner.jl/assets/22495855/9bb914c0-0741-46f2-b6ae-790abab6620e">

The quarto code seems to suggest requires.js is only added if either a javascript widget or a jupyter widget is detected:

https://github.com/quarto-dev/quarto-cli/blob/c001669480ce8cd0756eb49b74b11bd222077bcc/src/core/jupyter/widgets.ts#L102-L105

I then saw that some code referring to includes and dependencies that `jupyter.ts` uses is missing from `julia.ts`. I'm opening a PR to add that back in, it seems to be able to just reuse the logic. With that, I could get a PlotlyJS plot going:

engine: julia format: html keep-ipynb: true

struct AsHTML
    s::String
end

Base.show(io::IO, ::MIME"text/html", a::AsHTML) = print(io, a.s)

AsHTML("""
<script type="text/javascript">
window.PlotlyConfig = {MathJaxConfig: 'local'};
if (window.MathJax && window.MathJax.Hub && window.MathJax.Hub.Config) {window.MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}
if (typeof require !== 'undefined') {
require.undef("plotly");
requirejs.config({
    paths: {
        'plotly': ['https://cdn.plot.ly/plotly-2.32.0.min']
    }
});
require(['plotly'], function(Plotly) {
    window._Plotly = Plotly;
});
}
</script>
""")
using PlotlyJS

plot(rand(10,4))


<img width="926" alt="image" src="https://github.com/PumasAI/QuartoNotebookRunner.jl/assets/22495855/932bc222-54f4-4add-b7a7-ebf5df2ccaae">

Note that even with such a PR merged, we still need to include the setup code somehow, but that would be a QuartoNotebookRunner thing I think.
MichaelHatherly commented 3 months ago

Note that even with such a PR merged, we still need to include the setup code somehow, but that would be a QuartoNotebookRunner thing I think.

Yes, seems fine that way.