Skip to content

Commit 1deb307

Browse files
authored
Merge pull request #10 from ahuang11/add_gif_paused
Fix paused for GIF and add more example recipes
2 parents ba65bd9 + 0684b3f commit 1deb307

File tree

10 files changed

+353
-5
lines changed

10 files changed

+353
-5
lines changed

docs/best_practices.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,13 @@ No 'max_frames' specified; using the default 50 / 100 frames. Pass `-1` to use a
170170
## 🧩 Use `processes=False` for rendering HoloViews objects
171171

172172
This is done automatically! However, in case there's an edge case, note that the kdims/vdims don't seem to carry over properly to the subprocesses when rendering HoloViews objects. It might complain that it can't find the desired dimensions.
173+
174+
## 📚 Use `threads_per_worker` if flickering
175+
176+
Matplotlib is not always thread-safe, so if you're seeing flickering, set `threads_per_worker=1`.
177+
178+
```python
179+
from streamjoy import stream
180+
181+
stream(..., threads_per_worker=1)
182+
```

docs/example_recipes/air_temperature.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Highlights:
1515
import xarray as xr
1616
import streamjoy.xarray
1717

18-
ds = xr.tutorial.open_dataset("air_temperature")
19-
ds.streamjoy("air_temperature.mp4")
18+
if __name__ == "__main__":
19+
ds = xr.tutorial.open_dataset("air_temperature")
20+
ds.streamjoy("air_temperature.mp4")
2021
```
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# CO2 timeseries
2+
3+
<video controls="true" allowfullscreen="true">
4+
<source src="https://github.com/ahuang11/streamjoy/assets/15331990/1f6fa5ae-9298-452d-ae1c-41d8c9f6cd34" type="video/mp4">
5+
</video>
6+
7+
Shows the yearly CO2 measurements from the Mauna Loa Observatory in Hawaii.
8+
9+
The data is sourced from the [datasets/co2-ppm-daily](https://github.com/datasets/co2-ppm-daily/blob/master/co2-ppm-daily-flow.py).
10+
11+
Highlights:
12+
13+
- Uses `wrap_matplotlib` to automatically handle saving and closing the figure.
14+
- Uses a custom `renderer` function to create each frame of the animation.
15+
- Uses `Paused` to pause the animation at notable dates.
16+
17+
```python
18+
import pandas as pd
19+
import matplotlib.pyplot as plt
20+
from matplotlib.ticker import AutoMinorLocator
21+
from streamjoy import stream, wrap_matplotlib, Paused
22+
23+
URL = "https://raw.githubusercontent.com/datasets/co2-ppm-daily/master/data/co2-ppm-daily.csv"
24+
NOTABLE_YEARS = {
25+
1958: "Mauna Loa measurements begin",
26+
1979: "1st World Climate Conference",
27+
1997: "Kyoto Protocol",
28+
2005: "exceeded 380 ppm",
29+
2010: "exceeded 390 ppm",
30+
2013: "exceeded 400 ppm",
31+
2015: "Paris Agreement",
32+
}
33+
34+
35+
@wrap_matplotlib()
36+
def renderer(df):
37+
plt.style.use("dark_background")
38+
39+
fig, ax = plt.subplots(figsize=(7, 5))
40+
fig.patch.set_facecolor("#1b1e23")
41+
ax.set_facecolor("#1b1e23")
42+
ax.set_frame_on(False)
43+
ax.axis("off")
44+
ax.set_title(
45+
"CO2 Yearly Max",
46+
fontsize=20,
47+
loc="left",
48+
fontname="Courier New",
49+
color="lightgrey",
50+
)
51+
52+
# draw line
53+
df.plot(
54+
y="value",
55+
color="lightgrey", # Line color
56+
legend=False,
57+
ax=ax,
58+
)
59+
60+
# max date
61+
max_date = df["value"].idxmax()
62+
max_co2 = df["value"].max()
63+
ax.text(
64+
0.0,
65+
0.92,
66+
f"{max_co2:.0f} ppm",
67+
va="bottom",
68+
ha="left",
69+
transform=ax.transAxes,
70+
fontsize=25,
71+
color="lightgrey",
72+
)
73+
ax.text(
74+
0.0,
75+
0.91,
76+
f"Peaked in {max_date.year}",
77+
va="top",
78+
ha="left",
79+
transform=ax.transAxes,
80+
fontsize=12,
81+
color="lightgrey",
82+
fontname="Courier New",
83+
)
84+
85+
# draw end point
86+
date = df.index[-1]
87+
co2 = df["value"].values[-1]
88+
diff = df["diff"].fillna(0).values[-1]
89+
diff = f"+{diff:.0f}" if diff >= 0 else f"{diff:.0f}"
90+
ax.scatter(date, co2, color="red", zorder=999)
91+
ax.annotate(
92+
f"{diff} ppm",
93+
(date, co2),
94+
textcoords="offset points",
95+
xytext=(-10, 5),
96+
fontsize=12,
97+
ha="right",
98+
va="bottom",
99+
color="lightgrey",
100+
)
101+
102+
# draw source label
103+
ax.text(
104+
0.0,
105+
0.03,
106+
f"Source: {URL}",
107+
va="bottom",
108+
ha="left",
109+
transform=ax.transAxes,
110+
fontsize=8,
111+
color="lightgrey",
112+
)
113+
114+
# properly tighten layout
115+
plt.subplots_adjust(bottom=0, top=0.9, right=0.9, left=0.05)
116+
117+
# pause at notable years
118+
year = date.year
119+
if year in NOTABLE_YEARS:
120+
ax.annotate(
121+
f"{NOTABLE_YEARS[year]} - {year}",
122+
(date, co2),
123+
textcoords="offset points",
124+
xytext=(-10, 3),
125+
fontsize=10.5,
126+
ha="right",
127+
va="top",
128+
color="lightgrey",
129+
fontname="Courier New",
130+
)
131+
return Paused(ax, 2.8)
132+
else:
133+
ax.annotate(
134+
year,
135+
(date, co2),
136+
textcoords="offset points",
137+
xytext=(-10, 3),
138+
fontsize=10.5,
139+
ha="right",
140+
va="top",
141+
color="lightgrey",
142+
fontname="Courier New",
143+
)
144+
return ax
145+
146+
147+
if __name__ == "__main__":
148+
df = (
149+
pd.read_csv(URL, parse_dates=True, index_col="date")
150+
.resample("1YE")
151+
.max()
152+
.interpolate()
153+
.assign(
154+
diff=lambda df: df["value"].diff(),
155+
)
156+
)
157+
stream(df, renderer=renderer, max_frames=-1, threads_per_worker=1).write("co2_emissions.mp4")
158+
```
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Temperature anomaly
2+
3+
<video controls="true" allowfullscreen="true">
4+
<source src="https://github.com/ahuang11/streamjoy/assets/15331990/069b1826-de92-4643-8be5-6d5a5301d11e" type="video/mp4">
5+
</video>
6+
7+
Shows the global temperature anomaly from 1995 to 2024 using the HadCRUT5 dataset. The video pauses at notable dates.
8+
9+
Highlights:
10+
11+
- Uses `wrap_matplotlib` to automatically handle saving and closing the figure.
12+
- Uses a custom `renderer` function to create each frame of the animation.
13+
- Uses `Paused` to pause the animation at notable dates.
14+
15+
```python
16+
import pandas as pd
17+
import matplotlib.pyplot as plt
18+
from streamjoy import stream, wrap_matplotlib, Paused
19+
20+
URL = "https://climexp.knmi.nl/data/ihadcrut5_global.dat"
21+
NOTABLE_DATES = {
22+
"1997-12": "Kyoto Protocol adopted",
23+
"2005-01": "Exceeded 380 ppm",
24+
"2010-01": "Exceeded 390 ppm",
25+
"2013-05": "Exceeded 400 ppm",
26+
"2015-12": "Paris Agreement signed",
27+
"2016-01": "CO2 permanently over 400 ppm",
28+
}
29+
30+
31+
@wrap_matplotlib()
32+
def renderer(df):
33+
plt.style.use("dark_background") # Setting the style for dark mode
34+
35+
fig, ax = plt.subplots()
36+
fig.patch.set_facecolor("#1b1e23")
37+
ax.set_facecolor("#1b1e23")
38+
ax.set_frame_on(False)
39+
ax.axis("off")
40+
41+
# Set title
42+
year = df["year"].iloc[-1]
43+
ax.set_title(
44+
f"Global Temperature Anomaly {year} [HadCRUT5]",
45+
fontsize=15,
46+
loc="left",
47+
fontname="Courier New",
48+
color="lightgrey",
49+
)
50+
51+
# draw line
52+
df.groupby("year")["anom"].plot(
53+
y="anom", color="lightgrey", legend=False, ax=ax, lw=0.5
54+
)
55+
56+
# add source text at bottom right
57+
ax.text(
58+
0.01,
59+
0.05,
60+
f"Source: {URL}",
61+
va="bottom",
62+
ha="left",
63+
transform=ax.transAxes,
64+
fontsize=8,
65+
color="lightgrey",
66+
fontname="Courier New",
67+
)
68+
69+
# draw end point
70+
jday = df.index.values[-1]
71+
anom = df["anom"].values[-1]
72+
ax.scatter(jday, anom, color="red", zorder=999)
73+
anom_label = f"+{anom:.1f} K" if anom > 0 else f"{anom:.1f} K"
74+
ax.annotate(
75+
anom_label,
76+
(jday, anom),
77+
textcoords="offset points",
78+
xytext=(-10, 5),
79+
fontsize=12,
80+
ha="right",
81+
va="bottom",
82+
color="lightgrey",
83+
)
84+
85+
# draw yearly labels
86+
for year, df_year in df.reset_index().groupby("year").last().iloc[-5:].iterrows():
87+
if df_year["month"] != 12:
88+
continue
89+
ax.annotate(
90+
year,
91+
(df_year["jday"], df_year["anom"]),
92+
fontsize=12,
93+
ha="left",
94+
va="center",
95+
color="lightgrey",
96+
fontname="Courier New",
97+
)
98+
99+
plt.subplots_adjust(bottom=0, top=0.9, left=0.05)
100+
101+
month = df["date"].iloc[-1].strftime("%b")
102+
ax.annotate(
103+
month,
104+
(jday, anom),
105+
textcoords="offset points",
106+
xytext=(-10, 3),
107+
fontsize=12,
108+
ha="right",
109+
va="top",
110+
color="lightgrey",
111+
fontname="Courier New",
112+
)
113+
date_string = df["date"].iloc[-1].strftime("%Y-%m")
114+
if date_string in NOTABLE_DATES:
115+
ax.annotate(
116+
f"{NOTABLE_DATES[date_string]}",
117+
xy=(0, 1),
118+
xycoords="axes fraction",
119+
xytext=(0, -5),
120+
textcoords="offset points",
121+
fontsize=12,
122+
ha="left",
123+
va="top",
124+
color="lightgrey",
125+
fontname="Courier New",
126+
)
127+
return Paused(fig, 3)
128+
return fig
129+
130+
131+
df = (
132+
pd.read_csv(
133+
URL,
134+
comment="#",
135+
header=None,
136+
sep="\s+",
137+
na_values=[-999.9],
138+
)
139+
.rename(columns={0: "year"})
140+
.melt(id_vars="year", var_name="month", value_name="anom")
141+
)
142+
df.index = pd.to_datetime(
143+
df["year"].astype(str) + df["month"].astype(str), format="%Y%m"
144+
)
145+
df = df.sort_index()["1995":"2024"]
146+
df["jday"] = df.index.dayofyear
147+
df = df.rename_axis("date").reset_index().set_index("jday")
148+
df_list = [df[:i] for i in range(1, len(df) + 1)]
149+
150+
stream(df_list, renderer=renderer, threads_per_worker=1).write(
151+
"temperature_anomaly.mp4"
152+
)
153+
```

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ nav:
6767
- Example recipes:
6868
- Air temperature: example_recipes/air_temperature.md
6969
- Sine wave: example_recipes/sine_wave.md
70+
- CO2 timeseries: example_recipes/co2_timeseries.md
71+
- Temperature anomaly: example_recipes/temperature_anomaly.md
7072
- Sea ice: example_recipes/sea_ice.md
7173
- OISST globe: example_recipes/oisst_globe.md
7274
- Gender gapminder: example_recipes/gender_gapminder.md

streamjoy/_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,5 +296,5 @@ def imread_with_pause(
296296
) -> np.ndarray | Paused:
297297
imread_kwargs = dict(extension=extension, plugin=plugin)
298298
if isinstance(uri, Paused):
299-
return Paused(iio.imread(uri.output, **imread_kwargs), uri.seconds).squeeze()
299+
return Paused(iio.imread(uri.output, **imread_kwargs).squeeze(), uri.seconds)
300300
return iio.imread(uri, **imread_kwargs).squeeze()

streamjoy/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Paused(param.Parameterized):
1717

1818
output = param.Parameter(doc="The output to pause for.")
1919

20-
seconds = param.Integer(doc="The number of seconds to pause for.")
20+
seconds = param.Number(doc="The number of seconds to pause for.")
2121

2222
def __init__(self, output: Any, seconds: int, **params):
2323
self.output = output

streamjoy/streams.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,8 +1249,11 @@ def _write_images(
12491249

12501250
self._prepend_intro(buf, intro_frame, **write_kwargs)
12511251

1252-
for image in images:
1252+
for i, image in enumerate(images):
12531253
image = _utils.get_result(image)
1254+
if isinstance(image, Paused):
1255+
duration[i] = image.seconds * 1000
1256+
image = image.output
12541257
buf.write(image[:, :, :3], **write_kwargs)
12551258
del image
12561259

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def client():
4242

4343
@pytest.fixture(autouse=True, scope="session")
4444
def default_config():
45+
config["fps"] = 1
4546
config["max_frames"] = 3
4647
config["max_files"] = 2
4748
config["ending_pause"] = 0

0 commit comments

Comments
 (0)