Skip to content

Commit 4bdb9e2

Browse files
authored
Merge pull request #147 from ianhi/animations
Animations
2 parents 47e87fb + 076025a commit 4bdb9e2

File tree

9 files changed

+266
-3
lines changed

9 files changed

+266
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,5 @@ autoapi
142142
# this is to enable the madness with adding gifs
143143
docs/examples/tidbits/*.ipynb
144144
docs/examples/*.ipynb
145+
docs/examples/*.gif
145146
docs/*.ipynb

docs/gifmaker.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22
import base64
33
import os
44
import glob
5+
import shutil
56

67

78
def gogogo_all(source_dir, dest_dir):
89
"""
910
copy and render all the notebooks from one dir to another
1011
"""
1112
notebooks = glob.glob(os.path.join(source_dir, "*.ipynb"))
13+
gifs = glob.glob(os.path.join(source_dir, "*.gif"))
1214
for nb in notebooks:
1315
to = os.path.join(dest_dir, os.path.basename(nb))
1416
gogogo_gif(nb, to)
17+
for gif in gifs:
18+
to = os.path.join(dest_dir, os.path.basename(gif))
19+
shutil.copyfile(gif, to)
1520

1621

1722
def gogogo_gif(notebook_from, notebook_to):

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Further discussion of the behavior as a function of backend can be found on the
9696
examples/hist.ipynb
9797
examples/mpl-sliders.ipynb
9898
examples/scatter-selector.ipynb
99+
examples/animations.ipynb
99100
examples/image-segmentation.ipynb
100101
examples/zoom-factory.ipynb
101102
examples/heatmap-slicer.ipynb

examples/animations.ipynb

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Saving Animations\n",
8+
"\n",
9+
"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",
10+
"\n",
11+
"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)"
12+
]
13+
},
14+
{
15+
"cell_type": "code",
16+
"execution_count": null,
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"%matplotlib ipympl\n",
21+
"import matplotlib.pyplot as plt\n",
22+
"import numpy as np\n",
23+
"\n",
24+
"import mpl_interactions.ipyplot as iplt"
25+
]
26+
},
27+
{
28+
"cell_type": "markdown",
29+
"metadata": {},
30+
"source": [
31+
"## Basic Usage"
32+
]
33+
},
34+
{
35+
"cell_type": "code",
36+
"execution_count": null,
37+
"metadata": {},
38+
"outputs": [],
39+
"source": [
40+
"x = np.linspace(0, 2 * np.pi, 200)\n",
41+
"\n",
42+
"\n",
43+
"def f(x, amp, freq):\n",
44+
" return amp * np.sin(x * freq)\n",
45+
"\n",
46+
"\n",
47+
"# Create the plot as normal\n",
48+
"fig, ax = plt.subplots()\n",
49+
"controls = iplt.plot(x, f, freq=(0.05, 10, 250), amp=(1,10))\n",
50+
"_ = iplt.title(\"the Frequency is: {freq:.2f}\", controls=controls[\"freq\"])"
51+
]
52+
},
53+
{
54+
"cell_type": "code",
55+
"execution_count": null,
56+
"metadata": {},
57+
"outputs": [],
58+
"source": [
59+
"# save as a gif\n",
60+
"anim = controls.save_animation(\"freq-plot-1.gif\", fig, \"freq\", interval=35)"
61+
]
62+
},
63+
{
64+
"cell_type": "markdown",
65+
"metadata": {},
66+
"source": [
67+
"### Which Generates this GIF\n",
68+
"\n",
69+
"![gif of sin animated over frequency](freq-plot-1.gif)"
70+
]
71+
},
72+
{
73+
"cell_type": "markdown",
74+
"metadata": {},
75+
"source": [
76+
"## Embeding the animation in a noteook.\n",
77+
"\n",
78+
"To embed the animation you can do:\n",
79+
"\n",
80+
"1. Link to it in markdown cell with `![alt-text](path/to/image)`\n",
81+
"2. Drag the file into a markdown cell\n",
82+
"3. Embed `anim.to_html5_video()` using IPython.display.Video:\n",
83+
"```python\n",
84+
"from IPython.display import Video\n",
85+
"Video(anim.to_html5_video(), embed=True)\n",
86+
"```\n",
87+
"\n",
88+
"4. Use IPython to display the saved gif\n",
89+
"\n",
90+
"You can also read more in this excellent blog post: http://louistiao.me/posts/notebooks/embedding-matplotlib-animations-in-jupyter-as-interactive-javascript-widgets/"
91+
]
92+
},
93+
{
94+
"cell_type": "code",
95+
"execution_count": null,
96+
"metadata": {},
97+
"outputs": [],
98+
"source": [
99+
"# NBVAL_IGNORE_OUTPUT\n",
100+
"from IPython.display import Image\n",
101+
"\n",
102+
"Image(\"freq-plot-1.gif\")"
103+
]
104+
},
105+
{
106+
"cell_type": "markdown",
107+
"metadata": {},
108+
"source": [
109+
"## Matplotlib Sliders with `valstep=None`\n",
110+
"\n",
111+
"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."
112+
]
113+
},
114+
{
115+
"cell_type": "code",
116+
"execution_count": null,
117+
"metadata": {},
118+
"outputs": [],
119+
"source": [
120+
"from matplotlib.widgets import Slider\n",
121+
"\n",
122+
"import mpl_interactions.ipyplot as iplt\n",
123+
"\n",
124+
"fig, ax = plt.subplots()\n",
125+
"plt.subplots_adjust(bottom=0.25)\n",
126+
"x = np.linspace(0, 2 * np.pi, 200)\n",
127+
"\n",
128+
"\n",
129+
"def f(x, freq):\n",
130+
" return np.sin(x * freq)\n",
131+
"\n",
132+
"\n",
133+
"axfreq = plt.axes([0.25, 0.1, 0.65, 0.03])\n",
134+
"slider = Slider(axfreq, label=\"freq\", valmin=0.05, valmax=10) # note the lack of valstep\n",
135+
"controls2 = iplt.plot(x, f, freq=slider, ax=ax)\n",
136+
"_ = iplt.title(\"the Frequency is: {freq:.2f}\", controls=controls2[\"freq\"])"
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"metadata": {},
143+
"outputs": [],
144+
"source": [
145+
"# save as a gif\n",
146+
"anim2 = controls2.save_animation(\"freq-plot-2.gif\", fig, \"freq\", interval=35, N_frames=100)"
147+
]
148+
},
149+
{
150+
"cell_type": "markdown",
151+
"metadata": {},
152+
"source": [
153+
"### Gives this GIF:\n",
154+
"\n",
155+
"![freq-plot-2.gif](freq-plot-2.gif)"
156+
]
157+
},
158+
{
159+
"cell_type": "code",
160+
"execution_count": null,
161+
"metadata": {},
162+
"outputs": [],
163+
"source": []
164+
}
165+
],
166+
"metadata": {
167+
"kernelspec": {
168+
"display_name": "Python 3",
169+
"language": "python",
170+
"name": "python3"
171+
},
172+
"language_info": {
173+
"codemirror_mode": {
174+
"name": "ipython",
175+
"version": 3
176+
},
177+
"file_extension": ".py",
178+
"mimetype": "text/x-python",
179+
"name": "python",
180+
"nbconvert_exporter": "python",
181+
"pygments_lexer": "ipython3",
182+
"version": "3.9.0"
183+
}
184+
},
185+
"nbformat": 4,
186+
"nbformat_minor": 4
187+
}

examples/devlop/devlop-base.ipynb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"import ipywidgets as widgets\n",
1313
"%load_ext autoreload\n",
1414
"%autoreload 2\n",
15-
"from mpl_interactions import *"
15+
"from mpl_interactions import *\n",
16+
"import mpl_interactions.ipyplot as iplt"
1617
]
1718
},
1819
{
@@ -39,7 +40,7 @@
3940
"name": "python",
4041
"nbconvert_exporter": "python",
4142
"pygments_lexer": "ipython3",
42-
"version": "3.7.8"
43+
"version": "3.9.0"
4344
}
4445
},
4546
"nbformat": 4,

examples/freq-plot-1.gif

2.86 MB
Loading

examples/freq-plot-2.gif

1.02 MB
Loading

mpl_interactions/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
version_info = (0, 13, 1)
1+
version_info = (0, 14, 0)
22
__version__ = ".".join(map(str, version_info))

mpl_interactions/controller.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from functools import partial
1919
from collections.abc import Iterable
2020
from matplotlib.widgets import AxesWidget
21+
from matplotlib.widgets import Slider as mSlider
22+
from matplotlib.animation import FuncAnimation
2123

2224

2325
class Controls:
@@ -181,6 +183,72 @@ def __getitem__(self, key):
181183
key = []
182184
return self, key
183185

186+
def save_animation(
187+
self, filename, fig, param, interval=20, func_anim_kwargs={}, N_frames=None, **kwargs
188+
):
189+
"""
190+
Save an animation over one of the parameters controlled by this `controls` object.
191+
192+
Parameters
193+
----------
194+
filename : str
195+
fig : figure
196+
param : str
197+
the name of the kwarg to use to animate
198+
interval : int, default: 2o
199+
interval between frames in ms
200+
func_anim_kwargs : dict
201+
kwargs to pass the creation of the underlying FuncAnimation
202+
N_frames : int
203+
Only used if the param is a matplotlib slider that was created without a
204+
valstep argument. This will only be relevant if you passed your own matplotlib
205+
slider as a kwarg when plotting. If needed but not given it will default to
206+
a value of 200.
207+
**kwargs :
208+
Passed through to anim.save
209+
210+
Returns
211+
-------
212+
anim : matplotlib.animation.FuncAniation
213+
"""
214+
slider = self.controls[param]
215+
ipywidgets_slider = False
216+
if "Box" in str(slider.__class__):
217+
ipywidgets_slider = True
218+
for obj in slider.children:
219+
if "Slider" in str(obj.__class__):
220+
slider = obj
221+
N = int((slider.max - slider.min) / slider.step)
222+
min_ = slider.min
223+
max_ = slider.max
224+
step = slider.step
225+
elif isinstance(slider, mSlider):
226+
min_ = slider.valmin
227+
max_ = slider.valmax
228+
if slider.valstep is None:
229+
N = N_frames if N_frames else 200
230+
step = (max_ - min_) / N
231+
else:
232+
N = int((max_ - min_) / slider.valstep)
233+
step = slider.valstep
234+
235+
def f(i):
236+
val = min_ + step * i
237+
if ipywidgets_slider:
238+
slider.value = val
239+
else:
240+
slider.set_val(val)
241+
return []
242+
243+
repeat = func_anim_kwargs.pop("repeat", False)
244+
anim = FuncAnimation(fig, f, frames=N, interval=interval, repeat=repeat, **func_anim_kwargs)
245+
# draw then stop necessary to prevent an extra loop after finished saving
246+
# see https://discourse.matplotlib.org/t/how-to-prevent-funcanimation-looping-a-single-time-after-save/21680/2
247+
fig.canvas.draw()
248+
anim.event_source.stop()
249+
anim.save(filename, **kwargs)
250+
return anim
251+
184252
def display(self):
185253
"""
186254
Display the display the ipywidgets controls or show the control figures

0 commit comments

Comments
 (0)