3.10  Interactive HTML Reports

What if your report could move? What if your reader could drag a slider and watch your simulation respond in real-time? What if your convergence plot let them zoom in on the interesting region?

This is the power of HTML reports.

While PDFs are the gold standard for formal submissions, HTML reports unlock a dimension that static documents simply cannot reach: interactivity. Your animations play automatically. Your plots respond to mouse hovers. Your readers can explore your data instead of just viewing it.

And the best part? You write the same notebook, just render to HTML instead of PDF.

When to Use HTML vs PDF

Consideration Choose PDF Choose HTML
Formal submission ✅ Required for journals, theses ❌ Usually not accepted
Animations ❌ Static frames only ✅ Full motion, autoplay
Interactive plots ❌ Not possible ✅ Zoom, pan, hover tooltips
Sharing online ⚠️ Requires download ✅ Opens in any browser
File size ✅ Compact ⚠️ Can be large with media
Printing ✅ Designed for print ⚠️ May not print well
Canvas/LMS upload ✅ Standard ✅ Also works!
LaTeX installation Required Not needed

Rule of thumb:

  • Use PDF for formal academic submissions
  • Use HTML when your work includes animations, simulations, or interactive visualizations, or when you want to share online
Tip

You can upload HTML files to Canvas just like PDFs! This gives your instructor a richer view of your work. Some even prefer it for computational assignments.

Embedding Animations

Let’s make a report come alive. We’ll animate a point moving around a circle using three different approaches, each with its own trade-offs.

Approach 1: GIF — Quick and Simple

GIFs are the easiest way to add animation. They work everywhere and require minimal code. The downside? They’re inefficient (large files) and have limited color depth (256 colors).

Best for: Quick demos, simple animations, maximum compatibility.

The circle is given by the parametric equations:

\[ x(t) = r\cos(t), \quad y(t) = r\sin(t) \]

where \(t \in [0, 2\pi]\) and \(r = 1\).

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')

Embed the GIF using standard markdown:

![Circle Animation](circle_animation.gif)

Circle Animation
Warning

GIF files can be surprisingly large. A 5-second animation can easily exceed 1 MB. For longer or higher-quality animations, use MP4 instead.

Approach 2: MP4 Video — Better in Every Way

MP4 is superior to GIF in almost every respect: smaller files, better quality, full color, and playback controls. The only downside is you need ffmpeg installed.

Best for: Any animation longer than a few seconds, high-quality visuals, smooth playback.

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)

Embed MP4 using HTML5 video tags with autoplay and loop:

```{=html}
<video width="100%" controls autoplay loop muted>
  <source src="circle_animation.mp4" type="video/mp4">
</video>
```
Format File Size (typical) Quality Browser Support Controls
GIF Large (1-10 MB) 256 colors Universal None
MP4 Small (100 KB-1 MB) Full color Universal Play/pause/seek

Approach 3: JS-HTML — Interactive Exploration

This is where HTML truly shines. Instead of a passive video, you get an interactive player with a slider that lets you scrub through the animation frame by frame. Your reader can pause, rewind, and explore at their own pace.

Best for: Educational content, when readers need to study specific frames, exploratory analysis.

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

JS-HTML animations embed each frame as a base64 image, so file size grows quickly. Keep frame counts reasonable (< 100 frames). For longer animations, use MP4.

You can simplify the boilerplate using MechanicsKit:

import mechanicskit as mk
mk.to_responsive_html(ani, container_id='my-animation')

Interactive Plots with Plotly

Static matplotlib plots are fine, but what if your reader could zoom in on that interesting spike, or hover over a point to see its exact value? Plotly makes this trivial.

Code
import plotly.express as px
import plotly.io as pio
import numpy as np
import pandas as pd

# Required for VSCode + Quarto compatibility
pio.renderers.default = "plotly_mimetype+notebook_connected"

# Generate some data
np.random.seed(42)
n = 100
df = pd.DataFrame({
    'x': np.linspace(0, 10, n),
    'y': np.sin(np.linspace(0, 10, n)) + 0.1*np.random.randn(n),
    'category': np.random.choice(['A', 'B', 'C'], n)
})

fig = px.scatter(df, x='x', y='y', color='category',
                 title='Interactive Scatter Plot: Try zooming and hovering!',
                 hover_data=['x', 'y'])
fig.show()

Try it: click and drag to zoom, double-click to reset, hover over points to see values.

Plotly works seamlessly with Quarto HTML output. Install with:

uv pip install plotly

Rendering to HTML

To create a self-contained HTML file with all media embedded:

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

Or add to your YAML header:

---
title: "My Interactive Report"
format:
  html:
    embed-resources: true
    theme:
      dark: darkly
      light: flatly
    toc: true
---

The embed-resources: true option embeds all images, CSS, and JavaScript into a single HTML file that can be shared anywhere.

Hosting on GitHub Pages

Want to share your report with a URL instead of a file? GitHub Pages provides free hosting for HTML files.

Quick Setup

  1. Create a GitHub repository for your project

  2. Enable GitHub Pages in repository Settings → Pages → Source: “Deploy from a branch” → select main and /docs

  3. Put your HTML in the docs folder:

    mkdir docs
    quarto render my_report.ipynb --to html --embed-resources --output-dir docs
  4. Commit and push:

    git add docs/
    git commit -m "Add HTML report"
    git push
  5. Your report is live at https://yourusername.github.io/yourrepo/my_report.html

For Multiple Reports

Structure your project like this:

my-project/
├── docs/
│   ├── index.html      # Landing page
│   ├── report1.html
│   └── report2.html
├── report1.ipynb
└── report2.ipynb
Tip

GitHub Pages is perfect for project documentation, course materials, or sharing computational notebooks with colleagues who don’t have Python installed.

Download This Notebook