Skip to content

Commit 0d855df

Browse files
committed
added decorator for dynamic plots
1 parent 8565780 commit 0d855df

File tree

5 files changed

+134
-15
lines changed

5 files changed

+134
-15
lines changed
126 KB
Loading

docs/tutorial/plotters.rst

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,66 @@ Vectors
290290
Dynamic Plots
291291
=============
292292

293+
Dynamic plots, or animations, can be made with the "on" decorator :meth:`compas_plotters.Plotter.on`.
294+
Simply add the decorator to a callback functions that updates the geometry in the plot at a specified interval.
295+
296+
.. code-block:: python
297+
298+
@plotter.on(interval=0.1, frames=50)
299+
def move(frame):
300+
for a, b in pairwise(pointcloud):
301+
vector = b - a
302+
a.transform(Translation.from_vector(vector * 0.1))
303+
304+
For example, the following will update the locations of the points of a pointcloud
305+
for 50 frames and with an interval of 0.1 seconds between the frames.
306+
307+
.. code-block:: python
308+
309+
from compas.geometry import Pointcloud, Translation
310+
from compas.utilities import i_to_red, pairwise
311+
312+
from compas_plotters import Plotter
313+
314+
plotter = Plotter(figsize=(8, 5))
315+
316+
pointcloud = Pointcloud.from_bounds(8, 5, 0, 10)
317+
318+
for index, (a, b) in enumerate(pairwise(pointcloud)):
319+
artist = plotter.add(a, edgecolor=i_to_red(max(index / 10, 0.1), normalize=True))
320+
321+
plotter.add(b, size=10, edgecolor=(1, 0, 0))
322+
plotter.zoom_extents()
323+
plotter.pause(1.0)
324+
325+
@plotter.on(interval=0.1, frames=50)
326+
def move(frame):
327+
for a, b in pairwise(pointcloud):
328+
vector = b - a
329+
a.transform(Translation.from_vector(vector * 0.1))
330+
331+
If you want to keep the plot alive at the end of the animation, add a call to ``show``.
332+
333+
.. code-block:: python
334+
335+
plotter.show()
336+
337+
To save the animation to an animated gif, set the ``record`` flag to ``True``, and add a ``recording`` path.
338+
339+
.. code-block:: python
340+
341+
@plotter.on(interval=0.1, frames=50, record=True, recording='docs/_images/tutorial/plotters_dynamic.gif')
342+
def move(frame):
343+
for a, b in pairwise(pointcloud):
344+
vector = b - a
345+
a.transform(Translation.from_vector(vector * 0.1))
346+
347+
.. figure:: /_images/tutorial/plotters_dynamic.gif
348+
:figclass: figure
349+
:class: figure-img img-fluid
350+
351+
293352
Interactive Plots
294353
=================
295354

296-
Exports
297-
=======
355+
*Coming soon*.

docs/tutorial/plotters_dynamic.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from compas.geometry import Pointcloud, Translation
2+
from compas.utilities import i_to_red, pairwise
3+
4+
from compas_plotters import Plotter
5+
6+
plotter = Plotter(figsize=(8, 5))
7+
8+
pointcloud = Pointcloud.from_bounds(8, 5, 0, 10)
9+
10+
for index, (a, b) in enumerate(pairwise(pointcloud)):
11+
artist = plotter.add(a, edgecolor=i_to_red(max(index / 10, 0.1), normalize=True))
12+
13+
plotter.add(b, size=10, edgecolor=(1, 0, 0))
14+
plotter.zoom_extents()
15+
plotter.pause(1.0)
16+
17+
18+
@plotter.on(interval=0.1, frames=50, record=True, recording='docs/_images/tutorial/plotters_dynamic.gif', dpi=150)
19+
def move(frame):
20+
print(frame)
21+
for a, b in pairwise(pointcloud):
22+
vector = b - a
23+
a.transform(Translation.from_vector(vector * 0.1))
24+
25+
26+
plotter.show()

docs/tutorial/plotters_line-options.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,17 @@
55

66
from compas_plotters import Plotter
77

8-
pointcloud = Pointcloud.from_bounds(8, 5, 0, 10)
9-
10-
118
plotter = Plotter(figsize=(8, 5))
129

10+
pointcloud = Pointcloud.from_bounds(8, 5, 0, 10)
11+
1312
for a, b in grouper(pointcloud, 2):
14-
if a and b:
15-
line = Line(a, b)
16-
plotter.add(line,
17-
linewidth=2.0,
18-
linestyle=random.choice(['dotted', 'dashed', 'solid']),
19-
color=i_to_rgb(random.random(), normalize=True),
20-
draw_points=True)
13+
line = Line(a, b)
14+
plotter.add(line,
15+
linewidth=2.0,
16+
linestyle=random.choice(['dotted', 'dashed', 'solid']),
17+
color=i_to_rgb(random.random(), normalize=True),
18+
draw_points=True)
2119

2220
plotter.zoom_extents()
2321
plotter.save('docs/_images/tutorial/plotters_line-options.png', dpi=300)

src/compas_plotters/plotter.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import os
12
from typing import Callable, Optional, Tuple, List, Union
23
import matplotlib
34
import matplotlib.pyplot as plt
5+
import tempfile
6+
from PIL import Image
47

58
import compas
69
from compas_plotters import Artist
@@ -202,8 +205,9 @@ def pause(self, pause: float) -> None:
202205
if pause:
203206
plt.pause(pause)
204207

205-
def zoom_extents(self) -> None:
208+
def zoom_extents(self, padding: Optional[int] = None) -> None:
206209
"""Zoom the view to the bounding box of all objects."""
210+
padding = padding or 0
207211
width, height = self.figsize
208212
fig_aspect = width / height
209213
data = []
@@ -214,8 +218,8 @@ def zoom_extents(self) -> None:
214218
xmax = max(x)
215219
ymin = min(y)
216220
ymax = max(y)
217-
xspan = xmax - xmin
218-
yspan = ymax - ymin
221+
xspan = xmax - xmin + padding
222+
yspan = ymax - ymin + padding
219223
data_aspect = xspan / yspan
220224
if data_aspect < fig_aspect:
221225
scale = fig_aspect / data_aspect
@@ -359,3 +363,36 @@ def save(self, filepath: str, **kwargs) -> None:
359363
360364
"""
361365
plt.savefig(filepath, **kwargs)
366+
367+
def on(self,
368+
interval: int = None,
369+
frames: int = None,
370+
record: bool = False,
371+
recording: str = None,
372+
dpi: int = 150) -> Callable:
373+
"""Method for decorating callback functions in dynamic plots."""
374+
if record:
375+
if not recording:
376+
raise Exception('Please provide a path for the recording.')
377+
378+
def outer(func: Callable):
379+
if record:
380+
with tempfile.TemporaryDirectory() as dirpath:
381+
paths = []
382+
for f in range(frames):
383+
func(f)
384+
self.redraw(pause=interval)
385+
if record:
386+
filepath = os.path.join(dirpath, f'frame-{f}.png')
387+
paths.append(filepath)
388+
self.save(filepath, dpi=dpi)
389+
images = []
390+
for path in paths:
391+
images.append(Image.open(path))
392+
images[0].save(recording, save_all=True, append_images=images[1:], optimize=False, duration=interval * 1000, loop=0)
393+
else:
394+
for f in range(frames):
395+
func(f)
396+
self.redraw(pause=interval)
397+
398+
return outer

0 commit comments

Comments
 (0)