How to redirect/render Pyodide output in browser?

Question:

I have recently come across the Pyodide project.

I have built a little demo using Pyodide, but although I’ve spent a lot of time looking at the source, it is not obvious (yet) to me how to redirect print output from python (other than modifying the CPython source), and also, how to redirect output from matplotlib.pyplot to the browser.

From the source code, FigureCanvasWasm does have a show() method with the appropriate backend for plotting to the browser canvas – however, it is not clear to me how to instantiate this class and invoke it’s show() method or indeed, if there is another more obvious way of redirecting plots to canvas.

My questions therefore are:

  1. How do I redirect print() messages
  2. How do I force pyodide to plot matplotlib figures in the browser?

Here is my test page:

<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
    <title>Demo</title>
    <script src="../../pyodide/build/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
      pyodide.loadPackage(['matplotlib']).then(() => {
          pyodide.runPython(`
                  import matplotlib.pyplot as plt
                  plt.plot([1, 2, 3, 4])
                  plt.ylabel('some numbers')
                  #fig = plt.gcf()
                  #fig.savefig(imgdata, format='png')                  
                  print('Done from python!')`
          );
          //var image = pyodide.pyimport('imgdata');
          //console.log(image);
      });});

    </script>
<html>

Answers:

First of all let’s see if we can get just anything to show up in the browser; e.g. a normal string. Python variables are stored in the pyodide.globals attribute. Hence we can take the python object from there and place it into a <div> element on the page.

<!doctype html>
<meta charset="utf-8">
<html>
<head>
    <title>Demo</title>
    <script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
          pyodide.runPython(`my_string = "This is a python string." `);

          document.getElementById("textfield").innerText = pyodide.globals.my_string;
      });

    </script>

    <div id="textfield"></div>
<html>

Now I guess we can do the same with a matplotlib figure. The following would show a saved png image in the document.

<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
    <title>Demo</title>
    <script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
      pyodide.loadPackage(['matplotlib']).then(() => {
          pyodide.runPython(`
                import matplotlib.pyplot as plt
                import io, base64

                fig, ax = plt.subplots()
                ax.plot([1,3,2])

                buf = io.BytesIO()
                fig.savefig(buf, format='png')
                buf.seek(0)
                img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')`
          );

          document.getElementById("pyplotfigure").src=pyodide.globals.img_str

      });});

    </script>

    <div id="textfield">A matplotlib figure:</div>
    <div id="pyplotdiv"><img id="pyplotfigure"/></div>
<html>

I haven’t looked into the backends.wasm_backend yet, so that may allow for a more automated way of the above.

When using the wasm backend, the canvas property of a figure is an instance of FigureCanvasWasm. Calling the show() method of the canvas should be sufficient to display the figure in the browser. Unfortunately a minor bug in the create_root_element() method of the canvas prevents the figure from being displayed. This method creates a div element that will contain the figure. It tries first to create an iodide output div element. If that fails a plain HTML div element is created. This element however is never appended to the document and remains therefore invisible.

Below are the lines of code from FigureCanvasWasm were it happens

def create_root_element(self):
    # Designed to be overridden by subclasses for use in contexts other
    # than iodide.
    try:
        from js import iodide
        return iodide.output.element('div')
    except ImportError:
        return document.createElement('div')

The comment suggests the non-iodide code is a stub that needs to be extended, by overriding the method. This would require subclassing FigureCanvasWasm, installing it as a pyodide module and configuring matplotlib to use that backend.

There is a shortcut however, because python allows overriding a method of an instance, without modifying the class, as per question 394770. Putting the following code in your HTML document gives a figure in the browser

import numpy as np
from matplotlib import pyplot as plt
from js import document

x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)

f = plt.figure()
plt.plot(x,y)

# ordinary function to create a div
def create_root_element1(self):
    div = document.createElement('div')
    document.body.appendChild(div)
    return div

#ordinary function to find an existing div
#you'll need to put a div with appropriate id somewhere in the document
def create_root_element2(self):
    return document.getElementById('figure1')

#override create_root_element method of canvas by one of the functions above
f.canvas.create_root_element = create_root_element1.__get__(
    create_root_element1, f.canvas.__class__)

