Pluto plots with PlotlyBase/PlotlyJS with working static html export

Hi everyone,

I recently wanted to use PlotlyJS directly in Pluto and still be able to view the plots in the static html export.
I just managed to somehow do that and wanted to post the procedure here to get feedback and eventually have a reference on discourse if anybody wants to achieve the same.

My need for PlotlyJS functionality in Pluto was due to the specific plot type I wanted to produce (Suburst Charts from the plotlyjs main library).
This trace, in fact, doesn’t seem to be exposed by the plotly/plotlyjs backend in Plots.jl and after asking on slack, I was told it would not be possible to plot this specific unsupported trace with Plots.jl without rewriting some code of the package itself.

While the use of WebIO is not yet officially supported, I managed to have a working PlotlyJS plot in pluto using the branch from this pull request.

All was working except that plots would not display on a static exported html file, which is normal as @fonsp pointed out in my comment in the pull request.

Trying to find a way to also make the static export work, I found a similar problem referenced in this issue.
I then tried adapting the proposed solution from @akdor1154 to have PlotlyBase plots show directly in the output of a pluto cell.

This is the piece of code that for me makes the Plot object correctly appear in the pluto cell both in the live notebook and in the static export.

using PlotlyBase, HypertextLiteral

# Custom abstract-trace array to string function
TraceData(data::Array{<:AbstractTrace}) = join(["[",join(string.(data),","),"]"])

function Base.show(io::IO, mimetype::MIME"text/html", p::PlotlyBase.Plot)
	show(io,mimetype,@htl("""
	<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
	<div style="height: auto">
		<script>
		let parentnode = currentScript.parentElement

	  Plotly.plot(parentnode,$(HypertextLiteral.JavaScript(TraceData(p.data))),$(HypertextLiteral.JavaScript(string(p.layout))),{responsive: true});
		</script>
	</div>
"""))
end

I suppose this breaks any possible interaction between julia and the plot which is coming from PlotlyJS, but I didn’t need that specifically.
Moreover, this also leaves PlotlyBase as the only dependency for plotting (and no requirement for the WebIO supporting branch of Pluto).

As I basically patched this solution without much knowledge about Javascript/HTML, I wanted to know whether there is something inherently wrong in adding the code above to a pluto cell in order to achieve what I wanted.
A potential issue I see is that I reference script containing the minfied plotlyjs code for each cell that contains a plot, but I have no idea how I could achieve the same functionality without putting the script inside the show function.

Here is also the source of the example .jl notebook for testing

Test notebook code
### A Pluto.jl notebook ###
# v0.14.5

using Markdown
using InteractiveUtils

# ╔═╡ a08c16a0-bc90-11eb-0274-e91d841ba34e
begin
	import Pkg
	Pkg.activate(".")
end

# ╔═╡ fa09788b-d9a6-496c-a126-4a1639211aaa
begin
	using PlotlyBase
	using HypertextLiteral
end

# ╔═╡ 923df2dc-3c00-493e-aac9-fbf03bd58fe7
begin
	# Custom abstract-trace array to string function
	TraceData(data::Array{<:AbstractTrace}) = join(["[",join(string.(data),","),"]"])
	
	function Base.show(io::IO, mimetype::MIME"text/html", p::PlotlyBase.Plot)
		show(io,mimetype,@htl("""
		<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
		<div style="height: auto">
			<script>
			let parentnode = currentScript.parentElement

		Plotly.plot(parentnode,$(HypertextLiteral.JavaScript(TraceData(p.data))),$(HypertextLiteral.JavaScript(string(p.layout))),{responsive: true});
			</script>
		</div>
	"""))
	end
end

