Skip to content

Commit

Permalink
Merge pull request #10 from ahuang11/add_gif_paused
Browse files Browse the repository at this point in the history
Fix paused for GIF and add more example recipes
  • Loading branch information
ahuang11 authored Mar 24, 2024
2 parents ba65bd9 + 0684b3f commit 1deb307
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 5 deletions.
10 changes: 10 additions & 0 deletions docs/best_practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,13 @@ No 'max_frames' specified; using the default 50 / 100 frames. Pass `-1` to use a
## 🧩 Use `processes=False` for rendering HoloViews objects

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.

## 📚 Use `threads_per_worker` if flickering

Matplotlib is not always thread-safe, so if you're seeing flickering, set `threads_per_worker=1`.

```python
from streamjoy import stream

stream(..., threads_per_worker=1)
```
5 changes: 3 additions & 2 deletions docs/example_recipes/air_temperature.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Highlights:
import xarray as xr
import streamjoy.xarray

ds = xr.tutorial.open_dataset("air_temperature")
ds.streamjoy("air_temperature.mp4")
if __name__ == "__main__":
ds = xr.tutorial.open_dataset("air_temperature")
ds.streamjoy("air_temperature.mp4")
```
158 changes: 158 additions & 0 deletions docs/example_recipes/co2_timeseries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# CO2 timeseries

<video controls="true" allowfullscreen="true">
<source src="https://github.com/ahuang11/streamjoy/assets/15331990/1f6fa5ae-9298-452d-ae1c-41d8c9f6cd34" type="video/mp4">
</video>

Shows the yearly CO2 measurements from the Mauna Loa Observatory in Hawaii.

The data is sourced from the [datasets/co2-ppm-daily](https://github.com/datasets/co2-ppm-daily/blob/master/co2-ppm-daily-flow.py).

Highlights:

- Uses `wrap_matplotlib` to automatically handle saving and closing the figure.
- Uses a custom `renderer` function to create each frame of the animation.
- Uses `Paused` to pause the animation at notable dates.

```python
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator
from streamjoy import stream, wrap_matplotlib, Paused

URL = "https://raw.githubusercontent.com/datasets/co2-ppm-daily/master/data/co2-ppm-daily.csv"
NOTABLE_YEARS = {
1958: "Mauna Loa measurements begin",
1979: "1st World Climate Conference",
1997: "Kyoto Protocol",
2005: "exceeded 380 ppm",
2010: "exceeded 390 ppm",
2013: "exceeded 400 ppm",
2015: "Paris Agreement",
}


@wrap_matplotlib()
def renderer(df):
plt.style.use("dark_background")

fig, ax = plt.subplots(figsize=(7, 5))
fig.patch.set_facecolor("#1b1e23")
ax.set_facecolor("#1b1e23")
ax.set_frame_on(False)
ax.axis("off")
ax.set_title(
"CO2 Yearly Max",
fontsize=20,
loc="left",
fontname="Courier New",
color="lightgrey",
)

# draw line
df.plot(
y="value",
color="lightgrey", # Line color
legend=False,
ax=ax,
)

# max date
max_date = df["value"].idxmax()
max_co2 = df["value"].max()
ax.text(
0.0,
0.92,
f"{max_co2:.0f} ppm",
va="bottom",
ha="left",
transform=ax.transAxes,
fontsize=25,
color="lightgrey",
)
ax.text(
0.0,
0.91,
f"Peaked in {max_date.year}",
va="top",
ha="left",
transform=ax.transAxes,
fontsize=12,
color="lightgrey",
fontname="Courier New",
)

# draw end point
date = df.index[-1]
co2 = df["value"].values[-1]
diff = df["diff"].fillna(0).values[-1]
diff = f"+{diff:.0f}" if diff >= 0 else f"{diff:.0f}"
ax.scatter(date, co2, color="red", zorder=999)
ax.annotate(
f"{diff} ppm",
(date, co2),
textcoords="offset points",
xytext=(-10, 5),
fontsize=12,
ha="right",
va="bottom",
color="lightgrey",
)

# draw source label
ax.text(
0.0,
0.03,
f"Source: {URL}",
va="bottom",
ha="left",
transform=ax.transAxes,
fontsize=8,
color="lightgrey",
)

# properly tighten layout
plt.subplots_adjust(bottom=0, top=0.9, right=0.9, left=0.05)

# pause at notable years
year = date.year
if year in NOTABLE_YEARS:
ax.annotate(
f"{NOTABLE_YEARS[year]} - {year}",
(date, co2),
textcoords="offset points",
xytext=(-10, 3),
fontsize=10.5,
ha="right",
va="top",
color="lightgrey",
fontname="Courier New",
)
return Paused(ax, 2.8)
else:
ax.annotate(
year,
(date, co2),
textcoords="offset points",
xytext=(-10, 3),
fontsize=10.5,
ha="right",
va="top",
color="lightgrey",
fontname="Courier New",
)
return ax


if __name__ == "__main__":
df = (
pd.read_csv(URL, parse_dates=True, index_col="date")
.resample("1YE")
.max()
.interpolate()
.assign(
diff=lambda df: df["value"].diff(),
)
)
stream(df, renderer=renderer, max_frames=-1, threads_per_worker=1).write("co2_emissions.mp4")
```
153 changes: 153 additions & 0 deletions docs/example_recipes/temperature_anomaly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Temperature anomaly

<video controls="true" allowfullscreen="true">
<source src="https://github.com/ahuang11/streamjoy/assets/15331990/069b1826-de92-4643-8be5-6d5a5301d11e" type="video/mp4">
</video>

Shows the global temperature anomaly from 1995 to 2024 using the HadCRUT5 dataset. The video pauses at notable dates.

Highlights:

- Uses `wrap_matplotlib` to automatically handle saving and closing the figure.
- Uses a custom `renderer` function to create each frame of the animation.
- Uses `Paused` to pause the animation at notable dates.

```python
import pandas as pd
import matplotlib.pyplot as plt
from streamjoy import stream, wrap_matplotlib, Paused

