jupyter icon indicating copy to clipboard operation
jupyter copied to clipboard

recommendation for using plotly?

Open jkitchin opened this issue 3 years ago • 19 comments

A src block like

import plotly.graph_objects as go

f = go.Figure(go.Scatter(x=[1, 2, 3], y=[5, 8, 9]))
f.show()

generates a lot of html. For me that causes a few issues, usually emacs chokes trying to fontify it, and it isn't helpful to see it, I would rather see the rendered html!

Any recommendation for handling this? I could see dumping it to a file and opening it in a browser. That isn't quite right here, it looks like the html is not complete enough to be rendered, but in general what do you do when your code outputs a lot of html code?

jkitchin avatar May 30 '21 19:05 jkitchin

Would widgets be a solution?

https://github.com/nnicandro/emacs-jupyter#building-the-widget-support-experimental

https://plotly.com/python/figurewidget/

dangom avatar Jun 08 '21 19:06 dangom

I was able to make the widgets extension, and when I ran some

import plotly.graph_objects as go

f = go.FigureWidget()
f.add_scatter(y=[2, 1, 4, 3]);
f

a browser did pop up, but it was blank. The html code looks empty too:

!DOCTYPE html>
<html>
<head>
<title>Jupyter Client</title>
<script type="application/javascript" src="/jupyter"></script>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"></script>
<style type="text/css">
  * {
      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  }
</style>
<script type="application/javascript">
  var kernel;
  document.addEventListener("DOMContentLoaded", function(event) {
      // TODO: May not be available everywhere
      var p = new URLSearchParams(window.location.search);
      var kernel = new EmacsJupyter({username: p.get('username'),
                                     clientId: p.get('clientId')},
                                    p.get('port'));
      var commManager = new CommManager(kernel);
      var widgetManager = new WidgetManager(kernel, document.getElementById("widget"));
      commManager.register_target(widgetManager.comm_target_name, function(comm, msg) {
          widgetManager.handle_comm_open(comm, msg);
      });
      kernel.widgetManager = widgetManager;
      kernel.commManager = commManager;
      window.kernel = kernel;
  });
</script>
</head>
<body>
</body>
</html>

Maybe there is some thing I am missing?

my current solution is to patch fig.show so it saves html and returns an Image and FileLink so I can see what the figure is, and click a link if I want to interact with it.

jkitchin avatar Jun 08 '21 23:06 jkitchin

Yes, this is the widget.html. The widgetManager where the widget is supposed to show up is placed under document.getElementById("widget")), which is a div created by index.js. I don't really know much JS, but looking a bit at the jupyter-widget-client elisp code, it seems that it is the one responsible for serving the <script type="application/javascript" src="/jupyter"></script> scripts, under which index.js is to be found. So maybe something is off with resolving that path.

I just tried and I no longer seem to be able to use widgets either. I remember trying them out back when the project was first announced in 2019 and it worked back then - that was under Emacs 26.3 and who knows what versions of node modules and browsers.

dangom avatar Jun 09 '21 00:06 dangom

my current solution is to patch fig.show so it saves html and returns an Image and FileLink so I can see what the figure is, and click a link if I want to interact with it.

@jkitchin thanks for mentioning this workaround, could you maybe share this solution if you still use it or have it lying around?

fleimgruber avatar Oct 14 '21 14:10 fleimgruber

I think it looks like this:


import os

from hashlib import md5

from IPython.display import Image, FileLink

import plotly.graph_objects as go
import plotly.io as pio


def myshow(self, *args, **kwargs):
    html = pio.to_html(self)
    mhash = md5(html.encode('utf-8')).hexdigest()
    if not os.path.isdir('.ob-jupyter'):
        os.mkdir('.ob-jupyter')
    fhtml = os.path.join('.ob-jupyter', mhash + '.html')

    with open(fhtml, 'w') as f:
        f.write(html)

    display(FileLink(fhtml, result_html_suffix=''))
    return Image(pio.to_image(self, 'png'))


go.Figure.show = myshow

jkitchin avatar Oct 14 '21 20:10 jkitchin

Thanks! I changed the last return line to return Image(pio.to_image(self, 'png', engine="kaleido")) to not use the deprecated orca engine. Now I get this as a result

#+RESULTS:
:RESULTS:
: c:\Users\LeimgruberF\dev\py\.ob-jupyter\06a4bacef7f9bccd239548566e03e3eb.html
: <IPython.core.display.Image object>
:END:

The file link is not clickable, but it references the correct HTML and I can interact with it after manually copying the path and opening it in a browser. Maybe the plotly API changed in the meantime? Or do I need to have different src block settings regarding the block result type?

fleimgruber avatar Oct 15 '21 09:10 fleimgruber

With the code I posted, here is what I get.

image

