Skip to content

Commit

Permalink
Merge pull request #147 from ianhi/animations
Browse files Browse the repository at this point in the history
Animations
  • Loading branch information
ianhi authored Nov 24, 2020
2 parents 47e87fb + 076025a commit 4bdb9e2
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,5 @@ autoapi
# this is to enable the madness with adding gifs
docs/examples/tidbits/*.ipynb
docs/examples/*.ipynb
docs/examples/*.gif
docs/*.ipynb
5 changes: 5 additions & 0 deletions docs/gifmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
import base64
import os
import glob
import shutil


def gogogo_all(source_dir, dest_dir):
"""
copy and render all the notebooks from one dir to another
"""
notebooks = glob.glob(os.path.join(source_dir, "*.ipynb"))
gifs = glob.glob(os.path.join(source_dir, "*.gif"))
for nb in notebooks:
to = os.path.join(dest_dir, os.path.basename(nb))
gogogo_gif(nb, to)
for gif in gifs:
to = os.path.join(dest_dir, os.path.basename(gif))
shutil.copyfile(gif, to)


def gogogo_gif(notebook_from, notebook_to):
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Further discussion of the behavior as a function of backend can be found on the
examples/hist.ipynb
examples/mpl-sliders.ipynb
examples/scatter-selector.ipynb
examples/animations.ipynb
examples/image-segmentation.ipynb
examples/zoom-factory.ipynb
examples/heatmap-slicer.ipynb
Expand Down
187 changes: 187 additions & 0 deletions examples/animations.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Saving Animations\n",
"\n",
"Since the controls object knows how to update figures as the sliders change their values it is also able to save an animation (e.g. `.gif` or `.mp4` by updating the slider values for you. Under the hood this makes use of [FuncAnimation](https://matplotlib.org/api/_as_gen/matplotlib.animation.FuncAnimation.html?highlight=funcanimation#matplotlib.animation.FuncAnimation) and you can pass any relevant kwargs in via `func_anim_kwargs`. Other `kwargs` will passed to `animation.save`.\n",
"\n",
"Saving animations will work with either ipywidgets Sliders or with matplotlib Sliders. However, it will not work with other widgets. (This is an potential area of improvement, PRs welcome)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%matplotlib ipympl\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"import mpl_interactions.ipyplot as iplt"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basic Usage"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"x = np.linspace(0, 2 * np.pi, 200)\n",
"\n",
"\n",
"def f(x, amp, freq):\n",
" return amp * np.sin(x * freq)\n",
"\n",
"\n",
"# Create the plot as normal\n",
"fig, ax = plt.subplots()\n",
"controls = iplt.plot(x, f, freq=(0.05, 10, 250), amp=(1,10))\n",
"_ = iplt.title(\"the Frequency is: {freq:.2f}\", controls=controls[\"freq\"])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# save as a gif\n",
"anim = controls.save_animation(\"freq-plot-1.gif\", fig, \"freq\", interval=35)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Which Generates this GIF\n",
"\n",
"![gif of sin animated over frequency](freq-plot-1.gif)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Embeding the animation in a noteook.\n",
"\n",
"To embed the animation you can do:\n",
"\n",
"1. Link to it in markdown cell with `![alt-text](path/to/image)`\n",
"2. Drag the file into a markdown cell\n",
"3. Embed `anim.to_html5_video()` using IPython.display.Video:\n",
"```python\n",
"from IPython.display import Video\n",
"Video(anim.to_html5_video(), embed=True)\n",
"```\n",
"\n",
"4. Use IPython to display the saved gif\n",
"\n",
"You can also read more in this excellent blog post: http://louistiao.me/posts/notebooks/embedding-matplotlib-animations-in-jupyter-as-interactive-javascript-widgets/"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# NBVAL_IGNORE_OUTPUT\n",
"from IPython.display import Image\n",
"\n",
"Image(\"freq-plot-1.gif\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Matplotlib Sliders with `valstep=None`\n",
"\n",
"Matplotlib sliders have an optional attribute `valstep` that allows for discrete slider steps. `mpl-interactions` uses this for all sliders that it creates, however if you passed a custom made slider in as a kwarg you may not have used `valstep` if this is the case then the `save_animation` function cannot infer how many frames it should render, so you can specify this with the `N_frames` arguments."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from matplotlib.widgets import Slider\n",
"\n",
"import mpl_interactions.ipyplot as iplt\n",
"\n",
"fig, ax = plt.subplots()\n",
"plt.subplots_adjust(bottom=0.25)\n",
"x = np.linspace(0, 2 * np.pi, 200)\n",
"\n",
"\n",
"def f(x, freq):\n",
" return np.sin(x * freq)\n",
"\n",
"\n",
"axfreq = plt.axes([0.25, 0.1, 0.65, 0.03])\n",
"slider = Slider(axfreq, label=\"freq\", valmin=0.05, valmax=10) # note the lack of valstep\n",
"controls2 = iplt.plot(x, f, freq=slider, ax=ax)\n",
"_ = iplt.title(\"the Frequency is: {freq:.2f}\", controls=controls2[\"freq\"])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# save as a gif\n",
"anim2 = controls2.save_animation(\"freq-plot-2.gif\", fig, \"freq\", interval=35, N_frames=100)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Gives this GIF:\n",
"\n",
"![freq-plot-2.gif](freq-plot-2.gif)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
5 changes: 3 additions & 2 deletions examples/devlop/devlop-base.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"import ipywidgets as widgets\n",
"%load_ext autoreload\n",
"%autoreload 2\n",
"from mpl_interactions import *"
"from mpl_interactions import *\n",
"import mpl_interactions.ipyplot as iplt"
]
},
{
Expand All @@ -39,7 +40,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.8"
"version": "3.9.0"
}
},
"nbformat": 4,
Expand Down
Binary file added examples/freq-plot-1.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/freq-plot-2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion mpl_interactions/_version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version_info = (0, 13, 1)
version_info = (0, 14, 0)
__version__ = ".".join(map(str, version_info))
68 changes: 68 additions & 0 deletions mpl_interactions/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from functools import partial
from collections.abc import Iterable
from matplotlib.widgets import AxesWidget
from matplotlib.widgets import Slider as mSlider
from matplotlib.animation import FuncAnimation


class Controls:
Expand Down Expand Up @@ -181,6 +183,72 @@ def __getitem__(self, key):
key = []
return self, key

def save_animation(
self, filename, fig, param, interval=20, func_anim_kwargs={}, N_frames=None, **kwargs
):
"""
Save an animation over one of the parameters controlled by this `controls` object.
Parameters
----------
filename : str
fig : figure
param : str
the name of the kwarg to use to animate
interval : int, default: 2o
interval between frames in ms
func_anim_kwargs : dict
kwargs to pass the creation of the underlying FuncAnimation
N_frames : int
Only used if the param is a matplotlib slider that was created without a
valstep argument. This will only be relevant if you passed your own matplotlib
slider as a kwarg when plotting. If needed but not given it will default to
a value of 200.
**kwargs :
Passed through to anim.save
Returns
-------
anim : matplotlib.animation.FuncAniation
"""
slider = self.controls[param]
ipywidgets_slider = False
if "Box" in str(slider.__class__):
ipywidgets_slider = True
for obj in slider.children:
if "Slider" in str(obj.__class__):
slider = obj
N = int((slider.max - slider.min) / slider.step)
min_ = slider.min
max_ = slider.max
step = slider.step
elif isinstance(slider, mSlider):
min_ = slider.valmin
max_ = slider.valmax
if slider.valstep is None:
N = N_frames if N_frames else 200
step = (max_ - min_) / N
else:
N = int((max_ - min_) / slider.valstep)
step = slider.valstep

def f(i):
val = min_ + step * i
if ipywidgets_slider:
slider.value = val
else:
slider.set_val(val)
return []

repeat = func_anim_kwargs.pop("repeat", False)
anim = FuncAnimation(fig, f, frames=N, interval=interval, repeat=repeat, **func_anim_kwargs)
# draw then stop necessary to prevent an extra loop after finished saving
# see https://discourse.matplotlib.org/t/how-to-prevent-funcanimation-looping-a-single-time-after-save/21680/2
fig.canvas.draw()
anim.event_source.stop()
anim.save(filename, **kwargs)
return anim

def display(self):
"""
Display the display the ipywidgets controls or show the control figures
Expand Down

0 comments on commit 4bdb9e2

Please sign in to comment.