f.canvas.show()

Initially the toolbar did not show icons. I had to download, unzip and install fontawesome alongside pyodide and include the following line in the header to get those

<link rel="stylesheet" href="font-awesome-4.7.0/css/font-awesome.min.css">

Edit:
About the first part of your question, redirecting the output stream to the browser, you could take a look at how it is done in pyodide’s console.html.

It replaces sys.stdout by a StringIO object

pyodide.runPython(`
    import sys
    import io
    sys.stdout = io.StringIO()
`);

Then run the python code (that can be completely oblivious to the fact that it is running in a wasm context)

pyodide.runPython(`
    print("Hello, world!")
`);

Finally, send the contents of the stdout buffer to an output element

var stdout = pyodide.runPython("sys.stdout.getvalue()")
var div = document.createElement('div');
div.innerText = stdout;
document.body.appendChild(div);

I created a simple interactive shell for Python. Read this tutorial if you need more detailed information.

(async () => { // create anonymous async function to enable await

  var output = document.getElementById("output")
  var code = document.getElementById("code")
  
  output.value = 'Initializing...n'

  window.pyodide = await loadPyodide({stdout: addToOutput, stderr: addToOutput}) // redirect stdout and stderr to addToOutput
        output.value += 'Ready!n' 
})()


function addToOutput(s) {
  output.value += `${s}n`
  output.scrollTop = output.scrollHeight
}

async function evaluatePython() {
  addToOutput(`>>>${code.value}`)

  await pyodide.loadPackagesFromImports(code.value, addToOutput, addToOutput)
  try {
    let result = await pyodide.runPythonAsync(code.value)
    addToOutput(`${result}`)
  }
  catch (e) {
    addToOutput(`${e}`)
  }
  code.value = ''
}

<script src="https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js"></script>

Output:
<textarea id="output" style="width: 100%;" rows="10" disabled=""></textarea>
<textarea id="code" rows="3">import numpy as np
np.ones((10,))
</textarea>
<button id="run" onclick="evaluatePython()">Run</button>

Here is the example for matplotlib. Note that this will load a bunch of dependencies which will take up to several minutes.

const python_code = `
from js import document
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import io, base64

def generate_plot_img():
  # get values from inputs
  mu = int(document.getElementById('mu').value)
  sigma = int(document.getElementById('sigma').value)
  # generate an interval
  x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100)
  # calculate PDF for each value in the x given mu and sigma and plot a line 
  plt.plot(x, stats.norm.pdf(x, mu, sigma))
  # create buffer for an image
  buf = io.BytesIO()
  # copy the plot into the buffer
  plt.savefig(buf, format='png')
  buf.seek(0)
  # encode the image as Base64 string
  img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
  # show the image
  img_tag = document.getElementById('fig')
  img_tag.src = img_str
  buf.close()
`;

(async () => { // create anonymous async function to enable await
  window.pyodide = await loadPyodide() 
  await pyodide.loadPackagesFromImports(python_code)
  await pyodide.runPythonAsync(python_code)

})()

<script src="https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js"></script>

mu:
<input id='mu' value='1' type="number">
<br><br>
sigma:
<input id='sigma' value='1' type="number">
<br><br>
<button onclick='pyodide.globals.get("generate_plot_img")()'>Plot</button>
<br>
<img id="fig" />

Answered By: Aray Karjauv

To show print() calls form pyodide you can use the parameters on loadPyodide to redirect stdout:

var paragraph = document.getElementById("p");

pyodide = await loadPyodide({
    indexURL : "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
    stdin: window.prompt,
    stdout: (text) => {paragraph.textContent += text;},
    stderr: (text) => {paragraph.textContent += text;}
  });

https://github.com/pyodide/pyodide/blob/main/src/js/pyodide.js

Answered By: Luis F.

I noticed that if "plt.show()" is included in the code, the image is automatically plotted.

pyodide.runPython("import matplotlib.pyplot as plt; fig = plt.figure(figsize=(8,3)); plt.plot([1, 2, 3, 4]); plt.ylabel('some numbers'); plt.show(); print('Done from python!')");
Answered By: Jamilah Foucher