The headers are set as (setq org-babel-default-header-args:jupyter-python '((:results . "value") (:session . "jupyter") (:kernel . "python3") (:pandoc . "t") (:exports . "both") (:cache . "no") (:noweb . "no") (:hlines . "no") (:tangle . "no") (:eval . "never-export")))

With the new engine='kaleido' it also seems work.

jkitchin avatar Oct 15 '21 12:10 jkitchin

Thanks for the example! I am getting results below with

org-babel-default-header-args:jupyter-python’s value is
((:async . "yes")
 (:session . "py")
 (:kernel . "lg"))

image

fleimgruber avatar Oct 15 '21 14:10 fleimgruber

maybe the difference is I have pandoc set to t. Did you try with the same settings I listed above?

jkitchin avatar Oct 17 '21 20:10 jkitchin

Thanks for the shove, with :pandoc t it works!

fleimgruber avatar Oct 18 '21 19:10 fleimgruber

A less hacky way to do it might be the following. Set

pio.renderers.default = "png"

Works in the interactive REPL at least.

I've been using the following code for displaying HTML figures in interactive notebooks, based on @jkitchin 's code above

import hashlib
import os

import plotly.io as pio


class EmacsRenderer(pio.base_renderers.ColabRenderer):
    save_dir = "ob-jupyter"
    base_url = f"http://localhost:8888/files"

    def to_mimebundle(self, fig_dict):
        html = super().to_mimebundle(fig_dict)["text/html"]

        mhash = hashlib.md5(html.encode("utf-8")).hexdigest()
        if not os.path.isdir(self.save_dir):
            os.mkdir(self.save_dir)
        fhtml = os.path.join(self.save_dir, mhash + ".html")
        with open(fhtml, "w") as f:
            f.write(html)

        return {"text/html": f'<a href="{self.base_url}/{fhtml}">Click to open {fhtml}</a>'}


pio.renderers["emacs"] = EmacsRenderer()

pio.renderers.default = "emacs"

Would this maybe be worth a pull request?

rhaps0dy avatar Mar 20 '23 23:03 rhaps0dy

do you have some kind of server running on port 8888?

This works better for me, it just opens a local file in the browser.

return {"text/html": f"<a href=\"{fhtml}\" target='_blank'>Click to open {fhtml}</a>"}

jkitchin avatar Mar 21 '23 17:03 jkitchin

Yes, the jupyter notebook is a server running on 8888, in a remote machine. I did open it by first calling on the command line jupyter nbclassic and then connecting using function jupyter-run-server-repl.

I agree if you don’t explicitly run the server it’s better to link to the path!

Adrià

On Mar 21, 2023, at 10:53, John Kitchin @.***> wrote:

do you have some kind of server running on port 8888?

This works better for me, it just opens a local file in the browser.

return {"text/html": f"<a href="{fhtml}" target='_blank'>Click to open {fhtml}"} — Reply to this email directly, view it on GitHub https://github.com/nnicandro/emacs-jupyter/issues/333#issuecomment-1478345625, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABFTF4VNFZVVFMGQKGL4W7LW5HTKFANCNFSM45ZUJ4XQ. You are receiving this because you commented.

rhaps0dy avatar Mar 21 '23 17:03 rhaps0dy

maybe can be configured in the python code somehow. it is pretty non-obvious to me, as a long time jupyter user!

jkitchin avatar Mar 21 '23 17:03 jkitchin

@rhaps0dy After connecting via jupyter-run-server-repl how to tell jupyter org client to use that connection? If I execute an org source block then a new kernel is started in a new *jupyter-repl...* buffer.

fleimgruber avatar Jun 11 '23 12:06 fleimgruber

See the README of emacs-jupyter for connecting to an existing kernel. You should start your kernel with jupyter kernel, note down the path of the connection .json file, and configure your source block as follows:

#+begin_src jupyter-python :session ~/Library/Jupyter/runtime/kernel-70ab2982-89ba-4a91-8a3d-1300a850b0df.json
import sys
print(sys.executable)
#+end_src

rhaps0dy avatar Jun 11 '23 21:06 rhaps0dy

Thanks, I was hoping to avoid configuring the explicit path of connection .json every time and for each source block in question and hoped for a more interactive way, e.g. jupyter-org noticing the new session after jupyter-run-server-repl and re-using that session from then on.

fleimgruber avatar Jun 12 '23 04:06 fleimgruber

Yeah fair. I have no idea, I don’t use emacs-jupyter with org mode. Maybe there’s a way to make jupyter always use the same connection Json, so you have one per project. 

rhaps0dy avatar Jun 12 '23 09:06 rhaps0dy

Yes, that's at least halfway then. I will try and see what fits best - thanks for your snippets and ideas.

fleimgruber avatar Jun 13 '23 07:06 fleimgruber