batplot 1.8.0__py3-none-any.whl → 1.8.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of batplot might be problematic. Click here for more details.
- batplot/__init__.py +1 -1
- batplot/args.py +5 -3
- batplot/batplot.py +44 -4
- batplot/cpc_interactive.py +96 -3
- batplot/electrochem_interactive.py +28 -0
- batplot/interactive.py +18 -2
- batplot/modes.py +12 -12
- batplot/operando.py +2 -0
- batplot/operando_ec_interactive.py +112 -11
- batplot/session.py +35 -1
- batplot/utils.py +40 -0
- batplot/version_check.py +85 -6
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/METADATA +1 -1
- batplot-1.8.2.dist-info/RECORD +75 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/top_level.txt +1 -0
- batplot_backup_20251221_101150/__init__.py +5 -0
- batplot_backup_20251221_101150/args.py +625 -0
- batplot_backup_20251221_101150/batch.py +1176 -0
- batplot_backup_20251221_101150/batplot.py +3589 -0
- batplot_backup_20251221_101150/cif.py +823 -0
- batplot_backup_20251221_101150/cli.py +149 -0
- batplot_backup_20251221_101150/color_utils.py +547 -0
- batplot_backup_20251221_101150/config.py +198 -0
- batplot_backup_20251221_101150/converters.py +204 -0
- batplot_backup_20251221_101150/cpc_interactive.py +4409 -0
- batplot_backup_20251221_101150/electrochem_interactive.py +4520 -0
- batplot_backup_20251221_101150/interactive.py +3894 -0
- batplot_backup_20251221_101150/manual.py +323 -0
- batplot_backup_20251221_101150/modes.py +799 -0
- batplot_backup_20251221_101150/operando.py +603 -0
- batplot_backup_20251221_101150/operando_ec_interactive.py +5487 -0
- batplot_backup_20251221_101150/plotting.py +228 -0
- batplot_backup_20251221_101150/readers.py +2607 -0
- batplot_backup_20251221_101150/session.py +2951 -0
- batplot_backup_20251221_101150/style.py +1441 -0
- batplot_backup_20251221_101150/ui.py +790 -0
- batplot_backup_20251221_101150/utils.py +1046 -0
- batplot_backup_20251221_101150/version_check.py +253 -0
- batplot-1.8.0.dist-info/RECORD +0 -52
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/WHEEL +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
"""UI utilities for batplot: font/tick helpers and resize operations.
|
|
2
|
+
|
|
3
|
+
This module provides functions for managing fonts, tick labels, and axis labels
|
|
4
|
+
in batplot plots. It handles:
|
|
5
|
+
- Font family and size changes across all text elements
|
|
6
|
+
- Positioning of duplicate axis labels (top x-axis, right y-axis)
|
|
7
|
+
- Font synchronization to ensure consistency
|
|
8
|
+
|
|
9
|
+
HOW FONT MANAGEMENT WORKS:
|
|
10
|
+
--------------------------
|
|
11
|
+
When you change fonts in the interactive menu, we need to update fonts for:
|
|
12
|
+
- Curve labels (text objects identifying each curve)
|
|
13
|
+
- Axis labels (x-axis and y-axis titles)
|
|
14
|
+
- Duplicate axis labels (top x-axis, right y-axis)
|
|
15
|
+
- Tick labels (numbers on axes)
|
|
16
|
+
- All of these must stay synchronized
|
|
17
|
+
|
|
18
|
+
This module provides functions to apply font changes consistently across
|
|
19
|
+
all these elements.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import List, Dict, Any
|
|
25
|
+
import numpy as np
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
from matplotlib.ticker import AutoMinorLocator, NullFormatter
|
|
28
|
+
import matplotlib.transforms as mtransforms
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def apply_font_changes(ax, fig, label_text_objects: List, normalize_label_text, new_size=None, new_family=None):
|
|
32
|
+
"""
|
|
33
|
+
Apply font size and/or family changes to all text elements in the plot.
|
|
34
|
+
|
|
35
|
+
HOW IT WORKS:
|
|
36
|
+
------------
|
|
37
|
+
This function updates fonts for all text elements in the plot:
|
|
38
|
+
1. Curve labels (text objects next to curves)
|
|
39
|
+
2. Axis labels (x-axis and y-axis titles)
|
|
40
|
+
3. Duplicate axis labels (top x-axis, right y-axis)
|
|
41
|
+
4. Tick labels (numbers on axes, including top/right ticks)
|
|
42
|
+
|
|
43
|
+
FONT FAMILY HANDLING:
|
|
44
|
+
--------------------
|
|
45
|
+
When changing font family, we:
|
|
46
|
+
1. Build a fallback chain (primary font + common fallbacks)
|
|
47
|
+
2. Update matplotlib's rcParams (affects new text)
|
|
48
|
+
3. Update all existing text objects
|
|
49
|
+
|
|
50
|
+
FONT SIZE HANDLING:
|
|
51
|
+
-------------------
|
|
52
|
+
When changing font size, we:
|
|
53
|
+
1. Update matplotlib's rcParams (affects new text)
|
|
54
|
+
2. Update all existing text objects directly
|
|
55
|
+
|
|
56
|
+
MATH TEXT FONT SET:
|
|
57
|
+
------------------
|
|
58
|
+
If font family contains "stix", "times", or "roman", we use STIX math font
|
|
59
|
+
(better for mathematical notation). Otherwise, use DejaVu Sans math font.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
ax: Matplotlib axes object
|
|
63
|
+
fig: Matplotlib figure object
|
|
64
|
+
label_text_objects: List of Text objects for curve labels
|
|
65
|
+
normalize_label_text: Function to normalize label text (handles LaTeX)
|
|
66
|
+
new_size: New font size (None = don't change)
|
|
67
|
+
new_family: New font family name (None = don't change)
|
|
68
|
+
"""
|
|
69
|
+
if new_family:
|
|
70
|
+
fallback_chain = ['DejaVu Sans', 'Arial Unicode MS', 'Liberation Sans']
|
|
71
|
+
existing = plt.rcParams.get('font.sans-serif', [])
|
|
72
|
+
new_list = [new_family] + [f for f in fallback_chain if f != new_family] + \
|
|
73
|
+
[f for f in existing if f not in fallback_chain and f != new_family]
|
|
74
|
+
plt.rcParams['font.family'] = 'sans-serif'
|
|
75
|
+
plt.rcParams['font.sans-serif'] = new_list
|
|
76
|
+
lf = new_family.lower()
|
|
77
|
+
if any(k in lf for k in ('stix', 'times', 'roman')):
|
|
78
|
+
plt.rcParams['mathtext.fontset'] = 'stix'
|
|
79
|
+
else:
|
|
80
|
+
plt.rcParams['mathtext.fontset'] = 'dejavusans'
|
|
81
|
+
if new_size is not None:
|
|
82
|
+
plt.rcParams['font.size'] = new_size
|
|
83
|
+
for txt in label_text_objects:
|
|
84
|
+
if new_size is not None:
|
|
85
|
+
txt.set_fontsize(new_size)
|
|
86
|
+
if new_family:
|
|
87
|
+
txt.set_fontfamily(new_family)
|
|
88
|
+
for axis_label in (ax.xaxis.label, ax.yaxis.label):
|
|
89
|
+
cur = axis_label.get_text()
|
|
90
|
+
norm = normalize_label_text(cur)
|
|
91
|
+
if norm != cur:
|
|
92
|
+
axis_label.set_text(norm)
|
|
93
|
+
if new_size is not None:
|
|
94
|
+
axis_label.set_fontsize(new_size)
|
|
95
|
+
if new_family:
|
|
96
|
+
axis_label.set_fontfamily(new_family)
|
|
97
|
+
if hasattr(ax, '_top_xlabel_artist') and ax._top_xlabel_artist is not None:
|
|
98
|
+
if new_size is not None:
|
|
99
|
+
ax._top_xlabel_artist.set_fontsize(new_size)
|
|
100
|
+
if new_family:
|
|
101
|
+
ax._top_xlabel_artist.set_fontfamily(new_family)
|
|
102
|
+
if hasattr(ax, '_right_ylabel_artist') and ax._right_ylabel_artist is not None:
|
|
103
|
+
if new_size is not None:
|
|
104
|
+
ax._right_ylabel_artist.set_fontsize(new_size)
|
|
105
|
+
if new_family:
|
|
106
|
+
ax._right_ylabel_artist.set_fontfamily(new_family)
|
|
107
|
+
for lbl in ax.get_xticklabels() + ax.get_yticklabels():
|
|
108
|
+
if new_size is not None:
|
|
109
|
+
lbl.set_fontsize(new_size)
|
|
110
|
+
if new_family:
|
|
111
|
+
lbl.set_fontfamily(new_family)
|
|
112
|
+
# Also update top/right tick labels (label2)
|
|
113
|
+
try:
|
|
114
|
+
for t in ax.xaxis.get_major_ticks():
|
|
115
|
+
if hasattr(t, 'label2'):
|
|
116
|
+
if new_size is not None: t.label2.set_size(new_size)
|
|
117
|
+
if new_family: t.label2.set_family(new_family)
|
|
118
|
+
for t in ax.yaxis.get_major_ticks():
|
|
119
|
+
if hasattr(t, 'label2'):
|
|
120
|
+
if new_size is not None: t.label2.set_size(new_size)
|
|
121
|
+
if new_family: t.label2.set_family(new_family)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
fig.canvas.draw_idle()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def sync_fonts(ax, fig, label_text_objects: List):
|
|
128
|
+
try:
|
|
129
|
+
base_size = plt.rcParams.get('font.size')
|
|
130
|
+
if base_size is None:
|
|
131
|
+
return
|
|
132
|
+
for txt in label_text_objects:
|
|
133
|
+
txt.set_fontsize(base_size)
|
|
134
|
+
if ax.xaxis.label: ax.xaxis.label.set_fontsize(base_size)
|
|
135
|
+
if ax.yaxis.label: ax.yaxis.label.set_fontsize(base_size)
|
|
136
|
+
if hasattr(ax, '_top_xlabel_artist') and ax._top_xlabel_artist is not None:
|
|
137
|
+
ax._top_xlabel_artist.set_fontsize(base_size)
|
|
138
|
+
if hasattr(ax, '_right_ylabel_artist') and ax._right_ylabel_artist is not None:
|
|
139
|
+
ax._right_ylabel_artist.set_fontsize(base_size)
|
|
140
|
+
for tl in ax.get_xticklabels() + ax.get_yticklabels():
|
|
141
|
+
tl.set_fontsize(base_size)
|
|
142
|
+
fig.canvas.draw_idle()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def position_top_xlabel(ax, fig, tick_state: Dict[str, bool]):
|
|
148
|
+
"""
|
|
149
|
+
Position the duplicate x-axis label at the top of the plot.
|
|
150
|
+
|
|
151
|
+
HOW IT WORKS:
|
|
152
|
+
------------
|
|
153
|
+
This function creates or updates a text label at the top of the plot that
|
|
154
|
+
duplicates the bottom x-axis label. This is useful when you want the same
|
|
155
|
+
label visible at both top and bottom.
|
|
156
|
+
|
|
157
|
+
POSITIONING LOGIC:
|
|
158
|
+
-----------------
|
|
159
|
+
1. Measure height of top tick labels (if visible)
|
|
160
|
+
2. Add spacing gap (14 points) to avoid overlap
|
|
161
|
+
3. Apply any manual offsets (if user nudged the label)
|
|
162
|
+
4. Position label above the plot area
|
|
163
|
+
|
|
164
|
+
COORDINATE SYSTEMS:
|
|
165
|
+
------------------
|
|
166
|
+
- transAxes: Coordinates relative to axes (0,0 = bottom-left, 1,1 = top-right)
|
|
167
|
+
- points: Physical units (72 points = 1 inch)
|
|
168
|
+
- We use offset_copy() to shift from axes coordinates by a fixed point distance
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
ax: Matplotlib axes object
|
|
172
|
+
fig: Matplotlib figure object
|
|
173
|
+
tick_state: Dictionary tracking which ticks/labels are visible
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
# Check if top xlabel should be visible
|
|
177
|
+
on = bool(getattr(ax, '_top_xlabel_on', False))
|
|
178
|
+
if on:
|
|
179
|
+
# Try multiple sources for label text (in order of priority):
|
|
180
|
+
# 1. Override text (if explicitly set)
|
|
181
|
+
# 2. Current bottom xlabel (most common case)
|
|
182
|
+
# 3. Stored xlabel (backup if bottom label was cleared)
|
|
183
|
+
# 4. Existing artist text (preserve if already exists)
|
|
184
|
+
base = getattr(ax, '_top_xlabel_text_override', None)
|
|
185
|
+
if not base:
|
|
186
|
+
base = ax.get_xlabel() # Get current bottom x-axis label
|
|
187
|
+
if not base and hasattr(ax, '_stored_xlabel'):
|
|
188
|
+
try:
|
|
189
|
+
base = ax._stored_xlabel # Fallback to stored label
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
if not base:
|
|
193
|
+
# Last resort: get text from existing artist (if it exists)
|
|
194
|
+
prev = getattr(ax, '_top_xlabel_artist', None)
|
|
195
|
+
if prev is not None and hasattr(prev, 'get_text'):
|
|
196
|
+
base = prev.get_text() or ''
|
|
197
|
+
else:
|
|
198
|
+
base = '' # No text available
|
|
199
|
+
|
|
200
|
+
# Get renderer to measure text dimensions
|
|
201
|
+
# Renderer converts text to pixels so we can measure its size
|
|
202
|
+
# We wrap in try/except because renderer might not be available in all contexts
|
|
203
|
+
try:
|
|
204
|
+
renderer = fig.canvas.get_renderer()
|
|
205
|
+
except Exception:
|
|
206
|
+
renderer = None
|
|
207
|
+
|
|
208
|
+
# Measure tick label height - ONLY use top labels for top title (independence)
|
|
209
|
+
# DPI = dots per inch (resolution of the figure)
|
|
210
|
+
# We need this to convert pixels to points (72 points = 1 inch)
|
|
211
|
+
dpi = float(fig.dpi) if hasattr(fig, 'dpi') else 100.0
|
|
212
|
+
max_h_px = 0.0 # Maximum height in pixels (will find tallest tick label)
|
|
213
|
+
|
|
214
|
+
# Measure TOP tick labels only (for independence from bottom side)
|
|
215
|
+
# tick_state dictionary tracks which ticks/labels are visible
|
|
216
|
+
# 't_labels' or 'tx' means top x-axis labels are visible
|
|
217
|
+
top_labels_on = bool(tick_state.get('t_labels', tick_state.get('tx', False)))
|
|
218
|
+
if top_labels_on and renderer is not None:
|
|
219
|
+
try:
|
|
220
|
+
# Loop through all major ticks on x-axis
|
|
221
|
+
for t in ax.xaxis.get_major_ticks():
|
|
222
|
+
# label2 is the top tick label (label1 is bottom)
|
|
223
|
+
lab = getattr(t, 'label2', None)
|
|
224
|
+
if lab is not None and lab.get_visible():
|
|
225
|
+
# Get bounding box (size) of this label in pixels
|
|
226
|
+
bb = lab.get_window_extent(renderer=renderer)
|
|
227
|
+
if bb is not None:
|
|
228
|
+
# Track the tallest label (we need space for the tallest one)
|
|
229
|
+
max_h_px = max(max_h_px, float(bb.height))
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
# Convert pixels to points and add gap
|
|
234
|
+
# 72 points = 1 inch, so: points = pixels * 72 / dpi
|
|
235
|
+
# We add 14 points of gap to match matplotlib's default labelpad
|
|
236
|
+
if max_h_px > 0:
|
|
237
|
+
tick_height_pts = max_h_px * 72.0 / dpi # Convert pixels → points
|
|
238
|
+
dy_pts = tick_height_pts + 14.0 # 14pt gap to match bottom labelpad
|
|
239
|
+
else:
|
|
240
|
+
# No tick labels visible - use minimal spacing
|
|
241
|
+
dy_pts = 6.0 # Minimal spacing when no tick labels (match small labelpad)
|
|
242
|
+
# Apply manual offsets (stored in points) if user nudged the duplicate title
|
|
243
|
+
# Users can manually adjust label position in interactive menu (WASD keys)
|
|
244
|
+
# These offsets are stored as figure attributes and applied here
|
|
245
|
+
try:
|
|
246
|
+
manual_y_pts = float(getattr(ax, '_top_xlabel_manual_offset_y_pts', 0.0) or 0.0)
|
|
247
|
+
except Exception:
|
|
248
|
+
manual_y_pts = 0.0
|
|
249
|
+
try:
|
|
250
|
+
manual_x_pts = float(getattr(ax, '_top_xlabel_manual_offset_x_pts', 0.0) or 0.0)
|
|
251
|
+
except Exception:
|
|
252
|
+
manual_x_pts = 0.0
|
|
253
|
+
dy_pts += manual_y_pts # Add user's manual vertical offset
|
|
254
|
+
|
|
255
|
+
# Create coordinate transformation
|
|
256
|
+
# transAxes: coordinates relative to axes (0,0 = bottom-left, 1,1 = top-right)
|
|
257
|
+
# offset_copy: creates a new transform that's offset by fixed point distances
|
|
258
|
+
# This lets us position text at (0.5, 1.0) in axes coords, then shift by points
|
|
259
|
+
base_trans = ax.transAxes # Base coordinate system (axes-relative)
|
|
260
|
+
off_trans = mtransforms.offset_copy(base_trans, fig=fig, x=manual_x_pts, y=dy_pts, units='points')
|
|
261
|
+
art = getattr(ax, '_top_xlabel_artist', None)
|
|
262
|
+
try:
|
|
263
|
+
dup_color = ax.xaxis.label.get_color()
|
|
264
|
+
except Exception:
|
|
265
|
+
dup_color = None
|
|
266
|
+
if art is None:
|
|
267
|
+
ax._top_xlabel_artist = ax.text(
|
|
268
|
+
0.5, 1.0, base, ha='center', va='bottom',
|
|
269
|
+
transform=off_trans, clip_on=False, zorder=10,
|
|
270
|
+
color=dup_color
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
ax._top_xlabel_artist.set_transform(off_trans)
|
|
274
|
+
ax._top_xlabel_artist.set_text(base)
|
|
275
|
+
ax._top_xlabel_artist.set_visible(True)
|
|
276
|
+
if dup_color is not None:
|
|
277
|
+
try:
|
|
278
|
+
ax._top_xlabel_artist.set_color(dup_color)
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
else:
|
|
282
|
+
if hasattr(ax, '_top_xlabel_artist') and ax._top_xlabel_artist is not None:
|
|
283
|
+
try:
|
|
284
|
+
ax._top_xlabel_artist.set_visible(False)
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
# Do NOT call draw_idle() here - let the main loop handle drawing
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def position_right_ylabel(ax, fig, tick_state: Dict[str, bool]):
|
|
293
|
+
try:
|
|
294
|
+
on = bool(getattr(ax, '_right_ylabel_on', False))
|
|
295
|
+
if on:
|
|
296
|
+
# Try multiple sources for label text: override, left ylabel, stored, or existing artist
|
|
297
|
+
base = getattr(ax, '_right_ylabel_text_override', None)
|
|
298
|
+
if not base:
|
|
299
|
+
base = ax.get_ylabel()
|
|
300
|
+
if not base and hasattr(ax, '_stored_ylabel'):
|
|
301
|
+
try:
|
|
302
|
+
base = ax._stored_ylabel
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
if not base:
|
|
306
|
+
prev = getattr(ax, '_right_ylabel_artist', None)
|
|
307
|
+
if prev is not None and hasattr(prev, 'get_text'):
|
|
308
|
+
base = prev.get_text() or ''
|
|
309
|
+
else:
|
|
310
|
+
base = ''
|
|
311
|
+
|
|
312
|
+
# Get renderer without forcing draws (let main loop handle drawing)
|
|
313
|
+
try:
|
|
314
|
+
renderer = fig.canvas.get_renderer()
|
|
315
|
+
except Exception:
|
|
316
|
+
renderer = None
|
|
317
|
+
|
|
318
|
+
# Measure tick label width - ONLY use right labels for right title (independence)
|
|
319
|
+
dpi = float(fig.dpi) if hasattr(fig, 'dpi') else 100.0
|
|
320
|
+
max_w_px = 0.0
|
|
321
|
+
|
|
322
|
+
# Measure RIGHT tick labels only (for independence from left side)
|
|
323
|
+
right_labels_on = bool(tick_state.get('r_labels', tick_state.get('ry', False)))
|
|
324
|
+
if right_labels_on and renderer is not None:
|
|
325
|
+
try:
|
|
326
|
+
for t in ax.yaxis.get_major_ticks():
|
|
327
|
+
lab = getattr(t, 'label2', None)
|
|
328
|
+
if lab is not None and lab.get_visible():
|
|
329
|
+
bb = lab.get_window_extent(renderer=renderer)
|
|
330
|
+
if bb is not None:
|
|
331
|
+
max_w_px = max(max_w_px, float(bb.width))
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
# Convert to points and add gap (6pt gap to visually match left labelpad=8pt)
|
|
336
|
+
if max_w_px > 0:
|
|
337
|
+
tick_width_pts = max_w_px * 72.0 / dpi
|
|
338
|
+
dx_pts = tick_width_pts + 6.0 # 6pt gap to visually match left labelpad
|
|
339
|
+
else:
|
|
340
|
+
dx_pts = 6.0 # Minimal spacing when no tick labels (match small labelpad)
|
|
341
|
+
# Apply manual offsets (stored in points) if user nudged the duplicate title
|
|
342
|
+
try:
|
|
343
|
+
manual_x_pts = float(getattr(ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0)
|
|
344
|
+
except Exception:
|
|
345
|
+
manual_x_pts = 0.0
|
|
346
|
+
try:
|
|
347
|
+
manual_y_pts = float(getattr(ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0)
|
|
348
|
+
except Exception:
|
|
349
|
+
manual_y_pts = 0.0
|
|
350
|
+
dx_pts += manual_x_pts
|
|
351
|
+
|
|
352
|
+
# Place at (1.0, 0.5) in axes with a points-based offset to the right
|
|
353
|
+
base_trans = ax.transAxes
|
|
354
|
+
off_trans = mtransforms.offset_copy(base_trans, fig=fig, x=dx_pts, y=manual_y_pts, units='points')
|
|
355
|
+
art = getattr(ax, '_right_ylabel_artist', None)
|
|
356
|
+
try:
|
|
357
|
+
dup_color = ax.yaxis.label.get_color()
|
|
358
|
+
except Exception:
|
|
359
|
+
dup_color = None
|
|
360
|
+
if art is None:
|
|
361
|
+
ax._right_ylabel_artist = ax.text(
|
|
362
|
+
1.0, 0.5, base,
|
|
363
|
+
rotation=90, va='center', ha='left', transform=off_trans,
|
|
364
|
+
clip_on=False, zorder=10,
|
|
365
|
+
color=dup_color
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
ax._right_ylabel_artist.set_transform(off_trans)
|
|
369
|
+
ax._right_ylabel_artist.set_text(base)
|
|
370
|
+
ax._right_ylabel_artist.set_visible(True)
|
|
371
|
+
if dup_color is not None:
|
|
372
|
+
try:
|
|
373
|
+
ax._right_ylabel_artist.set_color(dup_color)
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
else:
|
|
377
|
+
if hasattr(ax, '_right_ylabel_artist') and ax._right_ylabel_artist is not None:
|
|
378
|
+
try:
|
|
379
|
+
ax._right_ylabel_artist.set_visible(False)
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
# Do NOT call draw_idle() here - let the main loop handle drawing
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def position_bottom_xlabel(ax, fig, tick_state: Dict[str, bool]):
|
|
388
|
+
"""Adjust bottom X label spacing based on bottom tick label visibility.
|
|
389
|
+
|
|
390
|
+
Uses labelpad (in points). Larger pad when bottom tick labels are visible,
|
|
391
|
+
smaller when hidden. Also applies manual offsets if set.
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
lbl = ax.get_xlabel()
|
|
395
|
+
if not lbl:
|
|
396
|
+
return
|
|
397
|
+
# If a one-shot pad restore is pending (after hide->show), honor it once to avoid drift
|
|
398
|
+
if hasattr(ax, '_pending_xlabelpad') and ax._pending_xlabelpad is not None:
|
|
399
|
+
try:
|
|
400
|
+
ax.xaxis.labelpad = ax._pending_xlabelpad
|
|
401
|
+
finally:
|
|
402
|
+
try:
|
|
403
|
+
delattr(ax, '_pending_xlabelpad')
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
return
|
|
407
|
+
# Otherwise choose pad based on current tick label visibility
|
|
408
|
+
pad = 8 if bool(tick_state.get('b_labels', tick_state.get('bx', False))) else 6
|
|
409
|
+
# Apply manual y-offset (affects labelpad - positive = down, negative = up)
|
|
410
|
+
try:
|
|
411
|
+
manual_y_pts = float(getattr(ax, '_bottom_xlabel_manual_offset_y_pts', 0.0) or 0.0)
|
|
412
|
+
except Exception:
|
|
413
|
+
manual_y_pts = 0.0
|
|
414
|
+
pad += manual_y_pts
|
|
415
|
+
try:
|
|
416
|
+
ax.xaxis.labelpad = pad
|
|
417
|
+
except Exception:
|
|
418
|
+
pass
|
|
419
|
+
# Do NOT call draw_idle() here - let the main loop handle drawing
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def position_left_ylabel(ax, fig, tick_state: Dict[str, bool]):
|
|
425
|
+
"""Adjust left Y label spacing based on left tick label visibility.
|
|
426
|
+
|
|
427
|
+
Uses labelpad (in points). Larger pad when left tick labels are visible,
|
|
428
|
+
smaller when hidden.
|
|
429
|
+
"""
|
|
430
|
+
try:
|
|
431
|
+
lbl = ax.get_ylabel()
|
|
432
|
+
if not lbl:
|
|
433
|
+
return
|
|
434
|
+
# If a one-shot pad restore is pending (after hide->show), honor it once to avoid drift
|
|
435
|
+
if hasattr(ax, '_pending_ylabelpad') and ax._pending_ylabelpad is not None:
|
|
436
|
+
try:
|
|
437
|
+
ax.yaxis.labelpad = ax._pending_ylabelpad
|
|
438
|
+
finally:
|
|
439
|
+
try:
|
|
440
|
+
delattr(ax, '_pending_ylabelpad')
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
return
|
|
444
|
+
pad = 8 if bool(tick_state.get('l_labels', tick_state.get('ly', False))) else 6
|
|
445
|
+
# Apply manual x-offset (affects labelpad - positive = left, negative = right)
|
|
446
|
+
try:
|
|
447
|
+
manual_x_pts = float(getattr(ax, '_left_ylabel_manual_offset_x_pts', 0.0) or 0.0)
|
|
448
|
+
except Exception:
|
|
449
|
+
manual_x_pts = 0.0
|
|
450
|
+
pad += manual_x_pts
|
|
451
|
+
try:
|
|
452
|
+
ax.yaxis.labelpad = pad
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
# Do NOT call draw_idle() here - let the main loop handle drawing
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def update_tick_visibility(ax, tick_state: Dict[str, bool]):
|
|
461
|
+
# Support new separate tick/label keys; fallback to legacy when absent
|
|
462
|
+
if 'b_ticks' in tick_state or 'b_labels' in tick_state:
|
|
463
|
+
ax.tick_params(axis='x',
|
|
464
|
+
bottom=bool(tick_state.get('b_ticks', True)), labelbottom=bool(tick_state.get('b_labels', True)),
|
|
465
|
+
top=bool(tick_state.get('t_ticks', False)), labeltop=bool(tick_state.get('t_labels', False)))
|
|
466
|
+
ax.tick_params(axis='y',
|
|
467
|
+
left=bool(tick_state.get('l_ticks', True)), labelleft=bool(tick_state.get('l_labels', True)),
|
|
468
|
+
right=bool(tick_state.get('r_ticks', False)), labelright=bool(tick_state.get('r_labels', False)))
|
|
469
|
+
else:
|
|
470
|
+
ax.tick_params(axis='x',
|
|
471
|
+
bottom=tick_state['bx'], labelbottom=tick_state['bx'],
|
|
472
|
+
top=tick_state['tx'], labeltop=tick_state['tx'])
|
|
473
|
+
ax.tick_params(axis='y',
|
|
474
|
+
left=tick_state['ly'], labelleft=tick_state['ly'],
|
|
475
|
+
right=tick_state['ry'], labelright=tick_state['ry'])
|
|
476
|
+
if tick_state['mbx'] or tick_state['mtx']:
|
|
477
|
+
ax.xaxis.set_minor_locator(AutoMinorLocator())
|
|
478
|
+
ax.xaxis.set_minor_formatter(NullFormatter())
|
|
479
|
+
ax.tick_params(axis='x', which='minor',
|
|
480
|
+
bottom=tick_state['mbx'],
|
|
481
|
+
top=tick_state['mtx'],
|
|
482
|
+
labelbottom=False, labeltop=False)
|
|
483
|
+
else:
|
|
484
|
+
ax.tick_params(axis='x', which='minor', bottom=False, top=False,
|
|
485
|
+
labelbottom=False, labeltop=False)
|
|
486
|
+
if tick_state['mly'] or tick_state['mry']:
|
|
487
|
+
ax.yaxis.set_minor_locator(AutoMinorLocator())
|
|
488
|
+
ax.yaxis.set_minor_formatter(NullFormatter())
|
|
489
|
+
ax.tick_params(axis='y', which='minor',
|
|
490
|
+
left=tick_state['mly'],
|
|
491
|
+
right=tick_state['mry'],
|
|
492
|
+
labelleft=False, labelright=False)
|
|
493
|
+
else:
|
|
494
|
+
ax.tick_params(axis='y', which='minor', left=False, right=False,
|
|
495
|
+
labelleft=False, labelright=False)
|
|
496
|
+
# After visibility changes, sync tick label fonts (label1 and label2) to rcParams
|
|
497
|
+
try:
|
|
498
|
+
fam_chain = plt.rcParams.get('font.sans-serif')
|
|
499
|
+
fam0 = fam_chain[0] if isinstance(fam_chain, list) and fam_chain else None
|
|
500
|
+
size0 = plt.rcParams.get('font.size', None)
|
|
501
|
+
# Standard tick labels (bottom/left)
|
|
502
|
+
for lbl in ax.get_xticklabels() + ax.get_yticklabels():
|
|
503
|
+
if size0 is not None:
|
|
504
|
+
try: lbl.set_fontsize(size0)
|
|
505
|
+
except Exception: pass
|
|
506
|
+
if fam0:
|
|
507
|
+
try: lbl.set_fontfamily(fam0)
|
|
508
|
+
except Exception: pass
|
|
509
|
+
# Top/right labels (label2)
|
|
510
|
+
for t in ax.xaxis.get_major_ticks():
|
|
511
|
+
lab2 = getattr(t, 'label2', None)
|
|
512
|
+
if lab2 is not None:
|
|
513
|
+
if size0 is not None:
|
|
514
|
+
try: lab2.set_fontsize(size0)
|
|
515
|
+
except Exception: pass
|
|
516
|
+
if fam0:
|
|
517
|
+
try: lab2.set_fontfamily(fam0)
|
|
518
|
+
except Exception: pass
|
|
519
|
+
for t in ax.yaxis.get_major_ticks():
|
|
520
|
+
lab2 = getattr(t, 'label2', None)
|
|
521
|
+
if lab2 is not None:
|
|
522
|
+
if size0 is not None:
|
|
523
|
+
try: lab2.set_fontsize(size0)
|
|
524
|
+
except Exception: pass
|
|
525
|
+
if fam0:
|
|
526
|
+
try: lab2.set_fontfamily(fam0)
|
|
527
|
+
except Exception: pass
|
|
528
|
+
except Exception:
|
|
529
|
+
pass
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def ensure_text_visibility(fig, ax, label_text_objects: List, max_iterations=4, check_only=False):
|
|
533
|
+
try:
|
|
534
|
+
renderer = fig.canvas.get_renderer()
|
|
535
|
+
except Exception:
|
|
536
|
+
fig.canvas.draw()
|
|
537
|
+
try:
|
|
538
|
+
renderer = fig.canvas.get_renderer()
|
|
539
|
+
except Exception:
|
|
540
|
+
return
|
|
541
|
+
if renderer is None:
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
def collect(renderer_obj):
|
|
545
|
+
items = []
|
|
546
|
+
# CRITICAL: Check visibility to avoid measuring hidden labels
|
|
547
|
+
if ax.xaxis.label.get_text() and ax.xaxis.label.get_visible():
|
|
548
|
+
try: items.append(ax.xaxis.label.get_window_extent(renderer=renderer_obj))
|
|
549
|
+
except Exception: pass
|
|
550
|
+
if ax.yaxis.label.get_text() and ax.yaxis.label.get_visible():
|
|
551
|
+
try: items.append(ax.yaxis.label.get_window_extent(renderer=renderer_obj))
|
|
552
|
+
except Exception: pass
|
|
553
|
+
# Include duplicate top/right title artists if present
|
|
554
|
+
if getattr(ax, '_top_xlabel_on', False) and hasattr(ax, '_top_xlabel_artist') and ax._top_xlabel_artist is not None:
|
|
555
|
+
try: items.append(ax._top_xlabel_artist.get_window_extent(renderer=renderer_obj))
|
|
556
|
+
except Exception: pass
|
|
557
|
+
if getattr(ax, '_right_ylabel_on', False) and hasattr(ax, '_right_ylabel_artist') and ax._right_ylabel_artist is not None:
|
|
558
|
+
try: items.append(ax._right_ylabel_artist.get_window_extent(renderer=renderer_obj))
|
|
559
|
+
except Exception: pass
|
|
560
|
+
for t in label_text_objects:
|
|
561
|
+
try: items.append(t.get_window_extent(renderer=renderer_obj))
|
|
562
|
+
except Exception: pass
|
|
563
|
+
return items
|
|
564
|
+
|
|
565
|
+
fig_w, fig_h = fig.get_size_inches(); dpi = fig.dpi
|
|
566
|
+
W, H = fig_w * dpi, fig_h * dpi
|
|
567
|
+
pad = 2
|
|
568
|
+
|
|
569
|
+
def is_out(bb):
|
|
570
|
+
return (bb.x0 < -pad or bb.y0 < -pad or bb.x1 > W + pad or bb.y1 > H + pad)
|
|
571
|
+
|
|
572
|
+
initial = collect(renderer)
|
|
573
|
+
overflow = any(is_out(bb) for bb in initial)
|
|
574
|
+
if check_only:
|
|
575
|
+
return overflow
|
|
576
|
+
if not overflow:
|
|
577
|
+
return False
|
|
578
|
+
|
|
579
|
+
for _ in range(max_iterations):
|
|
580
|
+
sp = fig.subplotpars
|
|
581
|
+
left, right, bottom, top = sp.left, sp.right, sp.bottom, sp.top
|
|
582
|
+
changed = False
|
|
583
|
+
for bb in collect(renderer):
|
|
584
|
+
if not is_out(bb):
|
|
585
|
+
continue
|
|
586
|
+
if bb.x0 < 0 and left < 0.40:
|
|
587
|
+
left = min(left + 0.01, 0.40); changed = True
|
|
588
|
+
if bb.x1 > W and right > left + 0.25:
|
|
589
|
+
right = max(right - 0.01, left + 0.25); changed = True
|
|
590
|
+
if bb.y0 < 0 and bottom < 0.40:
|
|
591
|
+
bottom = min(bottom + 0.01, 0.40); changed = True
|
|
592
|
+
if bb.y1 > H and top > bottom + 0.25:
|
|
593
|
+
top = max(top - 0.01, bottom + 0.25); changed = True
|
|
594
|
+
if not changed:
|
|
595
|
+
break
|
|
596
|
+
fig.subplots_adjust(left=left, right=right, bottom=bottom, top=top)
|
|
597
|
+
fig.canvas.draw()
|
|
598
|
+
try:
|
|
599
|
+
renderer = fig.canvas.get_renderer()
|
|
600
|
+
except Exception:
|
|
601
|
+
break
|
|
602
|
+
if not any(is_out(bb) for bb in collect(renderer)):
|
|
603
|
+
break
|
|
604
|
+
return True
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def resize_plot_frame(fig, ax, y_data_list: List, label_text_objects: List, args, update_labels_func):
|
|
608
|
+
while True:
|
|
609
|
+
try:
|
|
610
|
+
fig_w_in, fig_h_in = fig.get_size_inches()
|
|
611
|
+
ax_bbox = ax.get_position()
|
|
612
|
+
cur_ax_w_in = ax_bbox.width * fig_w_in
|
|
613
|
+
cur_ax_h_in = ax_bbox.height * fig_h_in
|
|
614
|
+
print(f"Current canvas: {fig_w_in:.2f} x {fig_h_in:.2f} in")
|
|
615
|
+
print(f"Current plot frame: {cur_ax_w_in:.2f} x {cur_ax_h_in:.2f} in (W x H)")
|
|
616
|
+
try:
|
|
617
|
+
spec = input("Enter new plot frame size (e.g. '6 4', '6x4', 'w=6 h=4', 'scale=1.2', single width, q=back): ").strip().lower()
|
|
618
|
+
except KeyboardInterrupt:
|
|
619
|
+
print("Canceled.")
|
|
620
|
+
return
|
|
621
|
+
if not spec or spec == 'q':
|
|
622
|
+
return
|
|
623
|
+
new_w_in, new_h_in = cur_ax_w_in, cur_ax_h_in
|
|
624
|
+
if 'scale=' in spec:
|
|
625
|
+
try:
|
|
626
|
+
factor = float(spec.split('scale=')[1].strip())
|
|
627
|
+
new_w_in = cur_ax_w_in * factor
|
|
628
|
+
new_h_in = cur_ax_h_in * factor
|
|
629
|
+
except Exception:
|
|
630
|
+
print("Invalid scale factor.")
|
|
631
|
+
continue
|
|
632
|
+
else:
|
|
633
|
+
parts = spec.replace('x', ' ').split()
|
|
634
|
+
kv = {}; numbers = []
|
|
635
|
+
for p in parts:
|
|
636
|
+
if '=' in p:
|
|
637
|
+
k, v = p.split('=', 1)
|
|
638
|
+
kv[k.strip()] = v.strip()
|
|
639
|
+
else:
|
|
640
|
+
numbers.append(p)
|
|
641
|
+
if kv:
|
|
642
|
+
if 'w' in kv: new_w_in = float(kv['w'])
|
|
643
|
+
if 'h' in kv: new_h_in = float(kv['h'])
|
|
644
|
+
elif len(numbers) == 2:
|
|
645
|
+
new_w_in, new_h_in = float(numbers[0]), float(numbers[1])
|
|
646
|
+
elif len(numbers) == 1:
|
|
647
|
+
new_w_in = float(numbers[0])
|
|
648
|
+
aspect = cur_ax_h_in / cur_ax_w_in if cur_ax_w_in else 1.0
|
|
649
|
+
new_h_in = new_w_in * aspect
|
|
650
|
+
else:
|
|
651
|
+
print("Could not parse specification.")
|
|
652
|
+
continue
|
|
653
|
+
req_w_in, req_h_in = new_w_in, new_h_in
|
|
654
|
+
# Apply exact requested size without any clamping
|
|
655
|
+
# Only enforce minimum size to prevent division by zero
|
|
656
|
+
min_ax_in = 0.01
|
|
657
|
+
new_w_in = max(min_ax_in, new_w_in)
|
|
658
|
+
new_h_in = max(min_ax_in, new_h_in)
|
|
659
|
+
tol = 1e-3
|
|
660
|
+
requesting_full_canvas = (abs(req_w_in - fig_w_in) < tol and abs(req_h_in - fig_h_in) < tol)
|
|
661
|
+
w_frac = new_w_in / fig_w_in
|
|
662
|
+
h_frac = new_h_in / fig_h_in
|
|
663
|
+
same_axes = False
|
|
664
|
+
if hasattr(fig, '_last_user_axes_inches'):
|
|
665
|
+
pw, ph = fig._last_user_axes_inches
|
|
666
|
+
if abs(pw - new_w_in) < tol and abs(ph - new_h_in) < tol:
|
|
667
|
+
same_axes = True
|
|
668
|
+
if same_axes and hasattr(fig, '_last_user_margins'):
|
|
669
|
+
lm, bm, rm, tm = fig._last_user_margins
|
|
670
|
+
fig.subplots_adjust(left=lm, bottom=bm, right=rm, top=tm)
|
|
671
|
+
update_labels_func(ax, y_data_list, label_text_objects, args.stack)
|
|
672
|
+
fig.canvas.draw_idle()
|
|
673
|
+
print(f"Plot frame unchanged ({new_w_in:.2f} x {new_h_in:.2f} in); layout preserved.")
|
|
674
|
+
continue
|
|
675
|
+
left = (1 - w_frac) / 2
|
|
676
|
+
right = left + w_frac
|
|
677
|
+
bottom = (1 - h_frac) / 2
|
|
678
|
+
top = bottom + h_frac
|
|
679
|
+
# Apply exact size without margin clamping
|
|
680
|
+
fig.subplots_adjust(left=left, right=right, bottom=bottom, top=top)
|
|
681
|
+
update_labels_func(ax, y_data_list, label_text_objects, args.stack)
|
|
682
|
+
# Store the final size (exactly as requested, no text visibility adjustments)
|
|
683
|
+
sp = fig.subplotpars
|
|
684
|
+
fig._last_user_axes_inches = (new_w_in, new_h_in)
|
|
685
|
+
fig._last_user_margins = (sp.left, sp.bottom, sp.right, sp.top)
|
|
686
|
+
final_w_in = (sp.right - sp.left) * fig_w_in
|
|
687
|
+
final_h_in = (sp.top - sp.bottom) * fig_h_in
|
|
688
|
+
# Show the requested size (which is what was applied)
|
|
689
|
+
print(f"Plot frame set to {req_w_in:.2f} x {req_h_in:.2f} in inside canvas {fig_w_in:.2f} x {fig_h_in:.2f} in.")
|
|
690
|
+
except KeyboardInterrupt:
|
|
691
|
+
print("Canceled.")
|
|
692
|
+
return
|
|
693
|
+
except Exception as e:
|
|
694
|
+
print(f"Error resizing plot frame: {e}")
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def resize_canvas(fig, ax):
|
|
698
|
+
while True:
|
|
699
|
+
try:
|
|
700
|
+
cur_w, cur_h = fig.get_size_inches()
|
|
701
|
+
bbox_before = ax.get_position()
|
|
702
|
+
frame_w_in_before = bbox_before.width * cur_w
|
|
703
|
+
frame_h_in_before = bbox_before.height * cur_h
|
|
704
|
+
print(f"Current canvas size: {cur_w:.2f} x {cur_h:.2f} in (frame {frame_w_in_before:.2f} x {frame_h_in_before:.2f} in)")
|
|
705
|
+
try:
|
|
706
|
+
spec = input("Enter new canvas size (e.g. '8 6', '6x4', 'w=6 h=5', 'scale=1.2', q=back): ").strip().lower()
|
|
707
|
+
except KeyboardInterrupt:
|
|
708
|
+
print("Canceled.")
|
|
709
|
+
return
|
|
710
|
+
if not spec or spec == 'q':
|
|
711
|
+
return
|
|
712
|
+
new_w, new_h = cur_w, cur_h
|
|
713
|
+
if 'scale=' in spec:
|
|
714
|
+
try:
|
|
715
|
+
fct = float(spec.split('scale=')[1])
|
|
716
|
+
new_w, new_h = cur_w * fct, cur_h * fct
|
|
717
|
+
except Exception:
|
|
718
|
+
print("Invalid scale factor.")
|
|
719
|
+
continue
|
|
720
|
+
else:
|
|
721
|
+
parts = spec.replace('x',' ').split()
|
|
722
|
+
kv = {}; nums = []
|
|
723
|
+
for p in parts:
|
|
724
|
+
if '=' in p:
|
|
725
|
+
k,v = p.split('=',1); kv[k.strip()] = v.strip()
|
|
726
|
+
else:
|
|
727
|
+
nums.append(p)
|
|
728
|
+
if kv:
|
|
729
|
+
if 'w' in kv: new_w = float(kv['w'])
|
|
730
|
+
if 'h' in kv: new_h = float(kv['h'])
|
|
731
|
+
elif len(nums)==2:
|
|
732
|
+
new_w, new_h = float(nums[0]), float(nums[1])
|
|
733
|
+
elif len(nums)==1:
|
|
734
|
+
new_w = float(nums[0]); aspect = cur_h/cur_w if cur_w else 1.0; new_h = new_w * aspect
|
|
735
|
+
else:
|
|
736
|
+
print("Could not parse specification.")
|
|
737
|
+
continue
|
|
738
|
+
min_size = 1.0
|
|
739
|
+
new_w = max(min_size, new_w)
|
|
740
|
+
new_h = max(min_size, new_h)
|
|
741
|
+
tol = 1e-3
|
|
742
|
+
same = hasattr(fig,'_last_canvas_size') and all(abs(a-b)<tol for a,b in zip(fig._last_canvas_size,(new_w,new_h)))
|
|
743
|
+
fig.set_size_inches(new_w, new_h, forward=True)
|
|
744
|
+
bbox_after = ax.get_position()
|
|
745
|
+
desired_w_frac = frame_w_in_before / new_w
|
|
746
|
+
desired_h_frac = frame_h_in_before / new_h
|
|
747
|
+
min_margin = 0.05
|
|
748
|
+
max_w_frac = 1 - 2*min_margin
|
|
749
|
+
max_h_frac = 1 - 2*min_margin
|
|
750
|
+
if desired_w_frac > max_w_frac:
|
|
751
|
+
desired_w_frac = max_w_frac
|
|
752
|
+
if desired_h_frac > max_h_frac:
|
|
753
|
+
desired_h_frac = max_h_frac
|
|
754
|
+
left = (1 - desired_w_frac) / 2
|
|
755
|
+
bottom = (1 - desired_h_frac) / 2
|
|
756
|
+
right = left + desired_w_frac
|
|
757
|
+
top = bottom + desired_h_frac
|
|
758
|
+
if right - left > 0.05 and top - bottom > 0.05:
|
|
759
|
+
fig.subplots_adjust(left=left, right=right, bottom=bottom, top=top)
|
|
760
|
+
fig._last_canvas_size = (new_w, new_h)
|
|
761
|
+
bbox_final = ax.get_position()
|
|
762
|
+
final_frame_w_in = bbox_final.width * new_w
|
|
763
|
+
final_frame_h_in = bbox_final.height * new_h
|
|
764
|
+
if same:
|
|
765
|
+
print(f"Canvas unchanged ({new_w:.2f} x {new_h:.2f} in). Frame {final_frame_w_in:.2f} x {final_frame_h_in:.2f} in.")
|
|
766
|
+
else:
|
|
767
|
+
note = ""
|
|
768
|
+
if abs(final_frame_w_in - frame_w_in_before) > 1e-3 or abs(final_frame_h_in - frame_h_in_before) > 1e-3:
|
|
769
|
+
note = " (clamped to fit)" if final_frame_w_in < frame_w_in_before or final_frame_h_in < frame_h_in_before else ""
|
|
770
|
+
print(f"Canvas resized to {new_w:.2f} x {new_h:.2f} in; frame preserved at {final_frame_w_in:.2f} x {final_frame_h_in:.2f} in{note} (was {frame_w_in_before:.2f} x {frame_h_in_before:.2f}).")
|
|
771
|
+
fig.canvas.draw_idle()
|
|
772
|
+
except KeyboardInterrupt:
|
|
773
|
+
print("Canceled.")
|
|
774
|
+
return
|
|
775
|
+
except Exception as e:
|
|
776
|
+
print(f"Error resizing canvas: {e}")
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
__all__ = [
|
|
780
|
+
'apply_font_changes',
|
|
781
|
+
'sync_fonts',
|
|
782
|
+
'position_top_xlabel',
|
|
783
|
+
'position_right_ylabel',
|
|
784
|
+
'position_bottom_xlabel',
|
|
785
|
+
'position_left_ylabel',
|
|
786
|
+
'update_tick_visibility',
|
|
787
|
+
'ensure_text_visibility',
|
|
788
|
+
'resize_plot_frame',
|
|
789
|
+
'resize_canvas',
|
|
790
|
+
]
|