Skip to content

Commit 8cae04e

Browse files
authored
Merge branch 'main' into feat/curved-text
2 parents e859b8d + e92fe33 commit 8cae04e

17 files changed

Lines changed: 232 additions & 41 deletions

File tree

.github/workflows/build-ultraplot.yml

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,22 @@ jobs:
135135
echo "TEST_NODEIDS=${TEST_NODEIDS}"
136136
# Save PR-selected nodeids for reuse after checkout (if provided)
137137
if [ "${TEST_MODE}" = "selected" ] && [ -n "${TEST_NODEIDS}" ]; then
138-
printf "%s\n" ${TEST_NODEIDS} > /tmp/pr_selected_nodeids.txt
138+
python -c 'import json, os
139+
raw = os.environ.get("TEST_NODEIDS", "").strip()
140+
nodeids = []
141+
if raw and raw != "[]":
142+
try:
143+
parsed = json.loads(raw)
144+
except json.JSONDecodeError:
145+
parsed = raw.split()
146+
if isinstance(parsed, str):
147+
parsed = [parsed]
148+
if isinstance(parsed, list):
149+
nodeids = [item for item in parsed if isinstance(item, str) and item]
150+
with open("/tmp/pr_selected_nodeids.txt", "w", encoding="utf-8") as fh:
151+
for nodeid in nodeids:
152+
fh.write(f"{nodeid}\n")
153+
print(f"Selected nodeids parsed: {len(nodeids)}")'
139154
else
140155
: > /tmp/pr_selected_nodeids.txt
141156
fi
@@ -152,26 +167,22 @@ jobs:
152167
python -c "import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')"
153168
if [ "${TEST_MODE}" = "selected" ] && [ -s /tmp/pr_selected_nodeids.txt ]; then
154169
status=0
155-
filter_nodeids() {
156-
local filtered=""
157-
for nodeid in $(cat /tmp/pr_selected_nodeids.txt); do
158-
local path="${nodeid%%::*}"
159-
if [ -f "$path" ]; then
160-
filtered="${filtered} ${nodeid}"
161-
fi
162-
done
163-
echo "${filtered}"
164-
}
165-
FILTERED_NODEIDS="$(filter_nodeids)"
166-
echo "FILTERED_NODEIDS_BASE=${FILTERED_NODEIDS}"
167-
if [ -z "${FILTERED_NODEIDS}" ]; then
170+
mapfile -t FILTERED_NODEIDS < <(
171+
while IFS= read -r nodeid; do
172+
[ -z "$nodeid" ] && continue
173+
path="${nodeid%%::*}"
174+
[ -f "$path" ] && printf '%s\n' "$nodeid"
175+
done < /tmp/pr_selected_nodeids.txt
176+
)
177+
echo "FILTERED_NODEIDS_BASE_COUNT=${#FILTERED_NODEIDS[@]}"
178+
if [ "${#FILTERED_NODEIDS[@]}" -eq 0 ]; then
168179
echo "No valid nodeids found on base; skipping baseline generation."
169180
else
170181
echo "=== Memory before baseline generation ===" && free -h
171182
pytest -n ${PYTEST_WORKERS} --dist loadfile --tb=short --disable-warnings -W ignore \
172183
--mpl-generate-path=./ultraplot/tests/baseline/ \
173184
--mpl-default-style="./ultraplot.yml" \
174-
${FILTERED_NODEIDS} || status=$?
185+
"${FILTERED_NODEIDS[@]}" || status=$?
175186
echo "=== Memory after baseline generation ===" && free -h
176187
if [ "$status" -eq 4 ] || [ "$status" -eq 5 ]; then
177188
echo "No tests collected from selected nodeids on base; skipping baseline generation."
@@ -213,19 +224,15 @@ jobs:
213224
echo "TEST_NODEIDS=${TEST_NODEIDS}"
214225
if [ "${TEST_MODE}" = "selected" ] && [ -s /tmp/pr_selected_nodeids.txt ]; then
215226
status=0
216-
filter_nodeids() {
217-
local filtered=""
218-
for nodeid in $(cat /tmp/pr_selected_nodeids.txt); do
219-
local path="${nodeid%%::*}"
220-
if [ -f "$path" ]; then
221-
filtered="${filtered} ${nodeid}"
222-
fi
223-
done
224-
echo "${filtered}"
225-
}
226-
FILTERED_NODEIDS="$(filter_nodeids)"
227-
echo "FILTERED_NODEIDS_PR=${FILTERED_NODEIDS}"
228-
if [ -z "${FILTERED_NODEIDS}" ]; then
227+
mapfile -t FILTERED_NODEIDS < <(
228+
while IFS= read -r nodeid; do
229+
[ -z "$nodeid" ] && continue
230+
path="${nodeid%%::*}"
231+
[ -f "$path" ] && printf '%s\n' "$nodeid"
232+
done < /tmp/pr_selected_nodeids.txt
233+
)
234+
echo "FILTERED_NODEIDS_PR_COUNT=${#FILTERED_NODEIDS[@]}"
235+
if [ "${#FILTERED_NODEIDS[@]}" -eq 0 ]; then
229236
echo "No valid nodeids found on PR branch; skipping image comparison."
230237
exit 0
231238
else
@@ -236,7 +243,7 @@ jobs:
236243
--mpl-results-path=./results/ \
237244
--mpl-generate-summary=html \
238245
--mpl-default-style="./ultraplot.yml" \
239-
${FILTERED_NODEIDS} || status=$?
246+
"${FILTERED_NODEIDS[@]}" || status=$?
240247
echo "=== Memory after image comparison ===" && free -h
241248
if [ "$status" -eq 4 ] || [ "$status" -eq 5 ]; then
242249
echo "No tests collected from selected nodeids; skipping image comparison."

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
run: |
7575
if [ "${{ github.event_name }}" != "pull_request" ]; then
7676
echo "mode=full" >> $GITHUB_OUTPUT
77-
echo "tests=" >> $GITHUB_OUTPUT
77+
echo "tests=[]" >> $GITHUB_OUTPUT
7878
exit 0
7979
fi
8080
@@ -104,7 +104,7 @@ jobs:
104104
import json
105105
data = json.load(open(".ci/selection.json", "r", encoding="utf-8"))
106106
print(f"mode={data['mode']}")
107-
print("tests=" + " ".join(data.get("tests", [])))
107+
print("tests=" + json.dumps(data.get("tests", []), separators=(",", ":")))
108108
PY
109109
cat .ci/selection.out >> $GITHUB_OUTPUT
110110

