batplot 1.7.28__py3-none-any.whl → 1.8.1__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 +3 -3
- batplot/cpc_interactive.py +89 -3
- batplot/electrochem_interactive.py +118 -55
- batplot/interactive.py +100 -63
- batplot/modes.py +12 -12
- batplot/operando.py +2 -0
- batplot/operando_ec_interactive.py +260 -89
- batplot/session.py +18 -1
- batplot/utils.py +40 -0
- batplot/version_check.py +85 -6
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/METADATA +1 -1
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/RECORD +17 -17
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/WHEEL +0 -0
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/top_level.txt +0 -0
|
@@ -18,6 +18,7 @@ from typing import Tuple, Dict, Optional, Any
|
|
|
18
18
|
import json
|
|
19
19
|
import os
|
|
20
20
|
import time
|
|
21
|
+
import sys
|
|
21
22
|
|
|
22
23
|
import matplotlib.pyplot as plt
|
|
23
24
|
from matplotlib.colors import LinearSegmentedColormap
|
|
@@ -38,7 +39,38 @@ from .color_utils import (
|
|
|
38
39
|
resolve_color_token,
|
|
39
40
|
manage_user_colors,
|
|
40
41
|
)
|
|
41
|
-
from .utils import choose_style_file
|
|
42
|
+
from .utils import choose_style_file, convert_label_shortcuts
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _FilterIMKWarning:
|
|
46
|
+
"""Filter that suppresses macOS IMKCFRunLoopWakeUpReliable warnings while preserving other errors."""
|
|
47
|
+
def __init__(self, original_stderr):
|
|
48
|
+
self.original_stderr = original_stderr
|
|
49
|
+
|
|
50
|
+
def write(self, message):
|
|
51
|
+
# Filter out the harmless macOS IMK warning
|
|
52
|
+
if 'IMKCFRunLoopWakeUpReliable' not in message:
|
|
53
|
+
self.original_stderr.write(message)
|
|
54
|
+
|
|
55
|
+
def flush(self):
|
|
56
|
+
self.original_stderr.flush()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _safe_input(prompt: str = "") -> str:
|
|
60
|
+
"""Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
|
|
61
|
+
|
|
62
|
+
This is a harmless macOS system message that appears when using input() in terminals.
|
|
63
|
+
"""
|
|
64
|
+
# Filter stderr to hide macOS IMK warnings while preserving other errors
|
|
65
|
+
original_stderr = sys.stderr
|
|
66
|
+
sys.stderr = _FilterIMKWarning(original_stderr)
|
|
67
|
+
try:
|
|
68
|
+
result = input(prompt)
|
|
69
|
+
return result
|
|
70
|
+
except (KeyboardInterrupt, EOFError):
|
|
71
|
+
raise
|
|
72
|
+
finally:
|
|
73
|
+
sys.stderr = original_stderr
|
|
42
74
|
|
|
43
75
|
|
|
44
76
|
def _axis_tick_width(axis_obj, which: str = 'major'):
|
|
@@ -322,7 +354,7 @@ def _draw_custom_colorbar(cbar_ax, im, label='Intensity', label_mode='normal'):
|
|
|
322
354
|
|
|
323
355
|
def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
|
|
324
356
|
"""Update the custom colorbar when colormap or limits change.
|
|
325
|
-
|
|
357
|
+
|
|
326
358
|
Args:
|
|
327
359
|
cbar_ax: Axes object containing the colorbar
|
|
328
360
|
im: Optional AxesImage object (if None, uses stored reference)
|
|
@@ -333,17 +365,82 @@ def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
|
|
|
333
365
|
im = getattr(cbar_ax, '_colorbar_im', None)
|
|
334
366
|
if im is None:
|
|
335
367
|
return
|
|
336
|
-
|
|
368
|
+
|
|
337
369
|
if label is None:
|
|
338
370
|
label = getattr(cbar_ax, '_colorbar_label', 'Intensity')
|
|
339
|
-
|
|
371
|
+
|
|
340
372
|
if label_mode is None:
|
|
341
373
|
label_mode = getattr(cbar_ax, '_colorbar_label_mode', 'normal')
|
|
342
|
-
|
|
374
|
+
|
|
343
375
|
# Redraw the colorbar
|
|
344
376
|
_draw_custom_colorbar(cbar_ax, im, label, label_mode)
|
|
345
377
|
|
|
346
378
|
|
|
379
|
+
def _safe_set_clim(im, vmin, vmax):
|
|
380
|
+
"""Safely set color limits without triggering matplotlib colorbar callbacks.
|
|
381
|
+
|
|
382
|
+
This wrapper around im.set_clim() prevents the NotImplementedError: cannot remove artist
|
|
383
|
+
error when using custom colorbars. The issue occurs because matplotlib's callback system
|
|
384
|
+
tries to update the colorbar when set_clim() is called, but our custom colorbar drawing
|
|
385
|
+
has already cleared the axes, causing the update to fail.
|
|
386
|
+
|
|
387
|
+
This function suppresses the callback traceback by redirecting stderr to a null device
|
|
388
|
+
during the set_clim() call, preventing matplotlib's callback system from printing
|
|
389
|
+
tracebacks to the terminal.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
im: AxesImage object
|
|
393
|
+
vmin: Minimum value for color scale
|
|
394
|
+
vmax: Maximum value for color scale
|
|
395
|
+
"""
|
|
396
|
+
import sys
|
|
397
|
+
import os
|
|
398
|
+
from io import StringIO
|
|
399
|
+
|
|
400
|
+
# Create a null device for stderr redirection
|
|
401
|
+
class NullDevice:
|
|
402
|
+
def write(self, s):
|
|
403
|
+
pass
|
|
404
|
+
def flush(self):
|
|
405
|
+
pass
|
|
406
|
+
def close(self):
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
# Suppress matplotlib's exception printing by redirecting stderr AND excepthook
|
|
410
|
+
old_stderr = sys.stderr
|
|
411
|
+
old_excepthook = sys.excepthook
|
|
412
|
+
null_dev = NullDevice()
|
|
413
|
+
|
|
414
|
+
# Create a no-op excepthook that suppresses all exceptions
|
|
415
|
+
def suppress_excepthook(exc_type, exc_value, exc_traceback):
|
|
416
|
+
# Only suppress if it's the specific error we're looking for
|
|
417
|
+
if exc_type == NotImplementedError and 'cannot remove artist' in str(exc_value).lower():
|
|
418
|
+
return # Suppress this specific exception
|
|
419
|
+
# For any other exception, use the original handler
|
|
420
|
+
old_excepthook(exc_type, exc_value, exc_traceback)
|
|
421
|
+
|
|
422
|
+
sys.stderr = null_dev
|
|
423
|
+
sys.excepthook = suppress_excepthook
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
# Set the color limits - matplotlib's callback will try to print traceback
|
|
427
|
+
# but both stderr and excepthook are suppressed
|
|
428
|
+
im.set_clim(vmin, vmax)
|
|
429
|
+
# The operation succeeds; any tracebacks are suppressed
|
|
430
|
+
except NotImplementedError:
|
|
431
|
+
# Suppress the specific error - color limits were still updated successfully
|
|
432
|
+
pass
|
|
433
|
+
except Exception:
|
|
434
|
+
# For any other unexpected error, restore handlers and re-raise
|
|
435
|
+
sys.stderr = old_stderr
|
|
436
|
+
sys.excepthook = old_excepthook
|
|
437
|
+
raise
|
|
438
|
+
finally:
|
|
439
|
+
# Always restore both stderr and excepthook
|
|
440
|
+
sys.stderr = old_stderr
|
|
441
|
+
sys.excepthook = old_excepthook
|
|
442
|
+
|
|
443
|
+
|
|
347
444
|
def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
|
|
348
445
|
"""Detach a Matplotlib Colorbar from its mappable callbacks.
|
|
349
446
|
|
|
@@ -363,7 +460,7 @@ def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
|
|
|
363
460
|
if cax is not None and getattr(cax, '_bp_detached_mpl_colorbar', False):
|
|
364
461
|
return
|
|
365
462
|
|
|
366
|
-
#
|
|
463
|
+
# APPROACH 1: Try to find and disconnect the callback ID
|
|
367
464
|
cid = None
|
|
368
465
|
for attr in ('_cid', '_cid_colorbar', 'cid'):
|
|
369
466
|
try:
|
|
@@ -374,7 +471,6 @@ def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
|
|
|
374
471
|
except Exception:
|
|
375
472
|
pass
|
|
376
473
|
|
|
377
|
-
# Disconnect from the ScalarMappable callback registry.
|
|
378
474
|
if cid is not None:
|
|
379
475
|
try:
|
|
380
476
|
cbreg = getattr(im, 'callbacksSM', None)
|
|
@@ -383,7 +479,38 @@ def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
|
|
|
383
479
|
except Exception:
|
|
384
480
|
pass
|
|
385
481
|
|
|
386
|
-
#
|
|
482
|
+
# APPROACH 2: Disconnect ALL callbacks from the image (nuclear option)
|
|
483
|
+
try:
|
|
484
|
+
cbreg = getattr(im, 'callbacksSM', None)
|
|
485
|
+
if cbreg is not None:
|
|
486
|
+
# Try to clear all callbacks
|
|
487
|
+
if hasattr(cbreg, 'callbacks'):
|
|
488
|
+
try:
|
|
489
|
+
cbreg.callbacks.clear()
|
|
490
|
+
except Exception:
|
|
491
|
+
pass
|
|
492
|
+
# Also try the _signals dict if it exists
|
|
493
|
+
if hasattr(cbreg, '_signals'):
|
|
494
|
+
try:
|
|
495
|
+
for signal_dict in cbreg._signals.values():
|
|
496
|
+
if hasattr(signal_dict, 'clear'):
|
|
497
|
+
signal_dict.clear()
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
except Exception:
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
# APPROACH 3: Monkey-patch the update_normal method to be a no-op
|
|
504
|
+
# This is the most reliable approach for preventing the callback
|
|
505
|
+
try:
|
|
506
|
+
if hasattr(cbar, 'update_normal'):
|
|
507
|
+
def _noop_update(*args, **kwargs):
|
|
508
|
+
pass
|
|
509
|
+
cbar.update_normal = _noop_update
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
|
|
513
|
+
# APPROACH 4: Prevent future built-in updates by nulling internal state
|
|
387
514
|
try:
|
|
388
515
|
if hasattr(cbar, 'mappable'):
|
|
389
516
|
cbar.mappable = None
|
|
@@ -675,7 +802,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
675
802
|
|
|
676
803
|
if intensity_max > intensity_min:
|
|
677
804
|
# Update color limits
|
|
678
|
-
im
|
|
805
|
+
_safe_set_clim(im, intensity_min, intensity_max)
|
|
679
806
|
|
|
680
807
|
# Update colorbar if available
|
|
681
808
|
try:
|
|
@@ -1049,7 +1176,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1049
1176
|
clim = im.get_clim()
|
|
1050
1177
|
except Exception:
|
|
1051
1178
|
clim = None
|
|
1052
|
-
|
|
1179
|
+
# Get colormap name: first check if we stored it explicitly, otherwise try to get from colormap object
|
|
1180
|
+
cmap_name = getattr(im, '_operando_cmap_name', None)
|
|
1181
|
+
if cmap_name is None:
|
|
1182
|
+
cmap_name = getattr(im.get_cmap(), 'name', None)
|
|
1053
1183
|
# EC mode and caches (only if ec_ax exists)
|
|
1054
1184
|
if ec_ax is not None:
|
|
1055
1185
|
mode = getattr(ec_ax, '_ec_y_mode', 'time')
|
|
@@ -1263,12 +1393,15 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1263
1393
|
cbar.solids = None
|
|
1264
1394
|
except Exception:
|
|
1265
1395
|
pass
|
|
1266
|
-
lo, hi = snap['clim']; im
|
|
1396
|
+
lo, hi = snap['clim']; _safe_set_clim(im, float(lo), float(hi))
|
|
1267
1397
|
except Exception:
|
|
1268
1398
|
pass
|
|
1269
1399
|
try:
|
|
1270
1400
|
if snap.get('cmap'):
|
|
1271
|
-
|
|
1401
|
+
cmap_name = snap['cmap']
|
|
1402
|
+
im.set_cmap(cmap_name)
|
|
1403
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
1404
|
+
setattr(im, '_operando_cmap_name', cmap_name)
|
|
1272
1405
|
if cbar is not None:
|
|
1273
1406
|
_update_custom_colorbar(cbar.ax, im)
|
|
1274
1407
|
except Exception:
|
|
@@ -1733,7 +1866,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1733
1866
|
print("Crosshair OFF.")
|
|
1734
1867
|
while True:
|
|
1735
1868
|
try:
|
|
1736
|
-
cmd =
|
|
1869
|
+
cmd = _safe_input("Press a key: ").strip().lower()
|
|
1737
1870
|
except (KeyboardInterrupt, EOFError):
|
|
1738
1871
|
print("\n\nExiting interactive menu...")
|
|
1739
1872
|
break
|
|
@@ -1741,7 +1874,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1741
1874
|
continue
|
|
1742
1875
|
if cmd == 'q':
|
|
1743
1876
|
try:
|
|
1744
|
-
ans =
|
|
1877
|
+
ans = _safe_input("Quit interactive? Remember to save (e=export, s=save). Quit now? (y/n): ").strip().lower()
|
|
1745
1878
|
except Exception:
|
|
1746
1879
|
ans = 'y'
|
|
1747
1880
|
if ans == 'y':
|
|
@@ -1784,9 +1917,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1784
1917
|
|
|
1785
1918
|
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
1786
1919
|
if last_figure_path:
|
|
1787
|
-
fname =
|
|
1920
|
+
fname = _safe_input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
1788
1921
|
else:
|
|
1789
|
-
fname =
|
|
1922
|
+
fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
1790
1923
|
if not fname or fname.lower() == 'q':
|
|
1791
1924
|
print_menu(); continue
|
|
1792
1925
|
|
|
@@ -1799,7 +1932,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1799
1932
|
if not os.path.exists(last_figure_path):
|
|
1800
1933
|
print(f"Previous export file not found: {last_figure_path}")
|
|
1801
1934
|
print_menu(); continue
|
|
1802
|
-
yn =
|
|
1935
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
1803
1936
|
if yn != 'y':
|
|
1804
1937
|
print_menu(); continue
|
|
1805
1938
|
target = last_figure_path
|
|
@@ -1810,7 +1943,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1810
1943
|
idx = int(fname)
|
|
1811
1944
|
if 1 <= idx <= len(files):
|
|
1812
1945
|
name = files[idx-1]
|
|
1813
|
-
yn =
|
|
1946
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
1814
1947
|
if yn != 'y':
|
|
1815
1948
|
print_menu(); continue
|
|
1816
1949
|
target = file_list[idx-1][1] # Full path from list
|
|
@@ -1889,7 +2022,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1889
2022
|
cb_h_offset = getattr(cbar.ax, '_cb_h_offset_in', 0.0)
|
|
1890
2023
|
ec_h_offset = getattr(ec_ax, '_ec_h_offset_in', 0.0)
|
|
1891
2024
|
print(f"Toggle: 1=colorbar, 2=EC panel, 3=both, 4=colorbar label mode, 5=colorbar label text, m=move horizontal position (cb:{cb_h_offset:.3f}\", ec:{ec_h_offset:.3f}\"), q=cancel")
|
|
1892
|
-
choice =
|
|
2025
|
+
choice = _safe_input("v> ").strip().lower()
|
|
1893
2026
|
if choice == '1':
|
|
1894
2027
|
# Toggle colorbar
|
|
1895
2028
|
cb_vis = cbar.ax.get_visible()
|
|
@@ -1923,7 +2056,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1923
2056
|
# Change colorbar label text
|
|
1924
2057
|
current_label = getattr(cbar.ax, '_colorbar_label', 'Intensity')
|
|
1925
2058
|
print(f"Current colorbar label: {current_label}")
|
|
1926
|
-
new_label =
|
|
2059
|
+
new_label = _safe_input("New colorbar label (blank to keep): ").strip()
|
|
1927
2060
|
if new_label:
|
|
1928
2061
|
cbar.ax._colorbar_label = new_label
|
|
1929
2062
|
try:
|
|
@@ -1944,12 +2077,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1944
2077
|
if ec_ax is not None:
|
|
1945
2078
|
print(f" EC panel offset: {ec_h_offset:.3f}\" (positive=right, negative=left)")
|
|
1946
2079
|
print("Commands: c=colorbar, e=EC panel, q=back")
|
|
1947
|
-
sub =
|
|
2080
|
+
sub = _safe_input("m> ").strip().lower()
|
|
1948
2081
|
if not sub or sub == 'q':
|
|
1949
2082
|
break
|
|
1950
2083
|
if sub == 'c':
|
|
1951
2084
|
try:
|
|
1952
|
-
new_offset =
|
|
2085
|
+
new_offset = _safe_input(f"Enter colorbar horizontal offset in inches (current: {cb_h_offset:.3f}\"): ").strip()
|
|
1953
2086
|
if new_offset:
|
|
1954
2087
|
cb_h_offset = float(new_offset)
|
|
1955
2088
|
setattr(cbar.ax, '_cb_h_offset_in', cb_h_offset)
|
|
@@ -1957,16 +2090,20 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1957
2090
|
_apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
|
|
1958
2091
|
fig.canvas.draw_idle()
|
|
1959
2092
|
print(f"Colorbar horizontal offset set to {cb_h_offset:.3f}\"")
|
|
2093
|
+
# Continue in loop to show menu again
|
|
2094
|
+
continue
|
|
1960
2095
|
except ValueError:
|
|
1961
2096
|
print("Invalid input. Enter a number.")
|
|
2097
|
+
continue
|
|
1962
2098
|
except Exception as e:
|
|
1963
2099
|
print(f"Error: {e}")
|
|
2100
|
+
continue
|
|
1964
2101
|
elif sub == 'e':
|
|
1965
2102
|
if ec_ax is None:
|
|
1966
2103
|
print("EC panel not available.")
|
|
1967
2104
|
continue
|
|
1968
2105
|
try:
|
|
1969
|
-
new_offset =
|
|
2106
|
+
new_offset = _safe_input(f"Enter EC panel horizontal offset in inches (current: {ec_h_offset:.3f}\"): ").strip()
|
|
1970
2107
|
if new_offset:
|
|
1971
2108
|
ec_h_offset = float(new_offset)
|
|
1972
2109
|
setattr(ec_ax, '_ec_h_offset_in', ec_h_offset)
|
|
@@ -1974,19 +2111,24 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1974
2111
|
_apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
|
|
1975
2112
|
fig.canvas.draw_idle()
|
|
1976
2113
|
print(f"EC panel horizontal offset set to {ec_h_offset:.3f}\"")
|
|
2114
|
+
# Continue in loop to show menu again
|
|
2115
|
+
continue
|
|
1977
2116
|
except ValueError:
|
|
1978
2117
|
print("Invalid input. Enter a number.")
|
|
2118
|
+
continue
|
|
1979
2119
|
except Exception as e:
|
|
1980
2120
|
print(f"Error: {e}")
|
|
2121
|
+
continue
|
|
1981
2122
|
else:
|
|
1982
2123
|
print("Invalid choice.")
|
|
2124
|
+
continue
|
|
1983
2125
|
elif choice != 'q':
|
|
1984
2126
|
print("Invalid choice")
|
|
1985
2127
|
else:
|
|
1986
2128
|
# Operando-only mode: toggle colorbar or change label mode
|
|
1987
2129
|
cb_h_offset = getattr(cbar.ax, '_cb_h_offset_in', 0.0)
|
|
1988
2130
|
print(f"Toggle: 1=colorbar visibility, 2=colorbar label mode, 3=colorbar label text, m=move horizontal position (cb:{cb_h_offset:.3f}\"), q=cancel")
|
|
1989
|
-
choice =
|
|
2131
|
+
choice = _safe_input("v> ").strip().lower()
|
|
1990
2132
|
if choice == '1':
|
|
1991
2133
|
cb_vis = cbar.ax.get_visible()
|
|
1992
2134
|
cbar.ax.set_visible(not cb_vis)
|
|
@@ -2006,7 +2148,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2006
2148
|
# Change colorbar label text
|
|
2007
2149
|
current_label = getattr(cbar.ax, '_colorbar_label', 'Intensity')
|
|
2008
2150
|
print(f"Current colorbar label: {current_label}")
|
|
2009
|
-
new_label =
|
|
2151
|
+
new_label = _safe_input("New colorbar label (blank to keep): ").strip()
|
|
2010
2152
|
if new_label:
|
|
2011
2153
|
cbar.ax._colorbar_label = new_label
|
|
2012
2154
|
try:
|
|
@@ -2024,12 +2166,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2024
2166
|
print(f"\nHorizontal position (relative to canvas center):")
|
|
2025
2167
|
print(f" Colorbar offset: {cb_h_offset:.3f}\" (positive=right, negative=left)")
|
|
2026
2168
|
print("Commands: c=colorbar, q=back")
|
|
2027
|
-
sub =
|
|
2169
|
+
sub = _safe_input("m> ").strip().lower()
|
|
2028
2170
|
if not sub or sub == 'q':
|
|
2029
2171
|
break
|
|
2030
2172
|
if sub == 'c':
|
|
2031
2173
|
try:
|
|
2032
|
-
new_offset =
|
|
2174
|
+
new_offset = _safe_input(f"Enter colorbar horizontal offset in inches (current: {cb_h_offset:.3f}\"): ").strip()
|
|
2033
2175
|
if new_offset:
|
|
2034
2176
|
cb_h_offset = float(new_offset)
|
|
2035
2177
|
setattr(cbar.ax, '_cb_h_offset_in', cb_h_offset)
|
|
@@ -2037,12 +2179,17 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2037
2179
|
_apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
|
|
2038
2180
|
fig.canvas.draw_idle()
|
|
2039
2181
|
print(f"Colorbar horizontal offset set to {cb_h_offset:.3f}\"")
|
|
2182
|
+
# Continue in loop to show menu again
|
|
2183
|
+
continue
|
|
2040
2184
|
except ValueError:
|
|
2041
2185
|
print("Invalid input. Enter a number.")
|
|
2186
|
+
continue
|
|
2042
2187
|
except Exception as e:
|
|
2043
2188
|
print(f"Error: {e}")
|
|
2189
|
+
continue
|
|
2044
2190
|
else:
|
|
2045
2191
|
print("Invalid choice.")
|
|
2192
|
+
continue
|
|
2046
2193
|
elif choice != 'q':
|
|
2047
2194
|
print("Invalid choice")
|
|
2048
2195
|
fig.canvas.draw_idle()
|
|
@@ -2079,7 +2226,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2079
2226
|
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
2080
2227
|
else:
|
|
2081
2228
|
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
2082
|
-
choice =
|
|
2229
|
+
choice = _safe_input(prompt).strip()
|
|
2083
2230
|
if not choice or choice.lower() == 'q':
|
|
2084
2231
|
print_menu(); continue
|
|
2085
2232
|
if choice.lower() == 'o':
|
|
@@ -2090,7 +2237,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2090
2237
|
if not os.path.exists(last_session_path):
|
|
2091
2238
|
print(f"Previous save file not found: {last_session_path}")
|
|
2092
2239
|
print_menu(); continue
|
|
2093
|
-
yn =
|
|
2240
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2094
2241
|
if yn != 'y':
|
|
2095
2242
|
print_menu(); continue
|
|
2096
2243
|
dump_operando_session(last_session_path, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
|
|
@@ -2100,7 +2247,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2100
2247
|
idx = int(choice)
|
|
2101
2248
|
if 1 <= idx <= len(files):
|
|
2102
2249
|
name = files[idx-1]
|
|
2103
|
-
yn =
|
|
2250
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
2104
2251
|
if yn != 'y':
|
|
2105
2252
|
print_menu(); continue
|
|
2106
2253
|
target = os.path.join(folder, name)
|
|
@@ -2117,7 +2264,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2117
2264
|
name = name + '.pkl'
|
|
2118
2265
|
target = name if os.path.isabs(name) else os.path.join(folder, name)
|
|
2119
2266
|
if os.path.exists(target):
|
|
2120
|
-
yn =
|
|
2267
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2121
2268
|
if yn != 'y':
|
|
2122
2269
|
print_menu(); continue
|
|
2123
2270
|
dump_operando_session(target, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
|
|
@@ -2142,7 +2289,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2142
2289
|
# Always read fresh value from attribute to avoid stale cached value
|
|
2143
2290
|
ax_h_in = getattr(ax, '_fixed_ax_h_in', ax_h_in)
|
|
2144
2291
|
print(f"Current height: {ax_h_in:.2f} in")
|
|
2145
|
-
val =
|
|
2292
|
+
val = _safe_input("New height (inches): ").strip()
|
|
2146
2293
|
if val:
|
|
2147
2294
|
_snapshot("height")
|
|
2148
2295
|
try:
|
|
@@ -2182,7 +2329,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2182
2329
|
print(f"\nFont submenu (current: family='{cur_family}', size={cur_size})")
|
|
2183
2330
|
print(" f: change family | s: change size | q: back")
|
|
2184
2331
|
while True:
|
|
2185
|
-
sub =
|
|
2332
|
+
sub = _safe_input("Font> ").strip().lower()
|
|
2186
2333
|
if not sub:
|
|
2187
2334
|
continue
|
|
2188
2335
|
if sub == 'q':
|
|
@@ -2195,7 +2342,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2195
2342
|
for i, font in enumerate(fonts, 1):
|
|
2196
2343
|
print(f" {i}: {font}")
|
|
2197
2344
|
print("Or enter custom font name directly.")
|
|
2198
|
-
choice =
|
|
2345
|
+
choice = _safe_input("Font family (number or name): ").strip()
|
|
2199
2346
|
if not choice:
|
|
2200
2347
|
continue
|
|
2201
2348
|
_snapshot("font-family")
|
|
@@ -2215,7 +2362,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2215
2362
|
elif sub == 's':
|
|
2216
2363
|
# Show current size and accept direct input
|
|
2217
2364
|
cur_size = plt.rcParams.get('font.size', None)
|
|
2218
|
-
choice =
|
|
2365
|
+
choice = _safe_input(f"Font size (current: {cur_size}): ").strip()
|
|
2219
2366
|
if not choice:
|
|
2220
2367
|
continue
|
|
2221
2368
|
_snapshot("font-size")
|
|
@@ -2238,7 +2385,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2238
2385
|
print(_colorize_inline_commands(" 1.5 2.5 - set frame=1.5, ticks=2.5"))
|
|
2239
2386
|
print(_colorize_inline_commands(" q - cancel"))
|
|
2240
2387
|
|
|
2241
|
-
inp =
|
|
2388
|
+
inp = _safe_input("Line widths> ").strip().lower()
|
|
2242
2389
|
if not inp or inp == 'q':
|
|
2243
2390
|
print_menu()
|
|
2244
2391
|
continue
|
|
@@ -2301,6 +2448,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2301
2448
|
# Unified WASD ticks/labels/spines submenu for either pane
|
|
2302
2449
|
# Import here to avoid scoping issues with nested functions
|
|
2303
2450
|
from matplotlib.ticker import AutoMinorLocator, NullFormatter, MaxNLocator, NullLocator
|
|
2451
|
+
# Import UI positioning functions locally to ensure they're accessible in nested functions
|
|
2452
|
+
from .ui import position_top_xlabel as _ui_position_top_xlabel, position_bottom_xlabel as _ui_position_bottom_xlabel, position_left_ylabel as _ui_position_left_ylabel, position_right_ylabel as _ui_position_right_ylabel
|
|
2304
2453
|
|
|
2305
2454
|
def _get_tick_state(a):
|
|
2306
2455
|
# Unified keys with fallbacks for legacy combined flags
|
|
@@ -2386,7 +2535,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2386
2535
|
print(_colorize_inline_commands("Choose pane: o=operando, e=ec, q=back"))
|
|
2387
2536
|
else:
|
|
2388
2537
|
print(_colorize_inline_commands("Choose pane: o=operando, q=back"))
|
|
2389
|
-
pane =
|
|
2538
|
+
pane = _safe_input("ot> ").strip().lower()
|
|
2390
2539
|
if not pane:
|
|
2391
2540
|
continue
|
|
2392
2541
|
if pane == 'q':
|
|
@@ -2605,7 +2754,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2605
2754
|
print(_colorize_inline_commands("Type 'i' to invert tick direction, 'l' to change tick length, 'list' for state, 'q' to return."))
|
|
2606
2755
|
print(_colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
|
|
2607
2756
|
while True:
|
|
2608
|
-
cmd2 =
|
|
2757
|
+
cmd2 = _safe_input(_colorize_prompt("Toggle> ")).strip().lower()
|
|
2609
2758
|
if not cmd2:
|
|
2610
2759
|
continue
|
|
2611
2760
|
if cmd2 == 'q':
|
|
@@ -2630,7 +2779,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2630
2779
|
# Get current major tick length from axes
|
|
2631
2780
|
current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
|
|
2632
2781
|
print(f"Current major tick length: {current_major}")
|
|
2633
|
-
new_length_str =
|
|
2782
|
+
new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
|
|
2634
2783
|
if not new_length_str:
|
|
2635
2784
|
continue
|
|
2636
2785
|
new_major = float(new_length_str)
|
|
@@ -2723,7 +2872,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2723
2872
|
current_y_px = _px_value('_top_xlabel_manual_offset_y_pts', target)
|
|
2724
2873
|
current_x_px = _px_value('_top_xlabel_manual_offset_x_pts', target)
|
|
2725
2874
|
print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
|
|
2726
|
-
sub =
|
|
2875
|
+
sub = _safe_input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
2727
2876
|
if not sub:
|
|
2728
2877
|
continue
|
|
2729
2878
|
if sub == 'q':
|
|
@@ -2760,7 +2909,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2760
2909
|
while True:
|
|
2761
2910
|
current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts', target)
|
|
2762
2911
|
print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
|
|
2763
|
-
sub =
|
|
2912
|
+
sub = _safe_input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
|
|
2764
2913
|
if not sub:
|
|
2765
2914
|
continue
|
|
2766
2915
|
if sub == 'q':
|
|
@@ -2790,7 +2939,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2790
2939
|
while True:
|
|
2791
2940
|
current_x_px = _px_value('_left_ylabel_manual_offset_x_pts', target)
|
|
2792
2941
|
print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
|
|
2793
|
-
sub =
|
|
2942
|
+
sub = _safe_input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
2794
2943
|
if not sub:
|
|
2795
2944
|
continue
|
|
2796
2945
|
if sub == 'q':
|
|
@@ -2827,7 +2976,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2827
2976
|
current_x_px = _px_value('_right_ylabel_manual_offset_x_pts', target)
|
|
2828
2977
|
current_y_px = _px_value('_right_ylabel_manual_offset_y_pts', target)
|
|
2829
2978
|
print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
|
|
2830
|
-
sub =
|
|
2979
|
+
sub = _safe_input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
|
|
2831
2980
|
if not sub:
|
|
2832
2981
|
continue
|
|
2833
2982
|
if sub == 'q':
|
|
@@ -2866,7 +3015,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2866
3015
|
print(" " + _colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
|
|
2867
3016
|
print(" " + _colorize_menu('r : reset all offsets'))
|
|
2868
3017
|
print(" " + _colorize_menu('q : return'))
|
|
2869
|
-
choice =
|
|
3018
|
+
choice = _safe_input(_colorize_prompt("p> ")).strip().lower()
|
|
2870
3019
|
if not choice:
|
|
2871
3020
|
continue
|
|
2872
3021
|
if choice == 'q':
|
|
@@ -2982,7 +3131,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2982
3131
|
elif cmd == 'ox':
|
|
2983
3132
|
while True:
|
|
2984
3133
|
cur = ax.get_xlim(); print(f"Current operando X: {cur[0]:.4g} {cur[1]:.4g}")
|
|
2985
|
-
line =
|
|
3134
|
+
line = _safe_input("New X range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
2986
3135
|
if not line or line.lower() == 'q':
|
|
2987
3136
|
break
|
|
2988
3137
|
if line.lower() == 'w':
|
|
@@ -2990,7 +3139,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2990
3139
|
while True:
|
|
2991
3140
|
cur = ax.get_xlim()
|
|
2992
3141
|
print(f"Current operando X: {cur[0]:.4g} {cur[1]:.4g}")
|
|
2993
|
-
val =
|
|
3142
|
+
val = _safe_input(f"Enter new upper X limit (current lower: {cur[0]:.4g}, q=back): ").strip()
|
|
2994
3143
|
if not val or val.lower() == 'q':
|
|
2995
3144
|
break
|
|
2996
3145
|
try:
|
|
@@ -3009,7 +3158,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3009
3158
|
while True:
|
|
3010
3159
|
cur = ax.get_xlim()
|
|
3011
3160
|
print(f"Current operando X: {cur[0]:.4g} {cur[1]:.4g}")
|
|
3012
|
-
val =
|
|
3161
|
+
val = _safe_input(f"Enter new lower X limit (current upper: {cur[1]:.4g}, q=back): ").strip()
|
|
3013
3162
|
if not val or val.lower() == 'q':
|
|
3014
3163
|
break
|
|
3015
3164
|
try:
|
|
@@ -3055,7 +3204,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3055
3204
|
elif cmd == 'oy':
|
|
3056
3205
|
while True:
|
|
3057
3206
|
cur = ax.get_ylim(); print(f"Current operando Y: {cur[0]:.4g} {cur[1]:.4g}")
|
|
3058
|
-
line =
|
|
3207
|
+
line = _safe_input("New Y range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3059
3208
|
if not line or line.lower() == 'q':
|
|
3060
3209
|
break
|
|
3061
3210
|
if line.lower() == 'w':
|
|
@@ -3063,7 +3212,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3063
3212
|
while True:
|
|
3064
3213
|
cur = ax.get_ylim()
|
|
3065
3214
|
print(f"Current operando Y: {cur[0]:.4g} {cur[1]:.4g}")
|
|
3066
|
-
val =
|
|
3215
|
+
val = _safe_input(f"Enter new upper Y limit (current lower: {cur[0]:.4g}, q=back): ").strip()
|
|
3067
3216
|
if not val or val.lower() == 'q':
|
|
3068
3217
|
break
|
|
3069
3218
|
try:
|
|
@@ -3082,7 +3231,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3082
3231
|
while True:
|
|
3083
3232
|
cur = ax.get_ylim()
|
|
3084
3233
|
print(f"Current operando Y: {cur[0]:.4g} {cur[1]:.4g}")
|
|
3085
|
-
val =
|
|
3234
|
+
val = _safe_input(f"Enter new lower Y limit (current upper: {cur[1]:.4g}, q=back): ").strip()
|
|
3086
3235
|
if not val or val.lower() == 'q':
|
|
3087
3236
|
break
|
|
3088
3237
|
try:
|
|
@@ -3185,9 +3334,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3185
3334
|
auto_available = False
|
|
3186
3335
|
|
|
3187
3336
|
if auto_available:
|
|
3188
|
-
line =
|
|
3337
|
+
line = _safe_input("New intensity range (min max, w=upper only, s=lower only, a=auto-fit to visible, q=back): ").strip()
|
|
3189
3338
|
else:
|
|
3190
|
-
line =
|
|
3339
|
+
line = _safe_input("New intensity range (min max, w=upper only, s=lower only, q=back): ").strip()
|
|
3191
3340
|
|
|
3192
3341
|
if not line or line.lower() == 'q':
|
|
3193
3342
|
break
|
|
@@ -3201,7 +3350,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3201
3350
|
except Exception:
|
|
3202
3351
|
print("Could not retrieve current color scale range")
|
|
3203
3352
|
break
|
|
3204
|
-
val =
|
|
3353
|
+
val = _safe_input(f"Enter new upper intensity limit (current lower: {cur[0]:.4g}, q=back): ").strip()
|
|
3205
3354
|
if not val or val.lower() == 'q':
|
|
3206
3355
|
break
|
|
3207
3356
|
try:
|
|
@@ -3210,7 +3359,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3210
3359
|
print("Invalid value, ignored.")
|
|
3211
3360
|
continue
|
|
3212
3361
|
_snapshot("operando-intensity-range")
|
|
3213
|
-
im
|
|
3362
|
+
_safe_set_clim(im, cur[0], new_upper)
|
|
3214
3363
|
try:
|
|
3215
3364
|
if cbar is not None:
|
|
3216
3365
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3227,7 +3376,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3227
3376
|
except Exception:
|
|
3228
3377
|
print("Could not retrieve current color scale range")
|
|
3229
3378
|
break
|
|
3230
|
-
val =
|
|
3379
|
+
val = _safe_input(f"Enter new lower intensity limit (current upper: {cur[1]:.4g}, q=back): ").strip()
|
|
3231
3380
|
if not val or val.lower() == 'q':
|
|
3232
3381
|
break
|
|
3233
3382
|
try:
|
|
@@ -3236,7 +3385,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3236
3385
|
print("Invalid value, ignored.")
|
|
3237
3386
|
continue
|
|
3238
3387
|
_snapshot("operando-intensity-range")
|
|
3239
|
-
im
|
|
3388
|
+
_safe_set_clim(im, new_lower, cur[1])
|
|
3240
3389
|
try:
|
|
3241
3390
|
if cbar is not None:
|
|
3242
3391
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3250,7 +3399,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3250
3399
|
if line.lower() == 'a':
|
|
3251
3400
|
# Apply auto-normalization to visible data
|
|
3252
3401
|
if auto_available:
|
|
3253
|
-
im
|
|
3402
|
+
_safe_set_clim(im, auto_lo, auto_hi)
|
|
3254
3403
|
try:
|
|
3255
3404
|
if cbar is not None:
|
|
3256
3405
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3262,7 +3411,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3262
3411
|
print("Auto-fit unavailable: no finite data in visible area")
|
|
3263
3412
|
else:
|
|
3264
3413
|
lo, hi = map(float, line.split())
|
|
3265
|
-
im
|
|
3414
|
+
_safe_set_clim(im, lo, hi)
|
|
3266
3415
|
try:
|
|
3267
3416
|
if cbar is not None:
|
|
3268
3417
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3278,7 +3427,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3278
3427
|
while True:
|
|
3279
3428
|
ax_w_in = getattr(ax, '_fixed_ax_w_in', ax_w_in)
|
|
3280
3429
|
print(f"Current operando width: {ax_w_in:.2f} in")
|
|
3281
|
-
val =
|
|
3430
|
+
val = _safe_input("New width (inches, q=back): ").strip()
|
|
3282
3431
|
if not val or val.lower() == 'q':
|
|
3283
3432
|
break
|
|
3284
3433
|
_snapshot("operando-width")
|
|
@@ -3298,7 +3447,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3298
3447
|
while True:
|
|
3299
3448
|
ec_w_in = getattr(ec_ax, '_fixed_ec_w_in', ec_w_in)
|
|
3300
3449
|
print(f"Current EC width: {ec_w_in:.2f} in")
|
|
3301
|
-
val =
|
|
3450
|
+
val = _safe_input("New EC width (inches, q=back): ").strip()
|
|
3302
3451
|
if not val or val.lower() == 'q':
|
|
3303
3452
|
break
|
|
3304
3453
|
_snapshot("ec-width")
|
|
@@ -3311,6 +3460,15 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3311
3460
|
print_menu()
|
|
3312
3461
|
elif cmd == 'oc':
|
|
3313
3462
|
# Change operando colormap (perceptually uniform suggestions)
|
|
3463
|
+
# Show current colormap if one is applied
|
|
3464
|
+
try:
|
|
3465
|
+
current_cmap = getattr(im, '_operando_cmap_name', None)
|
|
3466
|
+
if current_cmap is None:
|
|
3467
|
+
current_cmap = getattr(im.get_cmap(), 'name', None)
|
|
3468
|
+
if current_cmap:
|
|
3469
|
+
print(f"Current operando colormap: {current_cmap}")
|
|
3470
|
+
except Exception:
|
|
3471
|
+
pass
|
|
3314
3472
|
def _refresh_available():
|
|
3315
3473
|
return set(name.lower() for name in plt.colormaps())
|
|
3316
3474
|
available = _refresh_available()
|
|
@@ -3341,7 +3499,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3341
3499
|
if optional:
|
|
3342
3500
|
print("\nOther available: " + ", ".join(optional))
|
|
3343
3501
|
print(_colorize_inline_commands("Append _r to reverse (e.g., viridis_r or 1_r). Blank to cancel."))
|
|
3344
|
-
choice =
|
|
3502
|
+
choice = _safe_input(f"Palette name or number (1-{len(rec_palettes)}): ").strip()
|
|
3345
3503
|
if not choice:
|
|
3346
3504
|
print_menu(); continue
|
|
3347
3505
|
try:
|
|
@@ -3383,6 +3541,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3383
3541
|
if reversed_choice:
|
|
3384
3542
|
palette_obj = palette_obj.reversed()
|
|
3385
3543
|
im.set_cmap(palette_obj)
|
|
3544
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
3545
|
+
setattr(im, '_operando_cmap_name', choice)
|
|
3386
3546
|
try:
|
|
3387
3547
|
# Update custom colorbar with new colormap
|
|
3388
3548
|
if cbar is not None:
|
|
@@ -3419,7 +3579,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3419
3579
|
cb_w_in, cb_gap_in, ec_gap_in, ec_w_in, ax_w_in, ax_h_in = _ensure_fixed_params(fig, ax, cbar.ax, ec_ax)
|
|
3420
3580
|
fam = plt.rcParams.get('font.sans-serif', [''])[0]
|
|
3421
3581
|
fsize = plt.rcParams.get('font.size', None)
|
|
3422
|
-
|
|
3582
|
+
# Get colormap name: first check if we stored it explicitly, otherwise try to get from colormap object
|
|
3583
|
+
cmap_name = getattr(im, '_operando_cmap_name', None)
|
|
3584
|
+
if cmap_name is None:
|
|
3585
|
+
cmap_name = getattr(im.get_cmap(), 'name', None)
|
|
3423
3586
|
cb_vis = bool(cbar.ax.get_visible())
|
|
3424
3587
|
ec_vis = bool(ec_ax.get_visible()) if ec_ax is not None else None
|
|
3425
3588
|
cb_label_text = str(getattr(cbar.ax, '_colorbar_label', cbar.ax.get_ylabel() or 'Intensity'))
|
|
@@ -3590,7 +3753,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3590
3753
|
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
3591
3754
|
if ec_ax is None:
|
|
3592
3755
|
print("\nNote: Style export (.bps/.bpsg) is only available in dual-pane mode (with EC file).")
|
|
3593
|
-
sub =
|
|
3756
|
+
sub = _safe_input("Style submenu: (q=return, r=refresh): ").strip().lower()
|
|
3594
3757
|
if sub == 'q':
|
|
3595
3758
|
break
|
|
3596
3759
|
if sub == 'r' or sub == '':
|
|
@@ -3600,9 +3763,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3600
3763
|
continue
|
|
3601
3764
|
else:
|
|
3602
3765
|
if last_style_path:
|
|
3603
|
-
sub =
|
|
3766
|
+
sub = _safe_input("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ").strip().lower()
|
|
3604
3767
|
else:
|
|
3605
|
-
sub =
|
|
3768
|
+
sub = _safe_input("Style submenu: (e=export, q=return, r=refresh): ").strip().lower()
|
|
3606
3769
|
if sub == 'q':
|
|
3607
3770
|
break
|
|
3608
3771
|
if sub == 'r' or sub == '':
|
|
@@ -3615,7 +3778,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3615
3778
|
if not os.path.exists(last_style_path):
|
|
3616
3779
|
print(f"Previous export file not found: {last_style_path}")
|
|
3617
3780
|
continue
|
|
3618
|
-
yn =
|
|
3781
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
3619
3782
|
if yn != 'y':
|
|
3620
3783
|
continue
|
|
3621
3784
|
# Determine export type from existing file and rebuild config
|
|
@@ -3636,7 +3799,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3636
3799
|
print("Export options:")
|
|
3637
3800
|
print(" ps = style only (.bps)")
|
|
3638
3801
|
print(" psg = style + geometry (.bpsg)")
|
|
3639
|
-
exp_choice =
|
|
3802
|
+
exp_choice = _safe_input("Export choice (ps/psg, q=cancel): ").strip().lower()
|
|
3640
3803
|
if not exp_choice or exp_choice == 'q':
|
|
3641
3804
|
print("Style export canceled.")
|
|
3642
3805
|
continue
|
|
@@ -3830,7 +3993,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3830
3993
|
else:
|
|
3831
3994
|
print(f" {_i}: {fname}")
|
|
3832
3995
|
|
|
3833
|
-
choice_name =
|
|
3996
|
+
choice_name = _safe_input("Enter new filename or number to overwrite (q=cancel): ").strip()
|
|
3834
3997
|
if not choice_name or choice_name.lower() == 'q':
|
|
3835
3998
|
print("Style export canceled.")
|
|
3836
3999
|
continue
|
|
@@ -3839,7 +4002,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3839
4002
|
_idx = int(choice_name)
|
|
3840
4003
|
if 1 <= _idx <= len(_style_files):
|
|
3841
4004
|
name = _style_files[_idx-1]
|
|
3842
|
-
yn =
|
|
4005
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
3843
4006
|
if yn == 'y':
|
|
3844
4007
|
target = file_list[_idx-1][1] # Full path from list
|
|
3845
4008
|
else:
|
|
@@ -3856,7 +4019,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3856
4019
|
else:
|
|
3857
4020
|
target = get_organized_path(name, 'style', base_path=save_base)
|
|
3858
4021
|
if os.path.exists(target):
|
|
3859
|
-
yn =
|
|
4022
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
3860
4023
|
if yn != 'y':
|
|
3861
4024
|
target = None
|
|
3862
4025
|
if target:
|
|
@@ -3986,6 +4149,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3986
4149
|
if cmap:
|
|
3987
4150
|
try:
|
|
3988
4151
|
im.set_cmap(cmap)
|
|
4152
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
4153
|
+
setattr(im, '_operando_cmap_name', cmap)
|
|
3989
4154
|
if cbar is not None:
|
|
3990
4155
|
_update_custom_colorbar(cbar.ax, im)
|
|
3991
4156
|
except Exception:
|
|
@@ -4259,7 +4424,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4259
4424
|
try:
|
|
4260
4425
|
intensity_range = op.get('intensity_range')
|
|
4261
4426
|
if intensity_range and isinstance(intensity_range, (list, tuple)) and len(intensity_range) == 2:
|
|
4262
|
-
im
|
|
4427
|
+
_safe_set_clim(im, float(intensity_range[0]), float(intensity_range[1]))
|
|
4263
4428
|
print(f"Applied intensity range: {intensity_range[0]:.4g} to {intensity_range[1]:.4g}")
|
|
4264
4429
|
except Exception as e:
|
|
4265
4430
|
print(f"Warning: Could not apply intensity range: {e}")
|
|
@@ -4553,16 +4718,18 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4553
4718
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
4554
4719
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
4555
4720
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
4721
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
4556
4722
|
while True:
|
|
4557
|
-
sub =
|
|
4723
|
+
sub = _safe_input("or> ").strip().lower()
|
|
4558
4724
|
if not sub:
|
|
4559
4725
|
continue
|
|
4560
4726
|
if sub == 'q':
|
|
4561
4727
|
break
|
|
4562
4728
|
if sub == 'x':
|
|
4563
4729
|
cur = ax.get_xlabel() or ''
|
|
4564
|
-
lab =
|
|
4730
|
+
lab = _safe_input(f"New operando X label (blank=cancel, current='{cur}'): ")
|
|
4565
4731
|
if lab:
|
|
4732
|
+
lab = convert_label_shortcuts(lab)
|
|
4566
4733
|
_snapshot("rename-op-x")
|
|
4567
4734
|
try:
|
|
4568
4735
|
ax.set_xlabel(lab)
|
|
@@ -4574,8 +4741,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4574
4741
|
pass
|
|
4575
4742
|
elif sub == 'y':
|
|
4576
4743
|
cur = ax.get_ylabel() or ''
|
|
4577
|
-
lab =
|
|
4744
|
+
lab = _safe_input(f"New operando Y label (blank=cancel, current='{cur}'): ")
|
|
4578
4745
|
if lab:
|
|
4746
|
+
lab = convert_label_shortcuts(lab)
|
|
4579
4747
|
_snapshot("rename-op-y")
|
|
4580
4748
|
try:
|
|
4581
4749
|
ax.set_ylabel(lab)
|
|
@@ -4605,16 +4773,18 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4605
4773
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
4606
4774
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
4607
4775
|
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
4776
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
4608
4777
|
while True:
|
|
4609
|
-
sub =
|
|
4778
|
+
sub = _safe_input("er> ").strip().lower()
|
|
4610
4779
|
if not sub:
|
|
4611
4780
|
continue
|
|
4612
4781
|
if sub == 'q':
|
|
4613
4782
|
break
|
|
4614
4783
|
if sub == 'x':
|
|
4615
4784
|
cur = ec_ax.get_xlabel() or ''
|
|
4616
|
-
lab =
|
|
4785
|
+
lab = _safe_input(f"New EC X label (blank=cancel, current='{cur}'): ")
|
|
4617
4786
|
if lab:
|
|
4787
|
+
lab = convert_label_shortcuts(lab)
|
|
4618
4788
|
_snapshot("rename-ec-x")
|
|
4619
4789
|
try:
|
|
4620
4790
|
ec_ax.set_xlabel(lab)
|
|
@@ -4625,8 +4795,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4625
4795
|
pass
|
|
4626
4796
|
elif sub == 'y':
|
|
4627
4797
|
cur = ec_ax.get_ylabel() or ''
|
|
4628
|
-
lab =
|
|
4798
|
+
lab = _safe_input(f"New EC Y label (blank=cancel, current='{cur}'): ")
|
|
4629
4799
|
if lab:
|
|
4800
|
+
lab = convert_label_shortcuts(lab)
|
|
4630
4801
|
_snapshot("rename-ec-y")
|
|
4631
4802
|
try:
|
|
4632
4803
|
ec_ax.set_ylabel(lab)
|
|
@@ -4663,7 +4834,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4663
4834
|
print_menu(); continue
|
|
4664
4835
|
print("EC line submenu: c=color, l=linewidth, q=back")
|
|
4665
4836
|
while True:
|
|
4666
|
-
sub =
|
|
4837
|
+
sub = _safe_input("el> ").strip().lower()
|
|
4667
4838
|
if not sub:
|
|
4668
4839
|
continue
|
|
4669
4840
|
if sub == 'q':
|
|
@@ -4679,7 +4850,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4679
4850
|
else:
|
|
4680
4851
|
print("\nNo saved colors. Type 'u' to manage saved colors.")
|
|
4681
4852
|
print(" (Enter color name/hex, saved color number, or 'u' to manage)")
|
|
4682
|
-
val =
|
|
4853
|
+
val = _safe_input(f"Color (current={cur}, blank=cancel): ").strip()
|
|
4683
4854
|
if not val:
|
|
4684
4855
|
continue
|
|
4685
4856
|
if val.lower() == 'u':
|
|
@@ -4696,7 +4867,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4696
4867
|
print(f"Invalid color: {e}")
|
|
4697
4868
|
elif sub == 'l':
|
|
4698
4869
|
cur = ln.get_linewidth()
|
|
4699
|
-
val =
|
|
4870
|
+
val = _safe_input(f"Line width (current={cur}, blank=cancel): ").strip()
|
|
4700
4871
|
if not val:
|
|
4701
4872
|
continue
|
|
4702
4873
|
_snapshot("ec-line-width")
|
|
@@ -4721,7 +4892,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4721
4892
|
continue
|
|
4722
4893
|
while True:
|
|
4723
4894
|
cur = ec_ax.get_ylim(); print(f"Current EC time range (Y): {cur[0]:.4g} {cur[1]:.4g}")
|
|
4724
|
-
line =
|
|
4895
|
+
line = _safe_input("New time range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
4725
4896
|
if not line or line.lower() == 'q':
|
|
4726
4897
|
break
|
|
4727
4898
|
if line.lower() == 'w':
|
|
@@ -4729,7 +4900,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4729
4900
|
while True:
|
|
4730
4901
|
cur = ec_ax.get_ylim()
|
|
4731
4902
|
print(f"Current EC time range (Y): {cur[0]:.4g} {cur[1]:.4g}")
|
|
4732
|
-
val =
|
|
4903
|
+
val = _safe_input(f"Enter new upper time limit (current lower: {cur[0]:.4g}, q=back): ").strip()
|
|
4733
4904
|
if not val or val.lower() == 'q':
|
|
4734
4905
|
break
|
|
4735
4906
|
try:
|
|
@@ -4747,7 +4918,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4747
4918
|
while True:
|
|
4748
4919
|
cur = ec_ax.get_ylim()
|
|
4749
4920
|
print(f"Current EC time range (Y): {cur[0]:.4g} {cur[1]:.4g}")
|
|
4750
|
-
val =
|
|
4921
|
+
val = _safe_input(f"Enter new lower time limit (current upper: {cur[1]:.4g}, q=back): ").strip()
|
|
4751
4922
|
if not val or val.lower() == 'q':
|
|
4752
4923
|
break
|
|
4753
4924
|
try:
|
|
@@ -4861,7 +5032,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4861
5032
|
print("The .mpt file must contain the '<I>/mA' column to use this feature.")
|
|
4862
5033
|
print_menu(); continue
|
|
4863
5034
|
while True:
|
|
4864
|
-
sub =
|
|
5035
|
+
sub = _safe_input("ey submenu: n=ions, t=time, q=back: ").strip().lower()
|
|
4865
5036
|
if not sub:
|
|
4866
5037
|
continue
|
|
4867
5038
|
if sub == 'q':
|
|
@@ -4878,7 +5049,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4878
5049
|
prompt = "Enter mass(mg), capacity-per-ion(mAh g^-1), start-ions (e.g. 4.5 26.8 0), q=cancel: "
|
|
4879
5050
|
else:
|
|
4880
5051
|
prompt = f"Enter mass,cap-per-ion,start-ions (blank=reuse {mass_mg} {cap_per_ion} {start_ions}; q=cancel): "
|
|
4881
|
-
s =
|
|
5052
|
+
s = _safe_input(prompt).strip()
|
|
4882
5053
|
if not s:
|
|
4883
5054
|
if need_input:
|
|
4884
5055
|
continue
|
|
@@ -5164,7 +5335,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
5164
5335
|
while True:
|
|
5165
5336
|
cur = ec_ax.get_xlim()
|
|
5166
5337
|
print(f"Current EC X range: {cur[0]:.4g} {cur[1]:.4g}")
|
|
5167
|
-
line =
|
|
5338
|
+
line = _safe_input("New EC X range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
5168
5339
|
if not line or line.lower() == 'q':
|
|
5169
5340
|
break
|
|
5170
5341
|
if line.lower() == 'w':
|
|
@@ -5172,7 +5343,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
5172
5343
|
while True:
|
|
5173
5344
|
cur = ec_ax.get_xlim()
|
|
5174
5345
|
print(f"Current EC X range: {cur[0]:.4g} {cur[1]:.4g}")
|
|
5175
|
-
val =
|
|
5346
|
+
val = _safe_input(f"Enter new upper EC X limit (current lower: {cur[0]:.4g}, q=back): ").strip()
|
|
5176
5347
|
if not val or val.lower() == 'q':
|
|
5177
5348
|
break
|
|
5178
5349
|
try:
|
|
@@ -5191,7 +5362,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
5191
5362
|
while True:
|
|
5192
5363
|
cur = ec_ax.get_xlim()
|
|
5193
5364
|
print(f"Current EC X range: {cur[0]:.4g} {cur[1]:.4g}")
|
|
5194
|
-
val =
|
|
5365
|
+
val = _safe_input(f"Enter new lower EC X limit (current upper: {cur[1]:.4g}, q=back): ").strip()
|
|
5195
5366
|
if not val or val.lower() == 'q':
|
|
5196
5367
|
break
|
|
5197
5368
|
try:
|
|
@@ -5253,7 +5424,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
5253
5424
|
cur_w, cur_h = _get_fig_size(fig)
|
|
5254
5425
|
print(f"Current canvas size: {cur_w:.2f} x {cur_h:.2f} in (W x H)")
|
|
5255
5426
|
print("Canvas: only figure size will change; panel widths/gaps are not altered.")
|
|
5256
|
-
line =
|
|
5427
|
+
line = _safe_input("New canvas size 'W H' (blank=cancel): ").strip()
|
|
5257
5428
|
if line:
|
|
5258
5429
|
_snapshot("canvas-size")
|
|
5259
5430
|
try:
|