Skip to content

Commit 265f0f6

Browse files
authored
Andrew/old api additions (#255)
* Split out function to calc image bits * Allow force_cdn when calculating page * Add kaleido.calc_fig * Add instructions for refactoring * Add __init__ wrapper for calc_fig * Add calc_fig to __all__ * Add shortcut functions * Allow force_cdn when calculating page * Cordon off broken tests * Add separate name for test_pypi * Rework temp logic for isolation workaround * Upgrade choreographer for is_isolated support * Add changelog * Remove erroneous argument from wrapper func * Check for profile before setting state * Add examples to mkdocs * Add more documents * Build docs * Update lock file
1 parent cb87437 commit 265f0f6

21 files changed

+1425
-287
lines changed

src/py/CHANGELOG.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
v1.0.0rc8
2+
- Add kaleido.calc_fig to return bytes
3+
- Add calc_fig[_sync], write_fig_sync, and write_fig_from_object_sync to kaleido
14
v1.0.0rc7
25
- Use new choero is_isolated() to improve platform support
36
v1.0.0rc6

src/py/REFACTOR_INSTRUCTIONS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
We are just at the line of techdebt:
2+
3+
How to refactor:
4+
5+
6+
1. We need more thoughtful design of how to manage parallel tasking with async/await
7+
a. We are beginning to look a bit like callback hell.
8+
2. The above would probably accompany a switch from inheritance of choreo to composition
9+
3. error_log and profile need to be full fledged classes.

src/py/docs/examples.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Kaleido Code Snippets
2+
3+
4+
### Basic
5+
6+
```python
7+
import plotly.express as px
8+
import kaleido
9+
10+
### SAMPLE DATA ###
11+
12+
fig = px.scatter(
13+
px.data.iris(),
14+
x="sepal_length",
15+
y="sepal_width",
16+
color="species"
17+
)
18+
19+
fig2 = px.line(
20+
px.data.gapminder().query("country=='Canada'"),
21+
x="year",
22+
y="lifeExp",
23+
title='Life expectancy in Canada'
24+
)
25+
26+
figures = [fig, fig2]
27+
28+
### WRITE FIGURES ###
29+
30+
## Simple one image synchronous write
31+
32+
kaleido.write_fig_sync(fig, path="./output/")
33+
34+
35+
## Multiple image write with error collection
36+
37+
error_log = []
38+
39+
kaleido.write_fig_sync(
40+
figures,
41+
path="./output/",
42+
opts={"format":"jpg"},
43+
error_log = error_log
44+
)
45+
46+
# Dump the error_log
47+
48+
if error_log:
49+
for e in error_log:
50+
print(str(e))
51+
raise RuntimeError("{len(error_log)} images failed.")
52+
53+
54+
## async/await style of above
55+
56+
await kaleido.write_fig(
57+
figures,
58+
path="./output/",
59+
opts={"format":"jpg"},
60+
error_log = error_log
61+
)
62+
```
63+
64+
### Generator (for batch processing)
65+
66+
Generating all of the plotly figures can take too much memory depending on the
67+
number of figures, so use a generator:
68+
69+
70+
```python
71+
import plotly.express as px
72+
import kaleido
73+
74+
### Make a figure generator
75+
76+
def generate_figures(): # can be async as well
77+
data = px.data.gapminder()
78+
for country in data["country"].unique(): # list all countries in dataset
79+
# yield unique plot for each country
80+
yield px.line(
81+
data.query(f'country=="{country}"'),
82+
x="year",
83+
y="lifeExp",
84+
title=f"Life expectancy in {country}"
85+
)
86+
87+
# four processors
88+
kaleido.write_fig_sync(generate_figures(), path="./output/", n=4)
89+
# file names will be taken from figure title
90+
91+
92+
### If you need more control, use an object
93+
94+
def generate_figure_objects():
95+
data = px.data.gapminder()
96+
for country in data["country"].unique(): # list all countries in dataset
97+
fig = px.line(
98+
data.query(f'country=="{country}"'),
99+
x="year",
100+
y="lifeExp",
101+
title=f"Life expectancy in {country}"
102+
)
103+
yield {"fig": fig, "path": f"./output/{country}.jpg"}
104+
# customize file name
105+
106+
# four processors
107+
kaleido.write_fig_from_object_sync(generate_figure_objects(), n=4)
108+
```

src/py/docs/examples_script.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# /// script
2+
# requires-python = ">=3.8"
3+
# dependencies = [
4+
# "pandas",
5+
# "plotly[express]",
6+
# "kaleido @ file:///${PROJECT_ROOT}/",
7+
# ]
8+
# ///
9+
"""Runs the examples in the documentation. Use `mkdir output/; uv run`."""
10+
11+
import asyncio
12+
13+
import plotly.express as px
14+
15+
import kaleido
16+
17+
### SAMPLE DATA ###
18+
19+
fig = px.scatter(
20+
px.data.iris(),
21+
x="sepal_length",
22+
y="sepal_width",
23+
color="species",
24+
)
25+
26+
fig2 = px.line(
27+
px.data.gapminder().query("country=='Canada'"),
28+
x="year",
29+
y="lifeExp",
30+
title="Life expectancy in Canada",
31+
)
32+
33+
figures = [fig, fig2]
34+
35+
### WRITE FIGURES ###
36+
37+
# Simple one image synchronous write
38+
39+
kaleido.write_fig_sync(fig, path="./output/")
40+
41+
42+
# Multiple image write with error collection
43+
44+
error_log = []
45+
46+
kaleido.write_fig_sync(
47+
figures,
48+
path="./output/",
49+
opts={"format": "jpg"},
50+
error_log=error_log,
51+
)
52+
53+
# Dump the error_log
54+
55+
if error_log:
56+
for e in error_log:
57+
print(str(e)) # noqa: T201
58+
raise RuntimeError("{len(error_log)} images failed.")
59+
60+
61+
# async/await style of above
62+
63+
asyncio.run(
64+
kaleido.write_fig(
65+
figures,
66+
path="./output/",
67+
opts={"format": "jpg"},
68+
error_log=error_log,
69+
),
70+
)
71+
72+
### Make a figure generator
73+
74+
75+
def generate_figures(): # can be async as well
76+
"""Generate plotly figures for each country in gapminder."""
77+
data = px.data.gapminder()
78+
for country in data["country"].unique(): # list all countries in dataset
79+
# yield unique plot for each country
80+
yield px.line(
81+
data.query(f'country=="{country}"'),
82+
x="year",
83+
y="lifeExp",
84+
title=f"Life expectancy in {country}",
85+
)
86+
87+
88+
kaleido.write_fig_sync(generate_figures(), path="./output/", n=15)
89+
# file names will be taken from figure title
90+
91+
92+
### If you need more control, use an object
93+
94+
95+
def generate_figure_objects():
96+
"""Generate plotly figure objects for each country in gapminder."""
97+
data = px.data.gapminder()
98+
for country in data["country"].unique(): # list all countries in dataset
99+
fig = px.line(
100+
data.query(f'country=="{country}"'),
101+
x="year",
102+
y="lifeExp",
103+
title=f"Life expectancy in {country}",
104+
)
105+
yield {"fig": fig, "path": f"./output/{country}.jpg"}
106+
# customize file name
107+
108+
109+
# use 15 processes
110+
kaleido.write_fig_from_object_sync(generate_figure_objects(), n=15)

src/py/kaleido/__init__.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
Please see the README.md for more information and a quickstart.
55
"""
66

7+
import asyncio
8+
import queue
9+
from threading import Thread
10+
711
from choreographer.cli import get_chrome, get_chrome_sync
812

913
from ._page_generator import PageGenerator
@@ -12,13 +16,42 @@
1216
__all__ = [
1317
"Kaleido",
1418
"PageGenerator",
19+
"calc_fig",
20+
"calc_fig_sync",
1521
"get_chrome",
1622
"get_chrome_sync",
1723
"write_fig",
1824
"write_fig_from_object",
25+
"write_fig_from_object_sync",
26+
"write_fig_sync",
1927
]
2028

2129

30+
async def calc_fig(
31+
fig,
32+
path=None,
33+
opts=None,
34+
*,
35+
topojson=None,
36+
):
37+
"""
38+
Return binary for plotly figure.
39+
40+
A convenience wrapper for `Kaleido.calc_fig()` which starts a `Kaleido` and
41+
executes the `calc_fig()`.
42+
43+
See documentation for `Kaleido.calc_fig()`.
44+
45+
"""
46+
async with Kaleido(n=1) as k:
47+
return await k.calc_fig(
48+
fig,
49+
path=path,
50+
opts=opts,
51+
topojson=topojson,
52+
)
53+
54+
2255
async def write_fig( # noqa: PLR0913 (too many args, complexity)
2356
fig,
2457
path=None,
@@ -74,5 +107,32 @@ async def write_fig_from_object(
74107
generator,
75108
error_log=error_log,
76109
profiler=profiler,
77-
n=n,
78110
)
111+
112+
113+
def _async_thread_run(func, args, kwargs):
114+
q = queue.Queue(maxsize=1)
115+
116+
def run(*args, **kwargs):
117+
# func is a closure
118+
q.put(asyncio.run(func(*args, **kwargs)))
119+
120+
t = Thread(target=run, args=args, kwargs=kwargs)
121+
t.start()
122+
t.join()
123+
return q.get()
124+
125+
126+
def calc_fig_sync(*args, **kwargs):
127+
"""Call `calc_fig` but blocking."""
128+
return _async_thread_run(calc_fig, args=args, kwargs=kwargs)
129+
130+
131+
def write_fig_sync(*args, **kwargs):
132+
"""Call `write_fig` but blocking."""
133+
_async_thread_run(write_fig, args=args, kwargs=kwargs)
134+
135+
136+
def write_fig_from_object_sync(*args, **kwargs):
137+
"""Call `write_fig_from_object` but blocking."""
138+
_async_thread_run(write_fig_from_object, args=args, kwargs=kwargs)

0 commit comments

Comments
 (0)