URL = "https://climexp.knmi.nl/data/ihadcrut5_global.dat"
NOTABLE_DATES = {
"1997-12": "Kyoto Protocol adopted",
"2005-01": "Exceeded 380 ppm",
"2010-01": "Exceeded 390 ppm",
"2013-05": "Exceeded 400 ppm",
"2015-12": "Paris Agreement signed",
"2016-01": "CO2 permanently over 400 ppm",
}


@wrap_matplotlib()
def renderer(df):
plt.style.use("dark_background") # Setting the style for dark mode

fig, ax = plt.subplots()
fig.patch.set_facecolor("#1b1e23")
ax.set_facecolor("#1b1e23")
ax.set_frame_on(False)
ax.axis("off")

# Set title
year = df["year"].iloc[-1]
ax.set_title(
f"Global Temperature Anomaly {year} [HadCRUT5]",
fontsize=15,
loc="left",
fontname="Courier New",
color="lightgrey",
)

# draw line
df.groupby("year")["anom"].plot(
y="anom", color="lightgrey", legend=False, ax=ax, lw=0.5
)

# add source text at bottom right
ax.text(
0.01,
0.05,
f"Source: {URL}",
va="bottom",
ha="left",
transform=ax.transAxes,
fontsize=8,
color="lightgrey",
fontname="Courier New",
)

# draw end point
jday = df.index.values[-1]
anom = df["anom"].values[-1]
ax.scatter(jday, anom, color="red", zorder=999)
anom_label = f"+{anom:.1f} K" if anom > 0 else f"{anom:.1f} K"
ax.annotate(
anom_label,
(jday, anom),
textcoords="offset points",
xytext=(-10, 5),
fontsize=12,
ha="right",
va="bottom",
color="lightgrey",
)

# draw yearly labels
for year, df_year in df.reset_index().groupby("year").last().iloc[-5:].iterrows():
if df_year["month"] != 12:
continue
ax.annotate(
year,
(df_year["jday"], df_year["anom"]),
fontsize=12,
ha="left",
va="center",
color="lightgrey",
fontname="Courier New",
)

plt.subplots_adjust(bottom=0, top=0.9, left=0.05)

month = df["date"].iloc[-1].strftime("%b")
ax.annotate(
month,
(jday, anom),
textcoords="offset points",
xytext=(-10, 3),
fontsize=12,
ha="right",
va="top",
color="lightgrey",
fontname="Courier New",
)
date_string = df["date"].iloc[-1].strftime("%Y-%m")
if date_string in NOTABLE_DATES:
ax.annotate(
f"{NOTABLE_DATES[date_string]}",
xy=(0, 1),
xycoords="axes fraction",
xytext=(0, -5),
textcoords="offset points",
fontsize=12,
ha="left",
va="top",
color="lightgrey",
fontname="Courier New",
)
return Paused(fig, 3)
return fig


df = (
pd.read_csv(
URL,
comment="#",
header=None,
sep="\s+",
na_values=[-999.9],
)
.rename(columns={0: "year"})
.melt(id_vars="year", var_name="month", value_name="anom")
)
df.index = pd.to_datetime(
df["year"].astype(str) + df["month"].astype(str), format="%Y%m"
)
df = df.sort_index()["1995":"2024"]
df["jday"] = df.index.dayofyear
df = df.rename_axis("date").reset_index().set_index("jday")
df_list = [df[:i] for i in range(1, len(df) + 1)]

stream(df_list, renderer=renderer, threads_per_worker=1).write(
"temperature_anomaly.mp4"
)
```
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ nav:
- Example recipes:
- Air temperature: example_recipes/air_temperature.md
- Sine wave: example_recipes/sine_wave.md
- CO2 timeseries: example_recipes/co2_timeseries.md
- Temperature anomaly: example_recipes/temperature_anomaly.md
- Sea ice: example_recipes/sea_ice.md
- OISST globe: example_recipes/oisst_globe.md
- Gender gapminder: example_recipes/gender_gapminder.md
Expand Down
2 changes: 1 addition & 1 deletion streamjoy/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,5 +296,5 @@ def imread_with_pause(
) -> np.ndarray | Paused:
imread_kwargs = dict(extension=extension, plugin=plugin)
if isinstance(uri, Paused):
return Paused(iio.imread(uri.output, **imread_kwargs), uri.seconds).squeeze()
return Paused(iio.imread(uri.output, **imread_kwargs).squeeze(), uri.seconds)
return iio.imread(uri, **imread_kwargs).squeeze()
2 changes: 1 addition & 1 deletion streamjoy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Paused(param.Parameterized):

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

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

def __init__(self, output: Any, seconds: int, **params):
self.output = output
Expand Down
5 changes: 4 additions & 1 deletion streamjoy/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -1249,8 +1249,11 @@ def _write_images(

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

for image in images:
for i, image in enumerate(images):
image = _utils.get_result(image)
if isinstance(image, Paused):
duration[i] = image.seconds * 1000
image = image.output
buf.write(image[:, :, :3], **write_kwargs)
del image

Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def client():

@pytest.fixture(autouse=True, scope="session")
def default_config():
config["fps"] = 1
config["max_frames"] = 3
config["max_files"] = 2
config["ending_pause"] = 0
Expand Down
Loading

0 comments on commit 1deb307

Please sign in to comment.