Rich Reports

Creating static PDF reports using Quarto, LaTeX, Word, etc. is a classic way to share results. But there is a simple way to create more dynamic reports that can be easily updated and shared.

Exporting notebooks as HTML from Jupyter notebooks or .qmd files

You can export Jupyter notebooks or Quarto markdown files as HTML documents. These HTML documents can be easily shared and viewed in any web browser. They can also include interactive elements such as plots, widgets, and links. Furthermore, we can embed media such as animations, gifs, videos and interactive plots in these files for the reader to explore!

The beauty with quarto is that you write your report once and can export it to multiple formats such as HTML, PDF, Word, etc. with a single command!

A practical example: Embedding animations in reports

Let’s see how to do this in practice. Take the example of creating an animation of a point moving in a circle. We create this animation in a python block in a Jupyter notebook or a .qmd file and quarto will take care of rendering it in the final report.

Here we will first create the animation as a gif file and then embed it in the report.

The circle is given by the parametric equations:

\[ \begin{align*} x(t) & =r\cos(t)\\ y(t) & =r\sin(t) \end{align*} \]

where \(t\) is the parameter representing the angle in radians, and \(r=1\) is the radius of the circle. We create a numpy array of \(N=100\) values of \(t\) evenly spaced between \(0\) and \(2\pi\). Then for every value of \(t\) a red point is drawn at the corresponding \((x,y)\) coordinates. Finally, we save the animation as a gif file using the PillowWriter from the matplotlib.animation module.

Code
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig, ax = plt.subplots()
line, = ax.plot([], [], 'ro')
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)
t = np.linspace(0, 2*np.pi, 100)
y = np.sin(t)
x = np.cos(t)
ax.plot(x, y, 'b-')
ax.set_aspect('equal', 'box')

def update(frame):
    x = np.cos(frame)
    y = np.sin(frame)
    line.set_data([x], [y])
    return line,
ani = animation.FuncAnimation(fig, update, frames=t, blit=True)
plt.close()
ani.save('circle_animation.gif', writer='pillow')

In this code we are using the matplotlib.animation module to create an animation of a point moving in a circle. We save the animation as a gif file using the PillowWriter. To embed the gif in the report, we use the markdown syntax for images.

![Circle Animation](circle_animation.gif)

Circle Animation
Important

Saving animations as gif files may require installing additional libraries such as Pillow. Make sure to install any necessary dependencies to ensure proper functionality.

We can export this notebook as HTML, having it include all media such as images and animations, using the command:

quarto render RichMediaReports.ipynb --to html --embed-resources

You can also add a YAML header to the notebook or .qmd file to specify that resources should be embedded in the HTML output:

format:
  html:
    theme:
      dark: darkly
      light: flatly
    embed-resources: true
    toc: true

This sets the theme for both dark and light modes, making dark the default. It also enables embedding of resources and adds a table of contents to the HTML output.

Having this in the YAML header means you can simply run:

quarto render RichMediaReports.ipynb 
Warning

Embedding resources such as gifs can significantly increase the size of the HTML file. Be cautious when embedding large media files, as it may affect loading times and performance.

Especially gif files, while simple, are notoriously inefficient in terms of file size. For larger animations or videos, consider using more efficient formats like MP4 or WebM and embedding them using HTML5 video tags.

Exporting to video

We can export the same animation to a video file format such as MP4. This is usuallly much more efficient than using gif files, especially for longer animations or higher quality videos.

Code
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig, ax = plt.subplots()
line, = ax.plot([], [], 'ro')
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)
t = np.linspace(0, 2*np.pi, 360*2)
y = np.sin(t)
x = np.cos(t)
ax.plot(x, y, 'b-')
ax.set_aspect('equal', 'box')

def update(frame):
    x = np.cos(frame)
    y = np.sin(frame)
    line.set_data([x], [y])
    return line,
ani = animation.FuncAnimation(fig, update, frames=t, blit=True)
plt.close()
ani.save('circle_animation.mp4', writer='ffmpeg', fps=30, dpi=200)

We include a mp4 video using HTML5 video tags. Here is an example of how to do this:

```{=html}
<video width="100%" height="auto" controls autoplay loop muted style="width: 100%; height: auto;" >
  <source src="circle_animation.mp4" type="video/mp4">
  Your browser does not support the video tag.
</video>
```

Exporting interactive plots - html-js

FuncAnimation from matplotlib.animation can also export animations as interactive HTML-JS files. This allows the reader to interact with the animation directly in the web browser, providing a more engaging experience.

Code
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML

fig, ax = plt.subplots()
line, = ax.plot([], [], 'ro')
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)
t = np.linspace(0, 2*np.pi, 60)
y = np.sin(t)
x = np.cos(t)
ax.plot(x, y, 'b-')
ax.set_aspect('equal', 'box')

def update(frame):
    x = np.cos(frame)
    y = np.sin(frame)
    line.set_data([x], [y])
    return line,
ani = animation.FuncAnimation(fig, update, frames=t, blit=True)
plt.close()


html_content = ani.to_jshtml(default_mode='loop')
HTML(f"""
<div style="width:100%">
<style>img {{ max-width: 100% !important; }}</style>
{html_content}
</div>
<script>setTimeout(() => document.querySelector('button[title="Play"]')?.click(), 100);</script>
""")
Warning

These js-html files can be quite large. Be vary carful with how many frames you include in the animation. Above we used 60 frames for the circle animation. Including too many frames will results in an error. You can override this limit but loading times may be long, crash canvas, freeze the browser and generate huge files.

Using mechanicskit.to_responsive_html

Note the boiler plate code below to make the animation responsive in width and to auto start the animation. You can use the MechanicsKit library to simplify this process.

import mechanicskit as mk
mk.to_responsive_html(anim, container_id='circle-animation')

See MechanicsKit for more details.