From 83b62bcfe342c7e9a68eb14e4f73b4d0a725284f Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Thu, 5 Oct 2023 18:31:57 +0200 Subject: [PATCH 1/3] First draft --- Project.toml | 4 +- src/PlutoPlotly.jl | 4 ++ src/basics.jl | 15 +--- src/js_helpers.jl | 10 +-- src/main_struct.jl | 166 ++++++++++++++++++++++++++++++++------------- src/show.jl | 58 ++++++---------- 6 files changed, 152 insertions(+), 105 deletions(-) diff --git a/Project.toml b/Project.toml index 66b1522..6dde03c 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ AbstractPlutoDingetjes = "6e696c72-6542-2067-7265-42206c756150" BaseDirs = "18cc8868-cbac-4acf-b575-c8ff214dc66f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" HypertextLiteral = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" @@ -15,6 +16,7 @@ LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5" +PlutoDevMacros = "a0499f29-c39b-4c5c-807c-88074221b949" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" @@ -27,7 +29,7 @@ PlotlyKaleidoExt = "PlotlyKaleido" UnitfulExt = "Unitful" [compat] -AbstractPlutoDingetjes = "1" +AbstractPlutoDingetjes = "1.2" BaseDirs = "1" Colors = "0.12" HypertextLiteral = "0.9.4" diff --git a/src/PlutoPlotly.jl b/src/PlutoPlotly.jl index 09290ca..69b4a16 100644 --- a/src/PlutoPlotly.jl +++ b/src/PlutoPlotly.jl @@ -5,6 +5,7 @@ using Reexport using HypertextLiteral using AbstractPlutoDingetjes +using AbstractPlutoDingetjes.Display using Dates using BaseDirs using TOML @@ -13,6 +14,9 @@ using LaTeXStrings using Markdown using Downloads: download +using PlutoDevMacros.PlutoCombineHTL.WithTypes +using Dictionaries + export PlutoPlot, get_plotly_version, change_plotly_version, check_plotly_version, force_pluto_mathjax_local, htl_js, add_plotly_listener!, add_class!, remove_class!, add_js_listener! diff --git a/src/basics.jl b/src/basics.jl index f53989d..716d6b1 100644 --- a/src/basics.jl +++ b/src/basics.jl @@ -1,9 +1,5 @@ maybe_publish_to_js(x) = if is_inside_pluto() - if isdefined(Main.PlutoRunner, :core_published_to_js) - Main.PlutoRunner.PublishedToJavascript(x) - else - Main.PlutoRunner.publish_to_js(x) - end + published_to_js(x) else x end @@ -14,15 +10,6 @@ else Base.UUID(zero(UInt128)) end -function Base.show(io::IO, mime::MIME"text/html", s::JS) - if is_inside_pluto() - show(io, mime, Markdown.MD(Markdown.Code("js",s.content))) - else - show(io, MIME"text/plain",s) - end -end - - ## Plotly Version ## function change_plotly_version(ver::String) maybe_add_plotly_local(ver) diff --git a/src/js_helpers.jl b/src/js_helpers.jl index 5cb497d..1e4c32d 100644 --- a/src/js_helpers.jl +++ b/src/js_helpers.jl @@ -24,9 +24,9 @@ console.log(PLOT) // logs the plot div inside the developer console when pressin \"\"\" ``` """ -function add_js_listener!(p::PlutoPlot, event_name::String, listener::JS) +function add_js_listener!(p::PlutoPlot, event_name::String, listener::String) ldict = p.js_listeners - listeners_array = get!(ldict, event_name, JS[]) + listeners_array = get!(ldict, event_name, String[]) push!(listeners_array, listener) return p end @@ -72,7 +72,7 @@ end Add script contents contained in collection `items` at the end of the plot show method script. The `item` must either be a collection of `String` or `HypertextLiteral.JavaScript` elements """ -function push_script!(p::PlutoPlot, items::Vararg{JS,N}) where N +function push_script!(p::PlutoPlot, items::Vararg{Union{Script, ScriptContent, Function},N}) where N @nospecialize push!(p.script_contents.vec, items...) return p @@ -104,9 +104,9 @@ console.log(PLOT) // logs the plot div inside the developer console \"\"\" ``` """ -function add_plotly_listener!(p::PlutoPlot, event_name::String, listener::JS) +function add_plotly_listener!(p::PlutoPlot, event_name::String, listener::String) ldict = p.plotly_listeners - listeners_array = get!(ldict, event_name, JS[]) + listeners_array = get!(ldict, event_name, String[]) push!(listeners_array, listener) return p end diff --git a/src/main_struct.jl b/src/main_struct.jl index 1a8071b..6187101 100644 --- a/src/main_struct.jl +++ b/src/main_struct.jl @@ -1,56 +1,57 @@ -const PLOTLY_VERSION = Ref("2.25.2") -const JS = HypertextLiteral.JavaScript +const PLOTLY_VERSION = Ref("2.26.1") -""" - ScriptContents -Wrapper around a vector of `HypertextLiteral.JavaScript` elements. It has a custom print implementation of `HypertextLiteral.print_script` in order to allow serialization of its various elements inside a script tag. +const SCType = Dictionary{String, Union{Function, Script}} -It is used inside the PlutoPlot to allow modularity and ease customization of the script contents that is used to generate the plotlyjs plot in Javascript. -""" -struct ScriptContents - vec::Vector{JS} -end +# This will contain the default contents of the script generating the plot +const _default_script_contents = SCType() -function HypertextLiteral.print_script(io::IO, value::ScriptContents) - for el ∈ value.vec - print(io, el.content, '\n') - end -end +# We declare it here but define it later +function _load_data_JS end -""" - htl_js(x) -Simple convenience constructor for `HypertextLiteral.JavaScript` objects, renamed and re-exported from HypertextLiteral for convenience in case HypertextLiteral is not explicitly loaded alongisde PlutoPlotly. - -See also: [`add_plotly_listeners!`](@ref) -""" -htl_js(x) = HypertextLiteral.JavaScript(x) +set!(_default_script_contents, "publish_to_js", _load_data_JS) -const _default_script_contents = htl_js.([ - """ +# Preamble +set!(_default_script_contents, "PLOT variable", DualScript( + # This is rendered inside of Pluto + PlutoScript(""" // Flag to check if this cell was manually ran or reactively ran const firstRun = this ? false : true const PLOT = this ?? document.createElement("div"); const parent = currentScript.parentElement const isPlutoWrapper = parent.classList.contains('raw-html-wrapper') - """, - """ + if (firstRun) { // It seem plot divs would not autosize themself inside flexbox containers without this parent.appendChild(PLOT) } - """, - """ + """), + # This is rendered outside of Pluto + NormalScript(""" + const PLOT = document.createElement("div") + """) +)) + +# Adjust width/height +set!(_default_script_contents, "adjust widht/height", DualScript( + # This is rendered inside of Pluto + PlutoScript(""" // If width is not specified, set it to 100% PLOT.style.width = plot_obj.layout.width ? "" : "100%" // For the height we have to also put a fixed value in case the plot is put on a non-fixed-size container (like the default wrapper) PLOT.style.height = plot_obj.layout.height ? "" : (isPlutoWrapper || parent.clientHeight == 0) ? "400px" : "100%" - """, - """ - - + """), + # This is rendered outside of Pluto + NormalScript() +)) + +# classlist +set!(_default_script_contents, "classlist", DualScript( + # This is rendered inside of Pluto + PlutoScript(""" PLOT.classList.forEach(cn => { + // This is necessary for cleaning up the classlist if (cn !== 'js-plotly-plot' && !custom_classlist.includes(cn)) { PLOT.classList.toggle(cn, false) } @@ -58,9 +59,20 @@ const _default_script_contents = htl_js.([ for (const className of custom_classlist) { PLOT.classList.toggle(className, true) } - """, - """ - + """), + # This is rendered outside of Pluto + NormalScript(""" + for (const className of custom_classlist) { + PLOT.classList.toggle(className, true) + } + """) +)) + +# resizeObserver +set!(_default_script_contents, "resizeObserver", DualScript( + # This is rendered inside of Pluto + PlutoScript(; + body = """ // Create the resizeObserver to make the plot even more responsive! :magic: const resizeObserver = new ResizeObserver(entries => { PLOT.style.height = plot_obj.layout.height ? "" : @@ -74,8 +86,19 @@ const _default_script_contents = htl_js.([ resizeObserver.observe(PLOT) """, - """ - + invalidation = """ + // Remove the resizeObserver + resizeObserver.disconnect() + """), + # This is rendered outside of Pluto + NormalScript() +)) + +# Plotly.react +set!(_default_script_contents, "Plotly.react", let + # This is rendered inside of Pluto + ps = PlutoScript(; + body = """ Plotly.react(PLOT, plot_obj).then(() => { // Assign the Plotly event listeners for (const [key, listener_vec] of Object.entries(plotly_listeners)) { @@ -92,9 +115,7 @@ const _default_script_contents = htl_js.([ } ) """, - """ - - invalidation.then(() => { + invalidation = """ // Remove all plotly listeners PLOT.removeAllListeners() // Remove all JS listeners @@ -103,11 +124,13 @@ const _default_script_contents = htl_js.([ PLOT.removeEventListener(key, listener) } } - // Remove the resizeObserver - resizeObserver.disconnect() - }) - """, -]) + """) + # Outside of Pluto we simply re-use the body + ns = NormalScript(ps.body) + DualScript(ps, ns) +end) + +set!(_default_script_contents, "returned_element", DualScript(""; returned_element = "PLOT")) """ PlutoPlot(p::Plot; kwargs...) @@ -156,10 +179,10 @@ See also: [`ScriptContents`](@ref), [`add_js_listener!`](@ref), [`add_plotly_lis """ Base.@kwdef struct PlutoPlot Plot::PlotlyBase.Plot - plotly_listeners::Dict{String, Vector{JS}} = Dict{String, Vector{JS}}() - js_listeners::Dict{String, Vector{JS}} = Dict{String, Vector{JS}}() + plotly_listeners::Dict{String, Vector{String}} = Dict{String, Vector{String}}() + js_listeners::Dict{String, Vector{String}} = Dict{String, Vector{String}}() classList::Vector{String} = String[] - script_contents::ScriptContents = ScriptContents(deepcopy(_default_script_contents)) + script_contents::SCType = deepcopy(_default_script_contents) end PlutoPlot(p::PlotlyBase.Plot; kwargs...) = PlutoPlot(;kwargs..., Plot = p) @@ -175,4 +198,51 @@ end function plot(args...;kwargs...) @nospecialize PlutoPlot(Plot(args...;kwargs...)) +end + +_pluto_default_iocontext() = try + Main.PlutoRunner.default_iocontext +catch + IOContext(devnull) +end + +# Create a ScriptContent using the IOContext from PlutoRunner (to avoid breaking core_publish_to_js) +_pluto_sc(h::HypertextLiteral.Result) = ScriptContent(h; iocontext = _pluto_default_iocontext()) + +# Load data from the PlutoPlot object to JavaScript +function _load_data_JS(pp::PlutoPlot; script_id = "pluto-plotly-div", ver = PLOTLY_VERSION[], kwargs...) + common = @htl(""" + + """) + ps = PlutoScript(@htl(""" + + """) |> _pluto_sc) + ns = NormalScript(ScriptContent(common)) + return DualScript(ps, ns; id = script_id) end \ No newline at end of file diff --git a/src/show.jl b/src/show.jl index 53dca33..174f9fd 100644 --- a/src/show.jl +++ b/src/show.jl @@ -1,42 +1,26 @@ -function _show(pp::PlutoPlot; script_id = "pluto-plotly-div", ver = PLOTLY_VERSION[]) -@htl """ - -""" +function _show(pp::PlutoPlot; kwargs...) + make_script([ + x isa Function ? x(pp; kwargs...) : x for x in pp.script_contents + ]) end -# ╔═╡ d42d4694-e05d-4e0e-a198-79a3a5cb688a function Base.show(io::IO, mime::MIME"text/html", plt::PlutoPlot) show(io, mime, _show(plt; script_id = plotly_script_id(io))) # show(io, mime, _show(plt)) -end \ No newline at end of file +end + +function Base.show(io::IO, mime::MIME"text/javascript", plt::PlutoPlot) + if is_inside_pluto(io) + show(io, mime, published_to_js(_preprocess(plt))) + else + # We use HypertextLiteral to print the Dict out f _preprocess + HypertextLiteral.print_script(io, _preprocess(plt)) + end +end + +#= Fix for Julia 1.10 +The `@generated` `print_script` from HypertextLiteral is broken in 1.10 +See [issue 33](https://github.com/JuliaPluto/HypertextLiteral.jl/issues/33) +=# +HypertextLiteral.print_script(io::IO, p::Display._PublishToJS) = show(io, MIME"text/javascript"(), p) +HypertextLiteral.print_script(io::IO, p::PlutoPlot) = show(io, MIME"text/javascript"(), p) \ No newline at end of file From 9792ab746ac6e67140ca7f0e6422896cf5d1e053 Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Sun, 8 Oct 2023 19:01:06 +0200 Subject: [PATCH 2/3] working version, with show in vscode --- src/PlutoPlotly.jl | 2 + src/basics.jl | 2 +- src/default_scriptcontents.jl | 179 +++++++++++++++++++++++++++++++++ src/local_plotly_library.jl | 2 +- src/main_struct.jl | 180 ---------------------------------- src/show.jl | 60 +++++++++--- 6 files changed, 231 insertions(+), 194 deletions(-) create mode 100644 src/default_scriptcontents.jl diff --git a/src/PlutoPlotly.jl b/src/PlutoPlotly.jl index 69b4a16..fffd9cf 100644 --- a/src/PlutoPlotly.jl +++ b/src/PlutoPlotly.jl @@ -15,6 +15,7 @@ using Markdown using Downloads: download using PlutoDevMacros.PlutoCombineHTL.WithTypes +using PlutoDevMacros.PlutoCombineHTL: plutodefault, print_javascript, print_html using Dictionaries export PlutoPlot, get_plotly_version, change_plotly_version, @@ -26,6 +27,7 @@ export make_subplots include("local_plotly_library.jl") include("main_struct.jl") +include("default_scriptcontents.jl") include("basics.jl") include("mathjax.jl") include("preprocess.jl") diff --git a/src/basics.jl b/src/basics.jl index 716d6b1..82088bd 100644 --- a/src/basics.jl +++ b/src/basics.jl @@ -86,7 +86,7 @@ end ## Unique Counter ## function unique_io_counter(io::IO, identifier = "script_id") - !get(io, :is_pluto, false) && return -1 # We simply return -1 if not inside pluto + is_inside_pluto(io) || return -1 # We simply return -1 if not inside pluto # We extract (or create if not existing) a dictionary that will keep track of instances of the same script name dict = get_IO_DICT(io) # We use the objectid as the key diff --git a/src/default_scriptcontents.jl b/src/default_scriptcontents.jl new file mode 100644 index 0000000..dfb935b --- /dev/null +++ b/src/default_scriptcontents.jl @@ -0,0 +1,179 @@ +# This will simply load the plutoplot data in javascript +set!(_default_script_contents, "publish_to_js", (pp::PlutoPlot; kwargs...) -> let + pts = PrintToScript(pp) + sc = ScriptContent(""" + // We have to convert all typedarrays in the layout to normal arrays. See Issue #25 + // We use lodash for this for compactness + function removeTypedArray(o) { + return _.isTypedArray(o) ? Array.from(o) : + _.isPlainObject(o) ? _.mapValues(o, removeTypedArray) : + o + } + + if (_.has(plot_obj, "layout")) { + plot_obj.layout = removeTypedArray(plot_obj.layout) + } + """) + ds = DualScript(sc, sc) + [pts, ds] +end) + +# Load the plotly library +set!(_default_script_contents, "load plotly library", (pp; ver = PLOTLY_VERSION[], kwargs...) -> let + sc = ScriptContent(""" + // Load the plotly library + let Plotly = undefined + try { + let _mod = await import('$(get_plotly_src("$ver", "local"))') + Plotly = _mod.default + } catch (e) { + console.log("Local load failed, trying with the web esm.sh version") + let _mod = await import('$(get_plotly_src("$ver", "esm"))') + Plotly = _mod.default + } + """) + # We don't need a DualScript here as we have the same on both and we don't + # need to eventually load pluto compat. + PrintToScript(sc) +end) + +# Assign Script id +set!(_default_script_contents, "assign script id", (pp; script_id = missing, kwargs...) -> let + DualScript(""; id = script_id) +end) + +# Force local mathjax (just in Pluto). +#= +We make this a function because we want to load the _current_ value of +`force_pluto_mathjax_local` at each run. +=# +set!(_default_script_contents, "force mathjax local", (pp; kwargs...) -> let + sc = ScriptContent(""" + // Check if we have to force local mathjax font cache + if ($(force_pluto_mathjax_local()) && window?.MathJax?.config?.svg?.fontCache === 'global') { + window.MathJax.config.svg.fontCache = 'local' + } + """) + # This will only go on Pluto + PlutoScript(sc) +end) + +# Preamble +set!(_default_script_contents, "PLOT variable", DualScript( + # This is rendered inside of Pluto + PlutoScript(""" + // Flag to check if this cell was manually ran or reactively ran + const firstRun = this ? false : true + const PLOT = this ?? document.createElement("div"); + const parent = currentScript.parentElement + const isPlutoWrapper = parent.classList.contains('raw-html-wrapper') + + if (firstRun) { + // It seem plot divs would not autosize themself inside flexbox containers without this + parent.appendChild(PLOT) + } + """), + # This is rendered outside of Pluto + NormalScript(""" + const PLOT = document.createElement("div") + """); + returned_element = "PLOT", +)) + +# Adjust width/height +set!(_default_script_contents, "adjust widht/height", let + # We only do this inside of Pluto + PlutoScript(""" + // If width is not specified, set it to 100% + PLOT.style.width = plot_obj.layout.width ? "" : "100%" + + // For the height we have to also put a fixed value in case the plot is put on a non-fixed-size container (like the default wrapper) + PLOT.style.height = plot_obj.layout.height ? "" : + (isPlutoWrapper || parent.clientHeight == 0) ? "400px" : "100%" + """) +end) + +# classlist +set!(_default_script_contents, "classlist", let + # This script will be used for iterating the classlists and assigning them + ds = DualScript( + # This is rendered inside of Pluto + PlutoScript(""" + PLOT.classList.forEach(cn => { + // This is necessary for cleaning up the classlist + if (cn !== 'js-plotly-plot' && !custom_classlist.includes(cn)) { + PLOT.classList.toggle(cn, false) + } + }) + for (const className of custom_classlist) { + PLOT.classList.toggle(className, true) + } + """), + # This is rendered outside of Pluto + NormalScript(""" + for (const className of custom_classlist) { + PLOT.classList.toggle(className, true) + } + """) + ) +end) + +# resizeObserver +set!(_default_script_contents, "resizeObserver", let + # This is rendered inside of Pluto + PlutoScript(; + body = """ + // Create the resizeObserver to make the plot even more responsive! :magic: + const resizeObserver = new ResizeObserver(entries => { + PLOT.style.height = plot_obj.layout.height ? "" : + (isPlutoWrapper || parent.clientHeight == 0) ? "400px" : "100%" + /* + The addition of the invalid argument `plutoresize` seems to fix the problem with calling `relayout` simply with `{autosize: true}` as update breaking mouse relayout events tracking. + See https://github.com/plotly/plotly.js/issues/6156 for details + */ + Plotly.relayout(PLOT, {..._.pick(PLOT.layout, ['width','height']), autosize: true, plutoresize: true}) + }) + + resizeObserver.observe(PLOT) + """, + invalidation = """ + // Remove the resizeObserver + resizeObserver.disconnect() + """) +end) + +# Plotly.react +set!(_default_script_contents, "Plotly.react", let + # This is rendered inside of Pluto + ps = PlutoScript(; + body = """ + Plotly.react(PLOT, plot_obj).then(() => { + // Assign the Plotly event listeners + for (const [key, listener_vec] of Object.entries(plotly_listeners)) { + for (const listener of listener_vec) { + PLOT.on(key, listener) + } + } + // Assign the JS event listeners + for (const [key, listener_vec] of Object.entries(js_listeners)) { + for (const listener of listener_vec) { + PLOT.addEventListener(key, listener) + } + } + } + ) + """, + invalidation = """ + // Remove all plotly listeners + PLOT.removeAllListeners() + // Remove all JS listeners + for (const [key, listener_vec] of Object.entries(js_listeners)) { + for (const listener of listener_vec) { + PLOT.removeEventListener(key, listener) + } + } + """) + # Outside of Pluto we simply re-use the body + ns = NormalScript(ps.body) + DualScript(ps, ns) +end) \ No newline at end of file diff --git a/src/local_plotly_library.jl b/src/local_plotly_library.jl index f0ee444..248702a 100644 --- a/src/local_plotly_library.jl +++ b/src/local_plotly_library.jl @@ -19,7 +19,7 @@ end function maybe_put_plotly_in_pluto(v) name = get_local_name(v) pluto_path = pluto_server_folder() - pluto_path !== nothing || return false + pluto_path === nothing && return true maybe_add_plotly_local(v) # We check whether the plotly library has been already loaded in this Pluto location, and we copy it otherwise for subdir in ("frontend-dist", "frontend") diff --git a/src/main_struct.jl b/src/main_struct.jl index 303d322..62ff0dd 100644 --- a/src/main_struct.jl +++ b/src/main_struct.jl @@ -5,132 +5,6 @@ const SCType = Dictionary{String, Union{Function, Script}} # This will contain the default contents of the script generating the plot const _default_script_contents = SCType() -# We declare it here but define it later -function _load_data_JS end - -set!(_default_script_contents, "publish_to_js", _load_data_JS) - -# Preamble -set!(_default_script_contents, "PLOT variable", DualScript( - # This is rendered inside of Pluto - PlutoScript(""" - // Flag to check if this cell was manually ran or reactively ran - const firstRun = this ? false : true - const PLOT = this ?? document.createElement("div"); - const parent = currentScript.parentElement - const isPlutoWrapper = parent.classList.contains('raw-html-wrapper') - - if (firstRun) { - // It seem plot divs would not autosize themself inside flexbox containers without this - parent.appendChild(PLOT) - } - """), - # This is rendered outside of Pluto - NormalScript(""" - const PLOT = document.createElement("div") - """) -)) - -# Adjust width/height -set!(_default_script_contents, "adjust widht/height", DualScript( - # This is rendered inside of Pluto - PlutoScript(""" - // If width is not specified, set it to 100% - PLOT.style.width = plot_obj.layout.width ? "" : "100%" - - // For the height we have to also put a fixed value in case the plot is put on a non-fixed-size container (like the default wrapper) - PLOT.style.height = plot_obj.layout.height ? "" : - (isPlutoWrapper || parent.clientHeight == 0) ? "400px" : "100%" - """), - # This is rendered outside of Pluto - NormalScript() -)) - -# classlist -set!(_default_script_contents, "classlist", DualScript( - # This is rendered inside of Pluto - PlutoScript(""" - PLOT.classList.forEach(cn => { - // This is necessary for cleaning up the classlist - if (cn !== 'js-plotly-plot' && !custom_classlist.includes(cn)) { - PLOT.classList.toggle(cn, false) - } - }) - for (const className of custom_classlist) { - PLOT.classList.toggle(className, true) - } - """), - # This is rendered outside of Pluto - NormalScript(""" - for (const className of custom_classlist) { - PLOT.classList.toggle(className, true) - } - """) -)) - -# resizeObserver -set!(_default_script_contents, "resizeObserver", DualScript( - # This is rendered inside of Pluto - PlutoScript(; - body = """ - // Create the resizeObserver to make the plot even more responsive! :magic: - const resizeObserver = new ResizeObserver(entries => { - PLOT.style.height = plot_obj.layout.height ? "" : - (isPlutoWrapper || parent.clientHeight == 0) ? "400px" : "100%" - /* - The addition of the invalid argument `plutoresize` seems to fix the problem with calling `relayout` simply with `{autosize: true}` as update breaking mouse relayout events tracking. - See https://github.com/plotly/plotly.js/issues/6156 for details - */ - Plotly.relayout(PLOT, {..._.pick(PLOT.layout, ['width','height']), autosize: true, plutoresize: true}) - }) - - resizeObserver.observe(PLOT) - """, - invalidation = """ - // Remove the resizeObserver - resizeObserver.disconnect() - """), - # This is rendered outside of Pluto - NormalScript() -)) - -# Plotly.react -set!(_default_script_contents, "Plotly.react", let - # This is rendered inside of Pluto - ps = PlutoScript(; - body = """ - Plotly.react(PLOT, plot_obj).then(() => { - // Assign the Plotly event listeners - for (const [key, listener_vec] of Object.entries(plotly_listeners)) { - for (const listener of listener_vec) { - PLOT.on(key, listener) - } - } - // Assign the JS event listeners - for (const [key, listener_vec] of Object.entries(js_listeners)) { - for (const listener of listener_vec) { - PLOT.addEventListener(key, listener) - } - } - } - ) - """, - invalidation = """ - // Remove all plotly listeners - PLOT.removeAllListeners() - // Remove all JS listeners - for (const [key, listener_vec] of Object.entries(js_listeners)) { - for (const listener of listener_vec) { - PLOT.removeEventListener(key, listener) - } - } - """) - # Outside of Pluto we simply re-use the body - ns = NormalScript(ps.body) - DualScript(ps, ns) -end) - -set!(_default_script_contents, "returned_element", DualScript(""; returned_element = "PLOT")) """ PlutoPlot(p::Plot; kwargs...) @@ -198,58 +72,4 @@ end function plot(args...;kwargs...) @nospecialize PlutoPlot(Plot(args...;kwargs...)) -end - -_pluto_default_iocontext() = try - Main.PlutoRunner.default_iocontext -catch - IOContext(devnull) -end - -# Create a ScriptContent using the IOContext from PlutoRunner (to avoid breaking core_publish_to_js) -_pluto_sc(h::HypertextLiteral.Result) = ScriptContent(h; iocontext = _pluto_default_iocontext()) - -# Load data from the PlutoPlot object to JavaScript -function _load_data_JS(pp::PlutoPlot; script_id = "pluto-plotly-div", ver = PLOTLY_VERSION[], kwargs...) - common = @htl(""" - - """) - ps = PlutoScript(@htl(""" - - """) |> _pluto_sc) - ns = NormalScript(ScriptContent(common)) - return DualScript(ps, ns; id = script_id) end \ No newline at end of file diff --git a/src/show.jl b/src/show.jl index 174f9fd..e8af7c3 100644 --- a/src/show.jl +++ b/src/show.jl @@ -1,21 +1,57 @@ -function _show(pp::PlutoPlot; kwargs...) - make_script([ - x isa Function ? x(pp; kwargs...) : x for x in pp.script_contents - ]) +function plot_script(pp::PlutoPlot; kwargs...) + gen = (x isa Function ? x(pp; kwargs...) : x for x in pp.script_contents) + reduce(vcat, gen) |> make_script end +function PlutoCombineHTL.print_html(io::IO, plt::PlutoPlot; full_html = false, pluto = is_inside_pluto(io), kwargs...) + script = plot_script(plt; pluto, kwargs...) + if full_html + println(io, "") + println(io, "") + println(io, "") + println(io, "") + end + print_html(io, script; pluto, kwargs...) + if full_html + println(io, "") + println(io, "") + end +end + +Base.show(io::IO, ::MIME"juliavscode/html", plt::PlutoPlot) = +print_html(io, plt; pluto = false, full_html = true) + function Base.show(io::IO, mime::MIME"text/html", plt::PlutoPlot) - show(io, mime, _show(plt; script_id = plotly_script_id(io))) + show(io, mime, plot_script(plt; script_id = plotly_script_id(io))) # show(io, mime, _show(plt)) end -function Base.show(io::IO, mime::MIME"text/javascript", plt::PlutoPlot) - if is_inside_pluto(io) - show(io, mime, published_to_js(_preprocess(plt))) - else - # We use HypertextLiteral to print the Dict out f _preprocess - HypertextLiteral.print_script(io, _preprocess(plt)) - end +function PlutoCombineHTL.print_javascript(io::IO, pts::PrintToScript{<:DisplayLocation, PlutoPlot}; pluto = is_inside_pluto(io)) + # Extract the PlutoPlot + pp = pts.el + _publish = x -> print_javascript(io, x; pluto) + # We publish the plot obj + print(io, " + // Publish the plot object to JS + let plot_obj = ") + _publish(_preprocess(pp)) + # We publish the listeners + ## Plotly + print(io, " + // Publish the plotly listeners + const plotly_listeners = ") + _publish(pp.plotly_listeners) + ## JS + print(io, " + // Publish the JS listeners + const js_listeners = ") + _publish(pp.js_listeners) + # Custom classes + print(io, " + // Deal with eventual custom classes + const custom_classlist = ") + _publish(pp.classList) + println(io) end #= Fix for Julia 1.10 From 54df14855cf200285ff8c5eb22949cd9b38215da Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Mon, 9 Oct 2023 15:15:21 +0200 Subject: [PATCH 3/3] working version --- notebooks/basic_tests.jl | 58 +++++++++++++++++--------------- notebooks/make_subplots_tests.jl | 24 ++++++++----- src/js_helpers.jl | 52 +++++++++++++++++++--------- src/main_struct.jl | 7 ++-- src/show.jl | 5 +-- 5 files changed, 89 insertions(+), 57 deletions(-) diff --git a/notebooks/basic_tests.jl b/notebooks/basic_tests.jl index e238cce..a417535 100644 --- a/notebooks/basic_tests.jl +++ b/notebooks/basic_tests.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.19.27 +# v0.19.29 #> custom_attrs = ["hide-enabled"] @@ -136,7 +136,7 @@ pp = Plot(scatter3d(x = rand(N), y = rand(N), z = rand(N), mode="markers"), Layo # ╔═╡ ccf62e33-8fcf-45d9-83ed-c7de80800b76 let p = PlutoPlot(pp) - add_plotly_listener!(p, "plotly_relayout", htl_js(""" + add_plotly_listener!(p, "plotly_relayout", """ (e) => { console.log(e) @@ -154,8 +154,7 @@ let console.log('plot_obj: ',plot_obj.layout.scene?.camera?.eye) } - """)) - PlutoPlotly._show(p) + """) end # ╔═╡ 1460ece1-7828-4e93-ac37-e979b874b492 @@ -228,40 +227,39 @@ md""" # ╔═╡ c3b1a198-ef19-4a54-9c32-d9ea32a63812 let p = PlutoPlot(Plot(rand(10), Layout(uirevision = 1))) - add_plotly_listener!(p, "plotly_relayout", htl_js(""" + add_plotly_listener!(p, "plotly_relayout", """ function(e) { console.log('listener 1') } - """)) - add_plotly_listener!(p, "plotly_relayout", htl_js(""" + """) + add_plotly_listener!(p, "plotly_relayout", """ function(e) { console.log('listener 2') } - """)) - @htl "$p" + """) end # ╔═╡ e9fc2030-c2f0-48e9-a807-424039e796b2 let p = PlutoPlot(Plot(rand(10), Layout(uirevision = 1))) - add_plotly_listener!(p, "plotly_relayout", htl_js(""" + add_plotly_listener!(p, "plotly_relayout", """ function(e) { console.log('listener 1') } - """)) - add_plotly_listener!(p, "plotly_relayout", htl_js(""" + """) + add_plotly_listener!(p, "plotly_relayout", """ function(e) { console.log('listener 2') } - """)) + """) p.plotly_listeners end @@ -277,13 +275,13 @@ lololol = 1 let lololol p = PlutoPlot(Plot(rand(10), Layout(uirevision = 1))) - add_js_listener!(p, "mousedown", htl_js(""" + add_js_listener!(p, "mousedown", """ function(e) { console.log('MOUSEDOWN!') } - """)) + """) end # ╔═╡ 6128ff76-3f1f-4144-bb3d-f44678210013 @@ -518,8 +516,8 @@ PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" [compat] Colors = "~0.12.10" -PlutoDevMacros = "~0.5.8" -PlutoExtras = "~0.7.9" +PlutoDevMacros = "0.6" +PlutoExtras = "0.7" PlutoUI = "~0.7.52" """ @@ -529,7 +527,7 @@ PLUTO_MANIFEST_TOML_CONTENTS = """ julia_version = "1.10.0-beta2" manifest_format = "2.0" -project_hash = "becb366eb1628f34a109423203d027d4a184385e" +project_hash = "6b1e8a02b00ef43bc0119f5085df2925b6e55084" [[deps.AbstractPlutoDingetjes]] deps = ["Pkg"] @@ -568,6 +566,12 @@ version = "1.0.5+1" deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +[[deps.DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.9.3" + [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" @@ -687,16 +691,16 @@ uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" version = "1.10.0" [[deps.PlutoDevMacros]] -deps = ["HypertextLiteral", "InteractiveUtils", "MacroTools", "Markdown", "Pkg", "Random", "TOML"] -git-tree-sha1 = "6ce1d9f7c078b493812161349c48735dee275466" +deps = ["AbstractPlutoDingetjes", "DocStringExtensions", "HypertextLiteral", "InteractiveUtils", "MacroTools", "Markdown", "Pkg", "Random", "TOML"] +git-tree-sha1 = "06fa4aa7a8f2239eec99cf54eeddd34f3d4359be" uuid = "a0499f29-c39b-4c5c-807c-88074221b949" -version = "0.5.8" +version = "0.6.0" [[deps.PlutoExtras]] -deps = ["AbstractPlutoDingetjes", "HypertextLiteral", "InteractiveUtils", "Markdown", "PlutoDevMacros", "PlutoUI", "REPL", "Reexport"] -git-tree-sha1 = "aad38509250eaa0840d2aadd73ef23c2a89bdb4a" +deps = ["AbstractPlutoDingetjes", "HypertextLiteral", "InteractiveUtils", "Markdown", "PlutoDevMacros", "PlutoUI", "REPL"] +git-tree-sha1 = "382b530c2ebe31f4a44cb055642bbd71197fbd20" uuid = "ed5d0301-4775-4676-b788-cf71e66ff8ed" -version = "0.7.9" +version = "0.7.11" [[deps.PlutoUI]] deps = ["AbstractPlutoDingetjes", "Base64", "ColorTypes", "Dates", "FixedPointNumbers", "Hyperscript", "HypertextLiteral", "IOCapture", "InteractiveUtils", "JSON", "Logging", "MIMEs", "Markdown", "Random", "Reexport", "URIs", "UUIDs"] @@ -712,9 +716,9 @@ version = "1.2.0" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "7eb1686b4f04b82f96ed7a4ea5890a4f0c7a09f1" +git-tree-sha1 = "00805cd429dcb4870060ff49ef443486c262e38e" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.4.0" +version = "1.4.1" [[deps.Printf]] deps = ["Unicode"] @@ -833,7 +837,7 @@ version = "17.4.0+2" # ╠═de0cb780-ff4e-4236-89c4-4c3163337cfc # ╠═dd23fe10-a8d5-461a-85a8-e03468cdcd97 # ╠═ccf62e33-8fcf-45d9-83ed-c7de80800b76 -# ╠═1460ece1-7828-4e93-ac37-e979b874b492 +# ╟─1460ece1-7828-4e93-ac37-e979b874b492 # ╠═18c80ea2-0df4-40ea-bd87-f8fee463161e # ╠═ce29fa1f-0c52-4d38-acbd-0a96cb3b9ce6 # ╟─c3e29c94-941d-4a52-a358-c4ffbfc8cab8 diff --git a/notebooks/make_subplots_tests.jl b/notebooks/make_subplots_tests.jl index d71707f..90157c4 100644 --- a/notebooks/make_subplots_tests.jl +++ b/notebooks/make_subplots_tests.jl @@ -170,8 +170,8 @@ PlutoExtras = "ed5d0301-4775-4676-b788-cf71e66ff8ed" PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" [compat] -PlutoDevMacros = "~0.5.8" -PlutoExtras = "~0.7.10" +PlutoDevMacros = "~0.6.0" +PlutoExtras = "~0.7.11" PlutoUI = "~0.7.52" """ @@ -181,7 +181,7 @@ PLUTO_MANIFEST_TOML_CONTENTS = """ julia_version = "1.10.0-beta2" manifest_format = "2.0" -project_hash = "f50c3104111b5d594359e3dd1628a076c5fe269d" +project_hash = "46f2e02ace855d54b6bc9494b62148b187ae7606" [[deps.AbstractPlutoDingetjes]] deps = ["Pkg"] @@ -214,6 +214,12 @@ version = "1.0.5+1" deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +[[deps.DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.9.3" + [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" @@ -333,16 +339,16 @@ uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" version = "1.10.0" [[deps.PlutoDevMacros]] -deps = ["HypertextLiteral", "InteractiveUtils", "MacroTools", "Markdown", "Pkg", "Random", "TOML"] -git-tree-sha1 = "6ce1d9f7c078b493812161349c48735dee275466" +deps = ["AbstractPlutoDingetjes", "DocStringExtensions", "HypertextLiteral", "InteractiveUtils", "MacroTools", "Markdown", "Pkg", "Random", "TOML"] +git-tree-sha1 = "06fa4aa7a8f2239eec99cf54eeddd34f3d4359be" uuid = "a0499f29-c39b-4c5c-807c-88074221b949" -version = "0.5.8" +version = "0.6.0" [[deps.PlutoExtras]] -deps = ["AbstractPlutoDingetjes", "HypertextLiteral", "InteractiveUtils", "Markdown", "PlutoDevMacros", "PlutoUI", "REPL", "Reexport"] -git-tree-sha1 = "beedecb30d8ed0874773d5641f5ce5ee2bfeeded" +deps = ["AbstractPlutoDingetjes", "HypertextLiteral", "InteractiveUtils", "Markdown", "PlutoDevMacros", "PlutoUI", "REPL"] +git-tree-sha1 = "382b530c2ebe31f4a44cb055642bbd71197fbd20" uuid = "ed5d0301-4775-4676-b788-cf71e66ff8ed" -version = "0.7.10" +version = "0.7.11" [[deps.PlutoUI]] deps = ["AbstractPlutoDingetjes", "Base64", "ColorTypes", "Dates", "FixedPointNumbers", "Hyperscript", "HypertextLiteral", "IOCapture", "InteractiveUtils", "JSON", "Logging", "MIMEs", "Markdown", "Random", "Reexport", "URIs", "UUIDs"] diff --git a/src/js_helpers.jl b/src/js_helpers.jl index 1e4c32d..5b6af1a 100644 --- a/src/js_helpers.jl +++ b/src/js_helpers.jl @@ -24,13 +24,14 @@ console.log(PLOT) // logs the plot div inside the developer console when pressin \"\"\" ``` """ -function add_js_listener!(p::PlutoPlot, event_name::String, listener::String) +function add_js_listener!(p::PlutoPlot, event_name::String, listener::JS) ldict = p.js_listeners - listeners_array = get!(ldict, event_name, String[]) + listeners_array = get!(ldict, event_name, JS[]) push!(listeners_array, listener) return p end -add_js_listener!(p::PlutoPlot, event_name, listener::String) = add_js_listener!(p, event_name, htl_js(listener)) +add_js_listener!(p::PlutoPlot, event_name, listener::String) = +add_js_listener!(p, event_name, JS(listener)) ## add class ## """ @@ -68,34 +69,52 @@ end ## Push Script ## """ - push_script!(p::PlutoPlot, items...) -Add script contents contained in collection `items` at the end of the plot show method script. -The `item` must either be a collection of `String` or `HypertextLiteral.JavaScript` elements + push_script!(p::PlutoPlot, item) + push_script!(p::PlutoPlot, name::String, item) +Add `item` to the end of the `script_contents` field of the `PlutoPlot` object +`p`. The function accepts a custom name to identify this script as the +`script_contents` is a Dictionary with String Indices. + +If the `name` is not provided, a name of the form `custom_script_N` will be +generated automatically. """ -function push_script!(p::PlutoPlot, items::Vararg{Union{Script, ScriptContent, Function},N}) where N +function push_script!(p::PlutoPlot, name::String, item) @nospecialize - push!(p.script_contents.vec, items...) + insert!(p.script_contents, name, PrintToScript(item)) return p end +function push_script!(p::PlutoPlot, item) + sc = p.script_contents + N = count(startswith("custom_script_"), sc.indices) + 1 + name = "custom_script_$N" + push_script!(p, name, item) +end + ## plotly listener ## """ - add_plotly_listener!(p::PlutoPlot, event_name::String, listener::HypertextLiteral.JavaScript) add_plotly_listener!(p::PlutoPlot, event_name::String, listener::String) -Add a custom *javascript* `listener` (to be provided as `String` or directly as `HypertextLiteral.JavaScript`) to the `PlutoPlot` object `p`, and associated to the [plotly event](https://plotly.com/javascript/plotlyjs-events/) specified by `event_name`. +Add a custom *javascript* `listener` (to be provided as `String` or directly as +`HypertextLiteral.JavaScript`) to the `PlutoPlot` object `p`, and associated to +the [plotly event](https://plotly.com/javascript/plotlyjs-events/) specified by +`event_name`. -The listeners are added to the HTML plot div after rendering. The div where the plot is inserted can be accessed using the variable named `PLOT` inside the listener code. +The listeners are added to the HTML plot div after rendering. The div where the +plot is inserted can be accessed using the variable named `PLOT` inside the +listener code. # Differences with `add_js_listener!` -This function adds a listener using the plotly internal events via the `on` function. These events differ from the standard javascript ones and provide data specific to the plot. +This function adds a listener using the plotly internal events via the `on` +function. These events differ from the standard javascript ones and provide data +specific to the plot. See also: [`add_js_listener!`](@ref), [`htl_js`](@ref) # Examples: ```julia p = PlutoPlot(Plot(rand(10), Layout(uirevision = 1))) -add_plotly_listener!(p, "plotly_relayout", htl_js(\"\"\" +add_plotly_listener!(p, "plotly_relayout", \"\"\" function(e) { console.log(PLOT) // logs the plot div inside the developer console @@ -104,10 +123,11 @@ console.log(PLOT) // logs the plot div inside the developer console \"\"\" ``` """ -function add_plotly_listener!(p::PlutoPlot, event_name::String, listener::String) +function add_plotly_listener!(p::PlutoPlot, event_name::String, listener::JS) ldict = p.plotly_listeners - listeners_array = get!(ldict, event_name, String[]) + listeners_array = get!(ldict, event_name, JS[]) push!(listeners_array, listener) return p end -add_plotly_listener!(p::PlutoPlot, event_name, listener::String) = add_plotly_listener!(p, event_name, htl_js(listener)) \ No newline at end of file +add_plotly_listener!(p::PlutoPlot, event_name::String, listener::String) = +add_plotly_listener!(p, event_name, JS(listener)) \ No newline at end of file diff --git a/src/main_struct.jl b/src/main_struct.jl index 62ff0dd..3837fa0 100644 --- a/src/main_struct.jl +++ b/src/main_struct.jl @@ -1,6 +1,7 @@ const PLOTLY_VERSION = Ref("2.26.2") -const SCType = Dictionary{String, Union{Function, Script}} +const SCType = Dictionary{String, Union{Function, Script, PrintToScript}} +const JS = HypertextLiteral.JavaScript # This will contain the default contents of the script generating the plot const _default_script_contents = SCType() @@ -53,8 +54,8 @@ See also: [`ScriptContents`](@ref), [`add_js_listener!`](@ref), [`add_plotly_lis """ Base.@kwdef struct PlutoPlot Plot::PlotlyBase.Plot - plotly_listeners::Dict{String, Vector{String}} = Dict{String, Vector{String}}() - js_listeners::Dict{String, Vector{String}} = Dict{String, Vector{String}}() + plotly_listeners::Dict{String, Vector{JS}} = Dict{String, Vector{JS}}() + js_listeners::Dict{String, Vector{JS}} = Dict{String, Vector{JS}}() classList::Vector{String} = String[] script_contents::SCType = deepcopy(_default_script_contents) end diff --git a/src/show.jl b/src/show.jl index e8af7c3..3d030c4 100644 --- a/src/show.jl +++ b/src/show.jl @@ -30,6 +30,7 @@ function PlutoCombineHTL.print_javascript(io::IO, pts::PrintToScript{<:DisplayLo # Extract the PlutoPlot pp = pts.el _publish = x -> print_javascript(io, x; pluto) + _publish_listeners = x -> HypertextLiteral.print_script(io, x) # We publish the plot obj print(io, " // Publish the plot object to JS @@ -40,12 +41,12 @@ function PlutoCombineHTL.print_javascript(io::IO, pts::PrintToScript{<:DisplayLo print(io, " // Publish the plotly listeners const plotly_listeners = ") - _publish(pp.plotly_listeners) + _publish_listeners(pp.plotly_listeners) ## JS print(io, " // Publish the JS listeners const js_listeners = ") - _publish(pp.js_listeners) + _publish_listeners(pp.js_listeners) # Custom classes print(io, " // Deal with eventual custom classes