emacs-jupyter / jupyter

An interface to communicate with Jupyter kernels.
GNU General Public License v3.0
940 stars 92 forks source link

Python code block returning file name for figure file #378

Open spluque opened 2 years ago

spluque commented 2 years ago

I've recently switched to emacs-jupyter for handling Python code blocks in org-mode. I want to save a plot to png format, display it as #+RESULTS, as well as export it. However, the block is returning a badly formatted link:

#+BEGIN_SRC jupyter-python :session py :results file link :exports output
import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(np.random.randn(10))
ofile = "junk.png"
fig.savefig(ofile)
plt.close()
ofile
#+END_SRC

#+RESULTS:
[[file:: junk.png
]]

Any pointers welcome.

timlod commented 2 years ago

I recommend using the :file /path/to/figure header argument to save figures as png - it's simpler and requires less code. Check out the "Rich kernel output" section of the readme of this repo. Ie:

#+BEGIN_SRC jupyter-python :session py :file junk.png :exports output
import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(np.random.randn(10))
#+END_SRC

#+RESULTS:
: your figure will show up here as the link you specified in the header
spluque commented 2 years ago

I was hoping to be able to define the file path within the source block. For example, one block has pandas read a file from one path to generate a dataframe, produce a plot, and save a png in the same containing folder, with a name consistent with the base name of the input data file. The plot also needs to be closed.

It seems as if the example I showed should work, but something is botched with the generated link to the file. Using the built-in ob-python, with :session py :results file header arguments, it returns the proper link as: [[file:junk.png]] under #+RESULTS.

dlukes commented 2 years ago

The :file /path/to/figure feature is super nice, but it isn't always usable in more complicated scenarios. For instance, it doesn't work with the Altair plotting library, which only generates a PNG version of the figure in the process of saving it to disk.

A possible solution, lifted from here, is to set the mimetype of the output to text/org, and generate the appropriate link yourself:

#+begin_src jupyter-python
import altair as alt
import seaborn as sns
from IPython.display import publish_display_data

iris = sns.load_dataset("iris")
chart = alt.Chart(iris).mark_circle().encode(
    x="sepal_length",
    y="sepal_width",
    color="species",
)
fname = "chart.png"
chart.save(fname)
publish_display_data({"text/org": f"[[file:{fname}]]"})
#+end_src

#+RESULTS:
:RESULTS:
[[file:chart.png]]
:END:

See also https://github.com/nnicandro/emacs-jupyter/issues/237.

dlukes commented 2 years ago

the Altair plotting library, which only generates a PNG version of the figure in the process of saving it to disk

Oops, just discovered this is not actually true. You can use a PNG renderer to make Altair output an image directly, which then works perfectly fine with :file ...:

#+begin_src jupyter-python :file chart_via_renderer.png
alt.renderers.enable("png")
chart
#+end_src

#+RESULTS:
[[file:chart_via_renderer.png]]

Still, the mimetype workaround still works for more complicated workflows, where you want to generate the filename dynamically in code.