# ╔═╡ 35c4a24e-c7e9-4b37-bc95-defb25f83f87
p = Plot(GenericTrace("sunburst",Dict(
  :type => "sunburst",
  :labels => ["Eve", "Cain", "Seth", "Enos", "Noam", "Abel", "Awan", "Enoch", "Azura"],
  :parents => ["", "Eve", "Eve", "Seth", "Seth", "Eve", "Eve", "Awan", "Eve" ],
  :values =>  [50, 14, 12, 10, 2, 6, 6, 4, 0],
  :leaf => Dict(:opacity => 0.4),
  :marker => Dict(:line => Dict(:width => 1)),
  :branchvalues => "total",
)))

# ╔═╡ Cell order:
# ╠═a08c16a0-bc90-11eb-0274-e91d841ba34e
# ╠═fa09788b-d9a6-496c-a126-4a1639211aaa
# ╠═923df2dc-3c00-493e-aac9-fbf03bd58fe7
# ╠═35c4a24e-c7e9-4b37-bc95-defb25f83f87

Edit: Added the TraceData function to correctly deal with multiple traces in a plot

6 Likes

@disberd This is a remarkable small piece of code. You have nailed it: PlotlyJS working without any problem at all inside Pluto notebooks. Moreover, we can have the plot displayed within Pluto’s window, or we can export it as an HTML file.

I tested your example and I just added some new cells where I experimented with different types of plots (2D, 3D, etc.). They all worked in an immaculate way. We can save the plots in html, png, pdf, svg formats. The only thing that I had to change is the name of the plot function: in VSCode we plot with plot(), in your approach we have to start it with a capital letter Plot().

I tried this piece of code (typical PlotlyJS syntax):

begin
	n = 200
	r = LinRange(1e-4,10,n)
	k = LinRange(0,2π,n)
	g = r * cos.(k)'
	f = r * sin.(k)'
	z = (g.^2 .* f.^2) ./ (g.^4 + f.^4)
	#p5 = Plot(surface(x=g,y=f,z=z),Layout(width=700,height=500))
	p5 = Plot(surface(x=g,y=f,z=z))
end

and it took just 2.5ms to get the plot below (while it takes 4s in Jupyter!!!). In case someone has doubts, here it is:

Congrats. Wonderful job.

3 Likes

I am glad I was able to help someone else with thi! :slight_smile:

This is simply due to the fact that the plot() explosed by PlotlyJS is a convenience method to create a SyncPlot to get the WebIO interactivity and attaching the standard Plot object from PlotlyBase to it.
In the approach I put above we are just using the Plot object directly as we are skipping the WebIO integration.
If you wish to also keep the plot() synthax you could simply generate your own method that calls Plot() underneath, but I preferred to keep them separate to avoid potential clash with other plotting packages.

1 Like

Ho no, I’m pretty happy using a capital letter in the plot function. I just mentioned it because some people may get stuck here. Great job.

Cool! I was doing similar experiments for the plotly backend of Plots.jl. You can use 💁 API to make objects available inside JS by fonsp · Pull Request #1124 · fonsp/Pluto.jl · GitHub to make it significantly faster:

I lost track of all the different Plotly+Julia combinations, but I thought I would quickly share this in case it is helpful. I plan to revisit plotly+pluto at some point.

3 Likes

Also note that the 2.5 ms runtime is an underestimation, it only measures the time to run the cell’s julia code. This does not include:

  • time to render the result object, i.e. call Base.show(io, best_mime, cell_result)
  • time to transfer data to the browser
  • time for the browser (and plotly) to render the result

The best way to test performance is to use a slider or PlutoUI.Clock to continually trigger the plot to refresh, and use your eyes to guess the frames per second that you are getting. Use bigger datasets to make it easier to see.

@fonsp , yes, I know that. Anyway, I can assure you that Pluto is much faster than Jupyter. I have tried both in real time, changing the number of grid points by a significant amount, and in Pluto, the new plot takes less than1s to pop up, while it takes around 5.8s to appear in Jupyter.

I used a slider in Pluto. I still have to see how the PlutoUI.Clock works, cause I could not find it in the docs, but the mistake may be mine.

