Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: display interactive figures given by Plots.jl with PlotlyJS backend just like fdplotly #890

Closed
ChasingZenith opened this issue Sep 30, 2021 · 5 comments · Fixed by #928

Comments

@ChasingZenith
Copy link
Contributor

When PlotlyJS.jl is used, a beautiful interactive plot can be display with the helper function fdplotly.
But I couldn't find a build-in method to show interactive Plots.jl figures with plotlyjs() backend just like when PlotlyJS.jl is used directly. (Don't know whether that is the case.)

So I have implemented a helper function myself. It works but I think it is not so good that several problem has to be solved.

function fdplotsplotlyjs(p; id="fdp"*Random.randstring('a':'z', 3),
  style="width:600px;height:350px")::Nothing
  print("""
    ~~~
    <div id="$id" style="$style"></div>
    <script>
      var fig = """)
  show(stdout, MIME("application/vnd.plotly.v1+json"), p);
  println("""; CONTAINER = document.getElementById('$id');
      Plotly.newPlot(CONTAINER, fig.data, fig.layout)
    </script>
    ~~~
    """) # a ";" is used to end var fig = ...;
end

It can be used like this in Franklin.jl

 ```julia:ex
using Plots
plotlyjs()
fdplotsplotlyjs(plot([1, 2])
```
\textoutput{ex}

Several Problem should be solved:

  1. I think there should be a better way to transfer the plot to a JSON format but don't know how. The function json in Plots.jl directly save it to a file which I think is unnecessary.
  2. I think it should be better to give a type declaration on p. It should be something like p::Plots.Plot{Plots.PlotlyJSBackend}. But when this function is placed in src/manager/extra.jl, it can't find Plots.
  3. Give the function a better name.
@ChasingZenith
Copy link
Contributor Author

I come up another implementation idea that maybe a \fig{} can be used to plot the plotlyjs json file that is saved by json in Plots.jl. So we can use something like:

```julia:pyplot1
using Plots
plotlyjs()

 p= plot(sinc)
savefig(joinpath(@OUTPUT, "sinc.json")) # hide
```
\fig{sinc}

Not test yet. Just an idea.

@tlienart
Copy link
Owner

I'm glad you're experimenting with this and what you're doing sounds pretty useful! as you noticed though typing shouldn't be in the package because I don't want Plots to become a dependency (it can be a dependency in the tests of course but not of the main package)

  1. saving the json to file (e.g. to _assets/xxx) is probably not such a bad idea; you could have the function check whether that json is available and if so not do anything
  2. see comment above
  3. hahaha yes :-) maybe you could just call it interactive_plot and wrap over fdplotly with a keyword selecting the appropriate path

I don't have much time right now to check this in more details but will do so next week, just sharing some early notes that you might find useful

@rikhuijzer
Copy link
Contributor

as you noticed though typing shouldn't be in the package because I don't want Plots to become a dependency

Maybe, this could be solved via Requires.jl or a FranklinPlottingInterface.jl (with Requires.jl and some Makie/Plots etc logic). I agree that the former is not ideal. Especially because it is difficult to add compat entries for packages added via Requires.jl.

@ChasingZenith
Copy link
Contributor Author

ChasingZenith commented Oct 15, 2021

After doing some experiment, I found several things.

Let me first post the the experimental solution now:

I choose to first save the Plots file in json file and then plot it with a helper function written in Javascript.

In _layout/head.html add a JS helper function

  {{if hasplotly}} <script src="/libs/plotly/plotly.min.js"></script>
  <script>
    const PlotlyJS_json = async (id, url) => {
      fetch(url)
        .then(response => response.json())
        .then(fig => {
          CONTAINER = document.getElementById(id);
          Plotly.newPlot(CONTAINER, fig.data, fig.layout)
        });

      // Here is another way to implement the same feature using async and await
      // response = await fetch(url); // get file
      // fig = await response.json(); // convert it to json
      // CONTAINER = document.getElementById(id);
      // Plotly.newPlot(CONTAINER, fig.data, fig.layout, fig.config);
    };
  </script>
    {{end}}

Another Julia helper function interactive_plot_json which will insert the previous JS function in proper place in the html

import Random
"""
Display a Plots plot `p` created  by the PlotlyJS backend
"" is the default size of Plots

import  Plots
plotlyjs(); # use PlotlyJS as the backend of Plots
p = Plots.plot(sinc)
Plots.savefig(p, joinpath(@OUTPUT, "sinc.json"))
# json_path = ...
interactive_plot_json(json_path)
"""
function interactive_plot_json(src ; id="fdp"*Random.randstring('a':'z', 3),
	 			  style="")::Nothing
    io = IOBuffer()
    println(io, """~~~""")

    print(io, """
		<div id="$id" style="$style"></div>
		<script>
    "use strict"
    PlotlyJS_json('$id', '$src');
		</script>
		""")
    println(io, """~~~""")
    println(String(take!(io)))
    return nothing
end
```julia:intplot
using Plots
plotlyjs(); # use PlotlyJS as the backend of Plots

# need a way to find where the picture is
interactive_plot_json("/assets/blog/2021/09/07_PlotlyJS/code/output/pp.json";style="") 
```
\textoutput{intplot}

Note: this implementation is definitely not the best way. I think a better way is to integrate it in to \fig{ } command.

I also find that fdplotly may have some performance problems

Since fdplotly write the json string directly into the html file, the html file may become very large. It may grow from 20kb or so to 9MB which may seriously effect the loading time of the webpage, especially when many complicated plot were inserted.

I recommend that we should change the implementation of fdplotly and let all plots be loaded asynchronously.

I use fetch in my implementation to make loading the plot asynchronous. (I am just starting with Javascript, please tell me whether I am doing it right or not. Should I use defer or async tag in the script tag.)

@aburousan
Copy link

Hi... I have a question, I tried this and when I was running it locally the interactive plot is showing but once I deploy it to gitpages, the interactive plot is not showing. Can someone help?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants