#!/usr/bin/env python
# coding: utf-8
# ## For [How do I construct a python function where the input in python code and output is ipython rich output (in HTML)?](https://stackoverflow.com/q/74683522/8508004), based on [Can I capture rich outputs in plain python sessions ?](https://github.com/ipython/ipython/issues/11163#issuecomment-393338313), and [Module: utils.capture](https://ipython.readthedocs.io/en/stable/api/generated/IPython.utils.capture.html)
# In[1]:
pycode = '''
from matplotlib import pyplot as plt
plt.plot([1, 2, 3])
plt.show()
'''
from IPython.utils import io
with io.capture_output() as captured:
exec(pycode)
# In[2]:
type(captured)
# Based on and [Module: utils.capture](https://ipython.readthedocs.io/en/stable/api/generated/IPython.utils.capture.html)
# In[3]:
captured.outputs[0]
# In[4]:
type(captured.outputs[0])
# In[5]:
captured.show()
# In[6]:
captured()
# In[7]:
from IPython.display import display
for o in captured.outputs:
display(o)
# ## Want a figure?
# In[8]:
pycode = '''
from matplotlib import pyplot as plt
plt.plot([1, 2, 3])
plt.savefig('my_plot.png')
plt.show()
'''
from IPython.utils import io
with io.capture_output() as captured:
exec(pycode)
# In[9]:
from IPython.display import Image
Image("my_plot.png")
# So adding in the saving the plot as an image in the original use of matplotlib works.
#
# If you include a line assigning the plot to a Matplotlib figure object before you make the plot, you can also decide after-the-fact to save it as an image.
# In[10]:
pycode = '''
from matplotlib import pyplot as plt
fig = plt.figure()
plt.plot([1, 2, 3])
plt.show()
'''
from IPython.utils import io
with io.capture_output() as captured:
exec(pycode)
# In[11]:
fig.savefig("after_the_fact_save.png")
# In[12]:
from IPython.display import Image
Image("after_the_fact_save.png")
# So adding in the saving the plot as an image via matplotlib works.
#
# But **can you save the 'RichOuput' from Matplotlib**, `captured.outputs[0]` in this case, to an image, too? So you can keep the matplotlib handling simpler and decide after-the-fact if you want the image and get it progammatically. (Because when in JupyterLab, you have the option to right-click on the plot and choose 'Create New View for Ouput' and then right-clicking on that in the 'Output View' and choosing to 'Save Image As..' to collect an image file by hand.)
# Based on [here](https://github.com/ipython/ipython/issues/11163#issuecomment-393995207), I didn't think that is easy to do. And I was going to suggest digging into how [nbgather](https://github.com/microsoft/gather) works, sugged [here](https://discourse.jupyter.org/t/is-there-a-way-to-save-the-output-of-a-cell/2489/2?u=fomightez) may provide a way; howver, it didn't seem straightforward right now.
#
# However, I found two options based on search of 'jupyter %%capture send to image file'. First based on [a reply to 'Exporting a single jupyter cell output'](https://github.com/jupyter/notebook/issues/3039#issuecomment-583860159), where use of '%%capture to save the png output' is discussed.
#
# So I tried:
#
# ```python
# with open('from_richoutputs.png', 'wb') as f:
# f.write(captured.outputs[0].data['image/png'])
# ```
#
# However, that gives 'TypeError: a bytes-like object is required, not 'str'' because it turns out that seems written for something already outputing `.png`, I think. Even though the pseudocode doesn't work that way.
# So similar with [the other promising idea](https://discourse.jupyter.org/t/cell-magic-to-save-image-output-as-a-png-file/11906/2) that turns out to be useful only when the RichOutput was a `.png`. And so I'm back to thinking not easy with matplotlib and be at least be happy one route works. (The [page with the other promising idea](https://discourse.jupyter.org/t/cell-magic-to-save-image-output-as-a-png-file/11906/2) also includes metnion of nbinteract/scrapbook and maybe that is appropriate here?)
# ## Can you make the plot HTML code?
#
# ### nbconvert of a notebook with just that code and output to get HTML
#
# If you are working in Jupyter than one option to get HTML version of the plot would be to use nbconvert to make the HTML, see [here](https://nbconvert.readthedocs.io/en/latest/usage.html). Example:
#
# ```shell
# jupyter nbconvert --to html so74683522.ipynb
# ```
#
# The produced HTML will contain code needed to make the plot.
# You can imagine just making a notebook where the only cell is what produces the plot to make it easy to find the pertinent code.
# In fact, let's demonstrate that by making a notebook that will only have the following single cell, with no output yet:
#
# ```python
# pycode = '''
# from matplotlib import pyplot as plt
# plt.plot([1, 2, 3])
# plt.show()
# '''
# exec(pycode)
# ```
#
# To do that, I'll us nbformat to make a Jupyter notebook `.ipynb` with that code as a single cell in it by running the next cell:
# In[13]:
# based on https://stackoverflow.com/a/62105736/8508004 (setting metadata to python based on https://github.com/ParaToolsInc/taucmdr/blob/70daea6e775903a04d3008f4500c0ea60970e023/packages/taucmdr/analysis/analysis.py)
from nbformat import v4 as nbf
import nbformat
code_source = "pycode = '''\nfrom matplotlib import pyplot as plt\nplt.plot([1, 2, 3])\nplt.show()\n'''\nexec(pycode)"
new_ntbk = nbf.new_notebook()
cells = [nbf.new_code_cell(code_source)]
new_ntbk['cells'] = cells
new_ntbk['metadata']['kernelspec'] = nbformat.NotebookNode()
new_ntbk['metadata']['kernelspec']['name'] = 'python3'
new_ntbk['metadata']['kernelspec']['language'] = 'python'
new_ntbk['metadata']['kernelspec']['display_name'] = 'Python 3 (ipykernel)'
nbformat.write(new_ntbk, "single_cell_exec_without_output_via_nbf.ipynb")
# We can see that is all that is there:
# In[14]:
cat single_cell_exec_without_output_via_nbf.ipynb
# That's it. Just contains that small block of code we made above. Let's excute it
# In[15]:
# based on https://nbconvert.readthedocs.io/en/latest/execute_api.html#executing-notebooks-using-the-python-api-interface
notebook_filename = "single_cell_exec_without_output_via_nbf.ipynb"
import nbformat
from nbconvert.preprocessors import ExecutePreprocessor
with open(notebook_filename) as f:
nb = nbformat.read(f, as_version=4)
ExecutePreprocessor().preprocess(nb, {'metadata': {'path': './'}})
with open('executed_notebook.ipynb', 'w', encoding='utf-8') as f:
nbformat.write(nb, f)
# That will execute the notebook. Now to get the HTML we'll convert it and specify we don't want the input cell source in the result, see [here](https://stackoverflow.com/a/58524517/8508004) for more about the `--no-input` flag to leave out the code cells from the produced HTML.
# In[16]:
get_ipython().system('jupyter nbconvert --no-input --to html executed_notebook.ipynb')
# Let's view the first few lines of HTML code produced.
# In[17]:
get_ipython().system('head executed_notebook.html')
# Replace `!head` part of the command above with `cat` and re-run it to see it all. There's a lot and so I just wanted to show the top to demonstrate it indeed produced an HTML file. You should be able to take the code and embed it elsewhere. In fact, if you really examine it you'll see it has an image of the plot embedded as base64. You can deduce how to embed only that elsewhere from examples below.
#
# To view the HTML as HTML, in JupyterLab you can just double-click on the HTML icon in the file browser pane to the left. You'll note there is just a plot.
#
# #### Make a image of the plot after-the-fact and converting that to HTML
#
# Also, using the figure, you can create a Base64 version of the plot image to embed in HTML, see [here](https://stackoverflow.com/a/51533307/8508004).
# In fact let's see that demonstrated in this notebook, based on a variation [here](https://stackoverflow.com/a/36993713/8508004), in reply to [Imbed [sic] matplotlib figure into iPython HTML](https://stackoverflow.com/q/36991600/8508004). Plus, updating that to Python 3 using [here](https://discourse.matplotlib.org/t/savefig-and-stringio-error-on-python3/18894/3) and [here](https://stackoverflow.com/a/62498083/8508004).
# In[18]:
from IPython.core.display import HTML
import binascii
#from io import StringIO
from io import BytesIO
import base64
import matplotlib.pyplot as plt
# open IO object
#sio = StringIO()
sio = BytesIO()
pycode = '''
from matplotlib import pyplot as plt
fig = plt.figure()
plt.plot([1, 2, 3])
plt.show()
'''
from IPython.utils import io
with io.capture_output() as captured:
exec(pycode)
# print raw canvas data to IO object
#fig.canvas.print_png(sio)
fig.savefig(sio, format="png")
# convert raw binary data to base64
# I use this to embed in an img tag
#img_data = binascii.b2a_base64(sio.getvalue())
img_data = base64.encodebytes(sio.getvalue()).decode()
# keep img tag outter html in its own variable
img_html = ''.format(img_data)
HTML("