docs/2dplots.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@
344344
ax.pcolormesh(data, cmap="magma", colorbar="b")
345345
ax = fig.subplot(gs[1], title="Logarithmic normalizer with norm='log'")
346346
ax.pcolormesh(data, cmap="magma", norm="log", colorbar="b")
347+
fig.show()
347348

348349

349350
# %% [raw] raw_mimetype="text/restructuredtext"
@@ -431,6 +432,7 @@
431432
ax.colorbar(m, loc="b")
432433
ax.format(title=f"{mode.title()}-skewed + {fair} scaling")
433434
i += 1
435+
fig.show()
434436

435437
# %% [raw] raw_mimetype="text/restructuredtext"
436438
# .. _ug_discrete:
@@ -531,6 +533,7 @@
531533
colorbar="b",
532534
colorbar_kw={"locator": 180},
533535
)
536+
fig.show()
534537

535538
# %% [raw] raw_mimetype="text/restructuredtext" tags=[]
536539
# .. _ug_autonorm:

docs/basics.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
# fig = uplt.figure(suptitle='Single subplot') # equivalent to above
8787
# ax = fig.subplot(xlabel='x axis', ylabel='y axis')
8888
ax.plot(data, lw=2)
89+
fig.show()
8990

9091

9192
# %% [raw] raw_mimetype="text/restructuredtext"
@@ -184,6 +185,7 @@
184185
ylabel="ylabel",
185186
)
186187
axs[2].plot(data, lw=2)
188+
fig.show()
187189
# fig.save('~/example2.png') # save the figure
188190
# fig.savefig('~/example2.png') # alternative
189191

@@ -301,6 +303,7 @@
301303
axs[1, :1].format(fc="sky blue")
302304
axs[-1, -1].format(fc="gray4", grid=False)
303305
axs[0].plot((state.rand(50, 10) - 0.5).cumsum(axis=0), cycle="Grays_r", lw=2)
306+
fig.show()
304307

305308

306309
# %% [raw] raw_mimetype="text/restructuredtext"
@@ -361,6 +364,7 @@
361364
suptitle="Quick plotting demo",
362365
)
363366
fig.colorbar(m, loc="b", label="label")
367+
fig.show()
364368

365369

