diff --git a/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyBaseExt.jl b/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyBaseExt.jl index 57ecf1c..a880295 100644 --- a/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyBaseExt.jl +++ b/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyBaseExt.jl @@ -3,17 +3,60 @@ module QuartoNotebookWorkerPlotlyBaseExt import QuartoNotebookWorker import PlotlyBase -QuartoNotebookWorker._mimetype_wrapper(p::PlotlyBase.Plot) = PlotlyBasePlot(p) +struct PlotlyBasePlotWithoutRequireJS + plot::PlotlyBase.Plot +end + +struct PlotlyRequireJSConfig end + +const FIRST_PLOT_DISPLAYED = Ref(false) + +function QuartoNotebookWorker.expand(p::PlotlyBase.Plot) + + plotcell = QuartoNotebookWorker.Cell(PlotlyBasePlotWithoutRequireJS(p)) + + # Quarto expects that the require.js preamble which Plotly needs to function + # comes in its own cell, which will then be hoisted into the HTML page header. + # So we cannot have that preamble concatenated with every plot's HTML content. + # Instead, e keep track whether a Plotly plot is the first per notebook, and in that + # case have it expand into the preamble cell and the plot cell. If it's not the + # first time, we expand only into the plot cell. + cells = if !FIRST_PLOT_DISPLAYED[] + [QuartoNotebookWorker.Cell(PlotlyRequireJSConfig()), plotcell] + else + [plotcell] + end + + FIRST_PLOT_DISPLAYED[] = true -struct PlotlyBasePlot <: QuartoNotebookWorker.WrapperType - value::PlotlyBase.Plot + return cells end -function Base.show(io::IO, ::MIME"text/html", wrapper::PlotlyBasePlot) +function Base.show(io::IO, ::MIME"text/html", p::PlotlyBasePlotWithoutRequireJS) # We want to embed only the minimum markup needed to render the # plotlyjs plots, otherwise a full HTML page is generated for every # plot which does not render correctly in our context. - PlotlyBase.to_html(io, wrapper.value; include_plotlyjs = "require", full_html = false) + # "require-loaded" means that we pass the require.js preamble ourselves. + PlotlyBase.to_html(io, p.plot; include_plotlyjs = "require-loaded", full_html = false) +end + +Base.show(io::IO, M::MIME, p::PlotlyBasePlotWithoutRequireJS) = show(io, M, p.plot) +Base.show(io::IO, m::MIME"text/plain", p::PlotlyBasePlotWithoutRequireJS) = + show(io, m, p.plot) +Base.showable(M::MIME, p::PlotlyBasePlotWithoutRequireJS) = showable(M, p.plot) + +function Base.show(io::IO, ::MIME"text/html", ::PlotlyRequireJSConfig) + print(io, PlotlyBase._requirejs_config()) +end + +function reset_first_plot_displayed_flag!() + FIRST_PLOT_DISPLAYED[] = false +end + +function __init__() + if ccall(:jl_generating_output, Cint, ()) == 0 + QuartoNotebookWorker.add_package_refresh_hook!(reset_first_plot_displayed_flag!) + end end end diff --git a/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyJSExt.jl b/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyJSExt.jl index 89db953..b25cb6e 100644 --- a/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyJSExt.jl +++ b/src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPlotlyJSExt.jl @@ -3,22 +3,6 @@ module QuartoNotebookWorkerPlotlyJSExt import QuartoNotebookWorker import PlotlyJS -QuartoNotebookWorker._mimetype_wrapper(p::PlotlyJS.SyncPlot) = PlotlyJSSyncPlot(p) - -struct PlotlyJSSyncPlot <: QuartoNotebookWorker.WrapperType - value::PlotlyJS.SyncPlot -end - -function Base.show(io::IO, ::MIME"text/html", wrapper::PlotlyJSSyncPlot) - # We want to embed only the minimum markup needed to render the - # plotlyjs plots, otherwise a full HTML page is generated for every - # plot which does not render correctly in our context. - PlotlyJS.PlotlyBase.to_html( - io, - wrapper.value.plot; - include_plotlyjs = "require", - full_html = false, - ) -end +QuartoNotebookWorker.expand(p::PlotlyJS.SyncPlot) = QuartoNotebookWorker.expand(p.plot) end diff --git a/test/testsets/integrations/PlotlyJS.jl b/test/testsets/integrations/PlotlyJS.jl index e98c132..6c9c272 100644 --- a/test/testsets/integrations/PlotlyJS.jl +++ b/test/testsets/integrations/PlotlyJS.jl @@ -6,7 +6,14 @@ if Sys.iswindows() else test_example(joinpath(@__DIR__, "../../examples/integrations/PlotlyJS.qmd")) do json cells = json["cells"] - for nth in (4, 6) + preamble_cell = cells[5] + outputs = preamble_cell["outputs"] + @test length(outputs) == 1 + data = outputs[1]["data"] + @test keys(data) == Set(["text/html", "text/plain"]) + @test startswith(data["text/html"], "