Skip to content

Commit

Permalink
Add command line usage + some typing (#11)
Browse files Browse the repository at this point in the history
* Add command line usage

* Remove lint action

redundant with pre-commit
  • Loading branch information
ianhi authored Jul 18, 2022
1 parent 010595f commit f855dc8
Show file tree
Hide file tree
Showing 5 changed files with 2,669 additions and 38 deletions.
13 changes: 0 additions & 13 deletions .github/workflows/lint.yml

This file was deleted.

22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,26 @@ See https://mpl-playback.readthedocs.io/en/latest/gallery/index.html for an exam

Directly inspired by https://github.com/matplotlib/matplotlib/issues/19222

# Q: Should you use this?
A: Probably not. I mainly made this so that I could more easily test widget interactions https://github.com/ianhi/mpl-interactions
## Command Line Usage

**recording interactions**
To record a json file for later playback:
```bash
python -m mpl_playback.record example_file.py -figures fig --output example_playback.json
```

This will launch example_file.py and record any interactions with the object named `fig`. Then it will be saved to `example_playback.json`. However, the output argument is optional, if not given then the name will be `example_file-playback.json`

**playback interactions in a gif**
To play back the file you must pass both the original python file and the recording json file. You can optionally pass names for the output gif(s) with the `--output` argument, or allow the names to be chosen automatically. 1 gif will be created for each figure that was recorded.

For one off gifs of interactions it's almost certainly easier to just record your screen to make a gif.
```bash
python -m mpl_playback.playback example_file.py example_playback.json
```


# Q: Should you use this?
A: Depends on what you want. For one off gifs of interactions it's almost certainly easier to just record your screen to make a gif. But if you want integration with `sphinx-gallery` then this is currently the only option.

### Example of a rendered gif:

Expand Down
2,590 changes: 2,589 additions & 1 deletion examples/_multifig-playback.json

Large diffs are not rendered by default.

64 changes: 44 additions & 20 deletions mpl_playback/playback.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from __future__ import annotations

import json
from pathlib import Path
from typing import Literal
from unittest import mock

import matplotlib
Expand Down Expand Up @@ -71,38 +75,37 @@ def gen_mock_events(events, globals, accessors):
return np.array(times), np.array(mock_events)


def load_events(events):
def load_events(events: Path | str) -> tuple[dict, dict]:
meta = {}
if isinstance(events, str):
with open(events) as f:
loaded = json.load(f)
meta["figures"] = loaded["figures"]
meta["schema-version"] = loaded["schema-version"]
events = loaded["events"]
with open(events) as f:
loaded = json.load(f)
meta["figures"] = loaded["figures"]
meta["schema-version"] = loaded["schema-version"]
events = loaded["events"]
return meta, events


def playback_file(
events,
path,
outputs,
events: Path | str,
path: Path | str,
outputs: str | list[str] | None,
fps=24,
from_first_event=True,
prog_bar=True,
writer="ffmpeg-pillow",
writer: Literal["pillow", "ffmpeg", "imagemagick", "avconv", None] = "pillow",
**kwargs,
):
"""
Parameters
----------
events : str
events : pathlike
Path to the json file defining the events or a dictionary of an
already loaded file.
path : str
path : pathlike
path to the file to be executed.
outputs : str, list of str, or None
The path(s) to the output file(s). If None then the
events will played back but no outputs will saved.
The path(s) to the output file(s). If None then output names will
be automatically generated.
fps : int, default: 24
Frames per second of the output
from_first_event : bool, default: True
Expand All @@ -111,14 +114,20 @@ def playback_file(
prog_bar : bool, default: True
Whether to display a progress bar. If tqdm is not
available then this kwarg has no effect.
writer : str, default: 'ffmpeg-pillow'
writer : str, default: 'pillow'
which writer to use. options 'ffmpeg', 'imagemagick', 'avconv', 'pillow'.
If the chosen writer is not available pillow will be used as a fallback.
"""
if isinstance(outputs, str):
outputs = [outputs]
meta, events = load_events(events)
figures = meta["figures"]
if isinstance(outputs, str):
outputs = [outputs]
elif outputs is None:
outputs = []
path = Path(path)
output_base = str(path.name[: -len(path.suffix)])
for i in range(len(figures)):
outputs.append(output_base + f"_{i}.gif")
gbl = exec_no_show(path)
playback_events(
figures,
Expand All @@ -143,7 +152,7 @@ def playback_events(
fps=24,
from_first_event=True,
prog_bar=True,
writer="ffmpeg-pillow",
writer: Literal["pillow", "ffmpeg", "imagemagick", "avconv", None] = "pillow",
**kwargs,
):
"""
Expand Down Expand Up @@ -203,7 +212,7 @@ def playback_events(
times, mock_events = gen_mock_events(events, globals, accessors)
if from_first_event:
times -= times[0]
N_frames = np.int(times[-1] * fps)
N_frames = int(times[-1] * fps)
# map from frames to events
event_frames = np.round(times * fps)

Expand Down Expand Up @@ -250,3 +259,18 @@ def animate(i):
if outputs is not None:
for w in writers:
w.finish()


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("py", type=str, nargs=1)
parser.add_argument("json", type=str, nargs=1)
parser.add_argument("-fps", type=int, default=24)
parser.add_argument("-o", "--output", type=str)
parser.add_argument("--writer", type=str, default="ffmpeg-pillow")
args = parser.parse_args()
playback_file(
args.json[0], args.py[0], args.output, fps=args.fps, writer=args.writer
)
18 changes: 17 additions & 1 deletion mpl_playback/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"record_events",
"record_file",
"record_figure",
"RECORDED_EVENTS",
]
possible_events = [
"button_press_event",
Expand All @@ -32,6 +33,10 @@

event_list = []

RECORDED_EVENTS = (
["motion_notify_event", "button_press_event", "button_release_event"],
)


def _find_obj(names, objs, obj, accessors):
"""
Expand Down Expand Up @@ -124,7 +129,7 @@ def record_figures(figures, globals, savename, accessors=None):
accessors[_fig] = fig
record_events(
_fig,
["motion_notify_event", "button_press_event", "button_release_event"],
RECORDED_EVENTS,
globals,
accessors,
)
Expand Down Expand Up @@ -160,3 +165,14 @@ def record_events(fig, events, globals, accessors=None):
start_time=start_time,
),
)


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("file", type=str, nargs=1)
parser.add_argument("-figs", "--figures", nargs="+", default=["fig"])
parser.add_argument("-o", "--output", type=str)
args = parser.parse_args()
record_file(args.file[0], args.figures, args.output)

0 comments on commit f855dc8

Please sign in to comment.