366370
# %% [raw] raw_mimetype="text/restructuredtext"
@@ -565,3 +569,4 @@
565569
for ax, style in zip(axs, styles):
566570
ax.format(style=style, xlabel="xlabel", ylabel="ylabel", title=style)
567571
ax.plot(data, linewidth=3)
572+
fig.show()

docs/conf.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
# Import statements
1515
import datetime
16+
import logging
1617
import os
1718
import re
1819
import subprocess
@@ -78,18 +79,25 @@ def __getattr__(self, name):
7879
except Exception:
7980
pass
8081

82+
# Silence font discovery warnings like "findfont: Font family ..."
83+
for _logger_name in ("matplotlib", "matplotlib.font_manager"):
84+
_logger = logging.getLogger(_logger_name)
85+
_logger.setLevel(logging.ERROR)
86+
_logger.propagate = False
87+
8188
# Suppress deprecated rc key warnings from local configs during docs builds.
8289
try:
8390
from ultraplot.internals.warnings import UltraPlotWarning
8491

92+
warnings.filterwarnings("ignore")
8593
warnings.filterwarnings(
8694
"ignore",
87-
message=r"The rc setting 'colorbar.rasterize' was deprecated.*",
8895
category=UltraPlotWarning,
8996
)
9097
except Exception:
9198
pass
9299

100+
93101
# Print available system fonts
94102
from matplotlib.font_manager import fontManager
95103
from sphinx_gallery.sorting import ExplicitOrder, FileNameSortKey
@@ -103,6 +111,10 @@ def _reset_ultraplot(gallery_conf, fname):
103111
import ultraplot as uplt
104112
except Exception:
105113
return
114+
for _logger_name in ("matplotlib", "matplotlib.font_manager"):
115+
_logger = logging.getLogger(_logger_name)
116+
_logger.setLevel(logging.ERROR)
117+
_logger.propagate = False
106118
uplt.rc.reset()
107119

108120

@@ -349,6 +361,9 @@ def _reset_ultraplot(gallery_conf, fname):
349361

350362
nbsphinx_execute = "auto"
351363