So I had some time to did some further testing based on the input from @fonsp and I have a few updates.

The first one is that indeed there is a speedup when using the code example from fonsp but I suspect it mostly comes from the use of Plotly.react when the div already exists rathern than Plotly.newPlot.
The use of MsgPack as opposed to JSON does bring some potential minor benefit from the visual tests with the PlutoUI.Clock but they don’t seem significant using the example surface plot referenced by @VivMendes.

Another update comes from the fact that PlotlyBase already exports a json() function that takes a Plot variable and directly creates the relevant JSON string to pass to the function, being more precise than my mockup TraceData function above and also including the layout characteristics.

Given the above considerations, my suggested function for showing PlotlyBase plots is the following

function Base.show(io::IO, mimetype::MIME"text/html", p::PlotlyBase.Plot)
	show(io,mimetype,@htl("""
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
	<div style="height: auto">
		<script id=plotly-show>
			const PLOT = this ?? document.createElement("div");
			(this == null ? Plotly.newPlot : Plotly.react)(PLOT,$(HypertextLiteral.JavaScript(json(p))));
			return PLOT
		</script>
	</div>
"""))
end

I also uploaded the example notebook used for the tests here:

Hi @disberd. I have experimented with your new function. It looks a little bit faster but there is a point that I think deserves some consideration. In your first approach, if we changed the width of Pluto’s window (for me the default size is impracticable for teaching), the plots’ width follows the change we introduce into the notebook. For example, if I choose

html"""<style>
main {
    max-width: 900px;
    align-self: flex-start;
    margin-left: 200px;
}
"""

the plots will adjust to the new size of the window, which is good indeed. In your new approach, apparently, this functionality is not available: plots keep the default size. I don’t know if I am doing something wrong, or if this is just a point that was not considered in the move from the first to the second version. I think tables adjust to the new width, but plots do not. Both followed the change in width window in your first version.

Indeed you are right.
I completely disregarded the responsivity of the plot that was “hard-coded” in my original example (it was the {responsive: true} as the last argument of the newPlot function.

The issue I am facing now is that I am not able to get responsivity to the div width in my plots anymore, not even with the original code…
So I have to understand what problem is causing this at the moment…

Edit:
I realized what was the issue. I was trying to trigger a resizing just by changing the value inside the cell:

html"""<style>
main {
    max-width: 900px;
    align-self: flex-start;
    margin-left: 200px;
}
"""

The plot resize from plotly from my understanding instead only triggers when you resize the actual window of the browser.
Using this modified code below:

function Base.show(io::IO, mimetype::MIME"text/html", p::PlotlyBase.Plot)
	show(io,mimetype,@htl("""
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
	<div style="height: auto">
		<script id=plotly-show>
			const PLOT = this ?? document.createElement("div"); 
			(this == null ? Plotly.newPlot : Plotly.react)(PLOT,$(HypertextLiteral.JavaScript(json(p.data))),$(HypertextLiteral.JavaScript(json(p.layout))),{responsive: true});
			return PLOT
		</script>
	</div>
"""))
end

I was able to get plot resizing when changing the size of the window.
There is still some investigation on how to avoid hard-coding responsivity and accepting other plotly config options.
According to this issue:

It was a design choice by Slygon to move the “config” part of the plot to PlotlyJS rather than plotlybase.

This makes it a bit more difficult to handle it nicely but we defeinitely can do better.

Hi. Your first version works very well. It is always responsive to the change of Pluto’s window width and the plots come out quite fast. At least 6x faster than Jupyter, which is quite something if we talk about heavyweight plotting. I tested also your third version, and in my case, it still forces the plots to go back to the default window size whenever I resize.

I made a PR to Plots.jl to use the leaner HTML show method: embeddable html show for performance in Pluto by fonsp · Pull Request #3559 · JuliaPlots/Plots.jl · GitHub

2 Likes