364+
# Suppress warnings in nbsphinx kernels without injecting visible cells.
365+
os.environ["PYTHONWARNINGS"] = "ignore"
366+
352367
# Sphinx gallery configuration
353368
sphinx_gallery_conf = {
354369
"doc_module": ("ultraplot",),

docs/stats.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@
428428
mean_temps = [14.0, 14.2, 14.5, 15.0, 15.5] # warming trend
429429
data = [state.normal(temp, 0.8, 500) for temp in mean_temps]
430430

431-
fig, axs = uplt.subplots(ncols=2, figsize=(11, 5))
431+
fig, axs = uplt.subplots(ncols=2, share=0)
432432
axs.format(abc="A.", abcloc="ul", suptitle="Categorical vs Continuous positioning")
433433

434434
# Categorical positioning (default)

ultraplot/axes/base.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3824,7 +3824,7 @@ def text(
38243824
bordercolor="w",
38253825
borderwidth=2,
38263826
borderinvert=False,
3827-
borderstyle="miter",
3827+
borderstyle=None,
38283828
bboxcolor="w",
38293829
bboxstyle="round",
38303830
bboxalpha=0.5,
@@ -3854,7 +3854,7 @@ def text(
38543854
The color of the text border.
38553855
borderinvert : bool, optional
38563856
If ``True``, the text and border colors are swapped.
3857-
borderstyle : {'miter', 'round', 'bevel'}, optional
3857+
borderstyle : {'miter', 'round', 'bevel'}, default: :rc:`text.borderstyle`
38583858
The `line join style \\
38593859
<https://matplotlib.org/stable/gallery/lines_bars_and_markers/joinstyle.html>`__
38603860
used for the border.
@@ -3901,6 +3901,7 @@ def text(
39013901
kwargs.update(_pop_props(kwargs, "text"))
39023902

39033903
# Update the text object using a monkey patch
3904+
borderstyle = _not_none(borderstyle, rc["text.borderstyle"])
39043905
obj = func(*args, transform=transform, **kwargs)
39053906
obj.update = labels._update_label.__get__(obj)
39063907
obj.update(
@@ -4629,7 +4630,7 @@ def _apply_inset_colorbar_layout(
46294630
"inset": bounds_inset,
46304631
"frame": bounds_frame,
46314632
}
4632-
if frame is not None:
4633+
if frame is not None and hasattr(frame, "set_bounds"):
46334634
frame.set_bounds(*bounds_frame)
46344635

46354636

ultraplot/axes/plot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6525,7 +6525,7 @@ def _apply_ridgeline(
65256525
else:
65266526
# Categorical (evenly-spaced) positioning mode
65276527
max_height = max(y.max() for x, y in ridges)
6528-
spacing = max_height * (1 + overlap)
6528+
spacing = max(0.0, 1 - overlap)
65296529

65306530
artists = []
65316531
# Base zorder for ridgelines - use a high value to ensure they're on top
@@ -6544,7 +6544,7 @@ def _apply_ridgeline(
65446544
y_plot = y_scaled + offset
65456545
else:
65466546
# Categorical mode: normalize and space evenly
6547-
y_normalized = y / max_height
6547+
y_normalized = y / max_height if max_height > 0 else y
65486548
offset = i * spacing
65496549
y_plot = y_normalized + offset
65506550

ultraplot/figure.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,7 @@ def __init__(
869869

870870
@override
871871
def draw(self, renderer):
872+
self._snap_axes_to_pixel_grid(renderer)
872873
# implement the tick sharing here
873874
# should be shareable --> either all cartesian or all geographic
874875
# but no mixing (panels can be mixed)
@@ -880,6 +881,53 @@ def draw(self, renderer):
880881
self._apply_share_label_groups()
881882
super().draw(renderer)
882883

884+
def _snap_axes_to_pixel_grid(self, renderer) -> None:
885+
"""
886+
Snap visible axes bounds to the renderer pixel grid.
887+
"""
888+
if not rc.find("subplots.pixelsnap", context=True):
889+
return
890+
891+
width = getattr(renderer, "width", None)
892+
height = getattr(renderer, "height", None)
893+
if not width or not height:
894+
return
895+
896+
width = float(width)
897+
height = float(height)
898+
if width <= 0 or height <= 0:
899+
return
900+
901+
invw = 1.0 / width
902+
invh = 1.0 / height
903+
minw = invw
904+
minh = invh
905+
906+
for ax in self._iter_axes(hidden=False, children=False, panels=True):
907+
bbox = ax.get_position(original=False)
908+
old = np.array([bbox.x0, bbox.y0, bbox.x1, bbox.y1], dtype=float)
909+
new = np.array(
910+
[
911+
round(old[0] * width) * invw,
912+
round(old[1] * height) * invh,
913+
round(old[2] * width) * invw,
914+
round(old[3] * height) * invh,
915+
],
916+
dtype=float,
917+
)
918+
919+
if new[2] <= new[0]:
920+
new[2] = new[0] + minw
921+
if new[3] <= new[1]:
922+
new[3] = new[1] + minh
923+
924+
if np.allclose(new, old, rtol=0.0, atol=1e-12):
925+
continue
926+
ax.set_position(
927+
[new[0], new[1], new[2] - new[0], new[3] - new[1]],
928+
which="both",
929+
)
930+
883931
def _share_ticklabels(self, *, axis: str) -> None:
884932
"""
885933
Tick label sharing is determined at the figure level. While

ultraplot/gridspec.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,27 @@ def _get_tight_space(self, w):
11631163
x1 = max(ax._range_tightbbox(x)[1] for ax in group1)
11641164
x2 = min(ax._range_tightbbox(x)[0] for ax in group2)
11651165
margins.append((x2 - x1) / self.figure.dpi)
1166-
s = 0 if not margins else max(0, s - min(margins) + p)
1166+
if not margins:
1167+
s = 0
1168+
else:
1169+
s = max(0, s - min(margins) + p)
1170+
# Keep at least the pad when adjacent axes exist.
1171+
if s == 0 and p:
1172+
s = p
1173+
# Ensure enough space for inner-side labels/ticks on the right axes.
1174+
if w == "w":
1175+
figwidth = self.figure.get_size_inches()[0]
1176+
left_margins = []
1177+
for _, group2 in groups:
1178+
for ax in group2:
1179+
bbox = getattr(ax, "_tight_bbox", None)
1180+
if bbox is None:
1181+
continue
1182+
x0 = ax.get_position().x0 * figwidth
1183+
left_margins.append(max(0.0, x0 - bbox.xmin))
1184+
if left_margins:
1185+
extra_pad = 0.5 * self._labelspace / 72
1186+
s = max(s, max(left_margins) + p + extra_pad)
11671187
space[i] = s
11681188

11691189
return space

0 commit comments

Comments
 (0)