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
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import Dict, Iterable, List, Optional, Tuple
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
|
+
import sys
|
|
12
13
|
|
|
13
14
|
import matplotlib.pyplot as plt
|
|
14
15
|
import matplotlib.cm as cm
|
|
@@ -30,6 +31,7 @@ from .utils import (
|
|
|
30
31
|
choose_style_file,
|
|
31
32
|
list_files_in_subdirectory,
|
|
32
33
|
get_organized_path,
|
|
34
|
+
convert_label_shortcuts,
|
|
33
35
|
)
|
|
34
36
|
import time
|
|
35
37
|
from .color_utils import (
|
|
@@ -42,6 +44,37 @@ from .color_utils import (
|
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
class _FilterIMKWarning:
|
|
48
|
+
"""Filter that suppresses macOS IMKCFRunLoopWakeUpReliable warnings while preserving other errors."""
|
|
49
|
+
def __init__(self, original_stderr):
|
|
50
|
+
self.original_stderr = original_stderr
|
|
51
|
+
|
|
52
|
+
def write(self, message):
|
|
53
|
+
# Filter out the harmless macOS IMK warning
|
|
54
|
+
if 'IMKCFRunLoopWakeUpReliable' not in message:
|
|
55
|
+
self.original_stderr.write(message)
|
|
56
|
+
|
|
57
|
+
def flush(self):
|
|
58
|
+
self.original_stderr.flush()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _safe_input(prompt: str = "") -> str:
|
|
62
|
+
"""Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
|
|
63
|
+
|
|
64
|
+
This is a harmless macOS system message that appears when using input() in terminals.
|
|
65
|
+
"""
|
|
66
|
+
# Filter stderr to hide macOS IMK warnings while preserving other errors
|
|
67
|
+
original_stderr = sys.stderr
|
|
68
|
+
sys.stderr = _FilterIMKWarning(original_stderr)
|
|
69
|
+
try:
|
|
70
|
+
result = input(prompt)
|
|
71
|
+
return result
|
|
72
|
+
except (KeyboardInterrupt, EOFError):
|
|
73
|
+
raise
|
|
74
|
+
finally:
|
|
75
|
+
sys.stderr = original_stderr
|
|
76
|
+
|
|
77
|
+
|
|
45
78
|
def _colorize_menu(text):
|
|
46
79
|
"""Colorize menu items: command in cyan, colon in white, description in default."""
|
|
47
80
|
if ':' not in text:
|
|
@@ -470,6 +503,28 @@ def _rebuild_legend(ax):
|
|
|
470
503
|
fig = ax.figure
|
|
471
504
|
# Capture existing title before any rebuild so it isn't lost
|
|
472
505
|
_store_legend_title(fig, ax)
|
|
506
|
+
# If no stored position yet, try to capture the current legend location once
|
|
507
|
+
# so rebuilds (e.g., after renaming) don't jump to a new "best" spot.
|
|
508
|
+
try:
|
|
509
|
+
if getattr(fig, '_ec_legend_xy_in', None) is None:
|
|
510
|
+
leg0 = ax.get_legend()
|
|
511
|
+
if leg0 is not None and leg0.get_visible():
|
|
512
|
+
try:
|
|
513
|
+
renderer = fig.canvas.get_renderer()
|
|
514
|
+
except Exception:
|
|
515
|
+
fig.canvas.draw()
|
|
516
|
+
renderer = fig.canvas.get_renderer()
|
|
517
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
518
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
519
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
520
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
521
|
+
fw, fh = fig.get_size_inches()
|
|
522
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
523
|
+
offset = _sanitize_legend_offset(fig, offset)
|
|
524
|
+
if offset is not None:
|
|
525
|
+
fig._ec_legend_xy_in = offset
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
473
528
|
if not _get_legend_user_pref(fig):
|
|
474
529
|
leg = ax.get_legend()
|
|
475
530
|
if leg is not None:
|
|
@@ -937,6 +992,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
937
992
|
|
|
938
993
|
def _title_offset_menu():
|
|
939
994
|
"""Allow nudging duplicate top/right titles by single-pixel increments."""
|
|
995
|
+
# Import UI positioning functions locally to ensure they're accessible in nested functions
|
|
996
|
+
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
|
|
997
|
+
|
|
940
998
|
def _dpi():
|
|
941
999
|
try:
|
|
942
1000
|
return float(fig.dpi)
|
|
@@ -980,7 +1038,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
980
1038
|
current_y_px = _px_value('_top_xlabel_manual_offset_y_pts')
|
|
981
1039
|
current_x_px = _px_value('_top_xlabel_manual_offset_x_pts')
|
|
982
1040
|
print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
|
|
983
|
-
sub =
|
|
1041
|
+
sub = _safe_input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
984
1042
|
if not sub:
|
|
985
1043
|
continue
|
|
986
1044
|
if sub == 'q':
|
|
@@ -1018,7 +1076,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1018
1076
|
current_x_px = _px_value('_right_ylabel_manual_offset_x_pts')
|
|
1019
1077
|
current_y_px = _px_value('_right_ylabel_manual_offset_y_pts')
|
|
1020
1078
|
print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
|
|
1021
|
-
sub =
|
|
1079
|
+
sub = _safe_input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
|
|
1022
1080
|
if not sub:
|
|
1023
1081
|
continue
|
|
1024
1082
|
if sub == 'q':
|
|
@@ -1055,7 +1113,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1055
1113
|
while True:
|
|
1056
1114
|
current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts')
|
|
1057
1115
|
print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
|
|
1058
|
-
sub =
|
|
1116
|
+
sub = _safe_input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
|
|
1059
1117
|
if not sub:
|
|
1060
1118
|
continue
|
|
1061
1119
|
if sub == 'q':
|
|
@@ -1085,7 +1143,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1085
1143
|
while True:
|
|
1086
1144
|
current_x_px = _px_value('_left_ylabel_manual_offset_x_pts')
|
|
1087
1145
|
print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
|
|
1088
|
-
sub =
|
|
1146
|
+
sub = _safe_input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
1089
1147
|
if not sub:
|
|
1090
1148
|
continue
|
|
1091
1149
|
if sub == 'q':
|
|
@@ -1116,7 +1174,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1116
1174
|
print(" " + _colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
|
|
1117
1175
|
print(" " + _colorize_menu('r : reset all offsets'))
|
|
1118
1176
|
print(" " + _colorize_menu('q : return'))
|
|
1119
|
-
choice =
|
|
1177
|
+
choice = _safe_input(_colorize_prompt("p> ")).strip().lower()
|
|
1120
1178
|
if not choice:
|
|
1121
1179
|
continue
|
|
1122
1180
|
if choice == 'q':
|
|
@@ -1587,7 +1645,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1587
1645
|
_print_menu(len(all_cycles), is_dqdv)
|
|
1588
1646
|
while True:
|
|
1589
1647
|
try:
|
|
1590
|
-
key =
|
|
1648
|
+
key = _safe_input("Press a key: ").strip().lower()
|
|
1591
1649
|
except (KeyboardInterrupt, EOFError):
|
|
1592
1650
|
print("\n\nExiting interactive menu...")
|
|
1593
1651
|
break
|
|
@@ -1595,7 +1653,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1595
1653
|
continue
|
|
1596
1654
|
if key == 'q':
|
|
1597
1655
|
try:
|
|
1598
|
-
confirm =
|
|
1656
|
+
confirm = _safe_input(_colorize_prompt("Quit EC interactive? Remember to save (e=export, s=save). Quit now? (y/n): ")).strip().lower()
|
|
1599
1657
|
except Exception:
|
|
1600
1658
|
confirm = 'y'
|
|
1601
1659
|
if confirm == 'y':
|
|
@@ -1630,9 +1688,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1630
1688
|
|
|
1631
1689
|
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
1632
1690
|
if last_figure_path:
|
|
1633
|
-
fname =
|
|
1691
|
+
fname = _safe_input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
1634
1692
|
else:
|
|
1635
|
-
fname =
|
|
1693
|
+
fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
1636
1694
|
if not fname or fname.lower() == 'q':
|
|
1637
1695
|
_print_menu(len(all_cycles), is_dqdv)
|
|
1638
1696
|
continue
|
|
@@ -1648,7 +1706,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1648
1706
|
print(f"Previous export file not found: {last_figure_path}")
|
|
1649
1707
|
_print_menu(len(all_cycles), is_dqdv)
|
|
1650
1708
|
continue
|
|
1651
|
-
yn =
|
|
1709
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
1652
1710
|
if yn != 'y':
|
|
1653
1711
|
_print_menu(len(all_cycles), is_dqdv)
|
|
1654
1712
|
continue
|
|
@@ -1660,7 +1718,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1660
1718
|
idx = int(fname)
|
|
1661
1719
|
if 1 <= idx <= len(files):
|
|
1662
1720
|
name = files[idx-1]
|
|
1663
|
-
yn =
|
|
1721
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
1664
1722
|
if yn != 'y':
|
|
1665
1723
|
_print_menu(len(all_cycles), is_dqdv)
|
|
1666
1724
|
continue
|
|
@@ -1798,7 +1856,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1798
1856
|
xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
|
|
1799
1857
|
print(f"Legend is {'ON' if vis else 'off'}; position (inches from center): x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
1800
1858
|
while True:
|
|
1801
|
-
sub =
|
|
1859
|
+
sub = _safe_input(_colorize_prompt("Legend: (t=toggle, p=set position, q=back): ")).strip().lower()
|
|
1802
1860
|
if not sub:
|
|
1803
1861
|
continue
|
|
1804
1862
|
if sub == 'q':
|
|
@@ -1822,7 +1880,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1822
1880
|
while True:
|
|
1823
1881
|
xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
|
|
1824
1882
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
1825
|
-
pos_cmd =
|
|
1883
|
+
pos_cmd = _safe_input(_colorize_prompt("Position: (x y) or x=x only, y=y only, q=back: ")).strip().lower()
|
|
1826
1884
|
if not pos_cmd or pos_cmd == 'q':
|
|
1827
1885
|
break
|
|
1828
1886
|
if pos_cmd == 'x':
|
|
@@ -1830,7 +1888,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1830
1888
|
while True:
|
|
1831
1889
|
xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
|
|
1832
1890
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
1833
|
-
val =
|
|
1891
|
+
val = _safe_input(f"Enter new x position (current y: {xy_in[1]:.2f}, q=back): ").strip()
|
|
1834
1892
|
if not val or val.lower() == 'q':
|
|
1835
1893
|
break
|
|
1836
1894
|
try:
|
|
@@ -1857,7 +1915,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1857
1915
|
while True:
|
|
1858
1916
|
xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
|
|
1859
1917
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
1860
|
-
val =
|
|
1918
|
+
val = _safe_input(f"Enter new y position (current x: {xy_in[0]:.2f}, q=back): ").strip()
|
|
1861
1919
|
if not val or val.lower() == 'q':
|
|
1862
1920
|
break
|
|
1863
1921
|
try:
|
|
@@ -1932,9 +1990,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1932
1990
|
|
|
1933
1991
|
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
1934
1992
|
if last_style_path:
|
|
1935
|
-
sub =
|
|
1993
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
1936
1994
|
else:
|
|
1937
|
-
sub =
|
|
1995
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
1938
1996
|
if sub == 'q':
|
|
1939
1997
|
break
|
|
1940
1998
|
if sub == 'r' or sub == '':
|
|
@@ -1947,7 +2005,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1947
2005
|
if not os.path.exists(last_style_path):
|
|
1948
2006
|
print(f"Previous export file not found: {last_style_path}")
|
|
1949
2007
|
continue
|
|
1950
|
-
yn =
|
|
2008
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
1951
2009
|
if yn != 'y':
|
|
1952
2010
|
continue
|
|
1953
2011
|
# Rebuild config based on current state
|
|
@@ -1974,7 +2032,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1974
2032
|
print("Export options:")
|
|
1975
2033
|
print(" ps = style only (.bps)")
|
|
1976
2034
|
print(" psg = style + geometry (.bpsg)")
|
|
1977
|
-
exp_choice =
|
|
2035
|
+
exp_choice = _safe_input(_colorize_prompt("Export choice (ps/psg, q=cancel): ")).strip().lower()
|
|
1978
2036
|
if not exp_choice or exp_choice == 'q':
|
|
1979
2037
|
print("Style export canceled.")
|
|
1980
2038
|
continue
|
|
@@ -2426,13 +2484,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2426
2484
|
print(f" {_colorize_menu('ld : show line and dots (markers) for all curves')}")
|
|
2427
2485
|
print(f" {_colorize_menu('d : show only dots (no connecting line) for all curves')}")
|
|
2428
2486
|
print(f" {_colorize_menu('q : return')}")
|
|
2429
|
-
sub =
|
|
2487
|
+
sub = _safe_input(_colorize_prompt("Choose (c/f/g/l/ld/d/q): ")).strip().lower()
|
|
2430
2488
|
if not sub:
|
|
2431
2489
|
continue
|
|
2432
2490
|
if sub == 'q':
|
|
2433
2491
|
break
|
|
2434
2492
|
if sub == 'c':
|
|
2435
|
-
spec =
|
|
2493
|
+
spec = _safe_input("Curve linewidth (single value for all curves, q=cancel): ").strip()
|
|
2436
2494
|
if not spec or spec.lower() == 'q':
|
|
2437
2495
|
continue
|
|
2438
2496
|
# Apply single width to all curves
|
|
@@ -2461,7 +2519,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2461
2519
|
except ValueError:
|
|
2462
2520
|
print("Invalid width value.")
|
|
2463
2521
|
elif sub == 'f':
|
|
2464
|
-
fw_in =
|
|
2522
|
+
fw_in = _safe_input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
|
|
2465
2523
|
if not fw_in or fw_in.lower() == 'q':
|
|
2466
2524
|
print("Canceled.")
|
|
2467
2525
|
continue
|
|
@@ -2536,7 +2594,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2536
2594
|
# Line + dots for all curves
|
|
2537
2595
|
push_state("line+dots")
|
|
2538
2596
|
try:
|
|
2539
|
-
msize_in =
|
|
2597
|
+
msize_in = _safe_input("Marker size (blank=auto ~3*lw): ").strip()
|
|
2540
2598
|
custom_msize = float(msize_in) if msize_in else None
|
|
2541
2599
|
except ValueError:
|
|
2542
2600
|
custom_msize = None
|
|
@@ -2566,7 +2624,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2566
2624
|
# Dots only for all curves
|
|
2567
2625
|
push_state("dots-only")
|
|
2568
2626
|
try:
|
|
2569
|
-
msize_in =
|
|
2627
|
+
msize_in = _safe_input("Marker size (blank=auto ~3*lw): ").strip()
|
|
2570
2628
|
custom_msize = float(msize_in) if msize_in else None
|
|
2571
2629
|
except ValueError:
|
|
2572
2630
|
custom_msize = None
|
|
@@ -2613,7 +2671,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2613
2671
|
print(f" {idx}: {color_block(color)} {color}")
|
|
2614
2672
|
print("Type 'u' to edit saved colors.")
|
|
2615
2673
|
print("q: back to main menu")
|
|
2616
|
-
line =
|
|
2674
|
+
line = _safe_input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
|
|
2617
2675
|
if not line or line.lower() == 'q':
|
|
2618
2676
|
break
|
|
2619
2677
|
if line.lower() == 'u':
|
|
@@ -2662,16 +2720,18 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2662
2720
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2663
2721
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2664
2722
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2723
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2665
2724
|
while True:
|
|
2666
2725
|
print("Rename axis: x, y, both, q=back")
|
|
2667
|
-
sub =
|
|
2726
|
+
sub = _safe_input("Rename> ").strip().lower()
|
|
2668
2727
|
if not sub:
|
|
2669
2728
|
continue
|
|
2670
2729
|
if sub == 'q':
|
|
2671
2730
|
break
|
|
2672
2731
|
if sub in ('x','both'):
|
|
2673
|
-
txt =
|
|
2732
|
+
txt = _safe_input("New X-axis label (blank=cancel): ")
|
|
2674
2733
|
if txt:
|
|
2734
|
+
txt = convert_label_shortcuts(txt)
|
|
2675
2735
|
push_state("rename-x")
|
|
2676
2736
|
try:
|
|
2677
2737
|
# Freeze layout and preserve existing pad for one-shot restore
|
|
@@ -2693,8 +2753,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2693
2753
|
except Exception:
|
|
2694
2754
|
pass
|
|
2695
2755
|
if sub in ('y','both'):
|
|
2696
|
-
txt =
|
|
2756
|
+
txt = _safe_input("New Y-axis label (blank=cancel): ")
|
|
2697
2757
|
if txt:
|
|
2758
|
+
txt = convert_label_shortcuts(txt)
|
|
2698
2759
|
push_state("rename-y")
|
|
2699
2760
|
base_ylabel = txt
|
|
2700
2761
|
try:
|
|
@@ -2829,7 +2890,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2829
2890
|
print(_colorize_inline_commands(" 1=spine 2=ticks 3=minor ticks 4=tick labels 5=axis title"))
|
|
2830
2891
|
print(_colorize_inline_commands("Type 'i' to invert tick direction, 'l' to change tick length, 'list' for state, 'q' to return."))
|
|
2831
2892
|
print(_colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
|
|
2832
|
-
cmd =
|
|
2893
|
+
cmd = _safe_input(_colorize_prompt("t> ")).strip().lower()
|
|
2833
2894
|
if not cmd:
|
|
2834
2895
|
continue
|
|
2835
2896
|
if cmd == 'q':
|
|
@@ -2856,7 +2917,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2856
2917
|
# Get current major tick length from axes
|
|
2857
2918
|
current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
|
|
2858
2919
|
print(f"Current major tick length: {current_major}")
|
|
2859
|
-
new_length_str =
|
|
2920
|
+
new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
|
|
2860
2921
|
if not new_length_str:
|
|
2861
2922
|
continue
|
|
2862
2923
|
new_major = float(new_length_str)
|
|
@@ -2944,7 +3005,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2944
3005
|
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
2945
3006
|
else:
|
|
2946
3007
|
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
2947
|
-
choice =
|
|
3008
|
+
choice = _safe_input(prompt).strip()
|
|
2948
3009
|
if not choice or choice.lower() == 'q':
|
|
2949
3010
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2950
3011
|
if choice.lower() == 'o':
|
|
@@ -2955,7 +3016,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2955
3016
|
if not os.path.exists(last_session_path):
|
|
2956
3017
|
print(f"Previous save file not found: {last_session_path}")
|
|
2957
3018
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2958
|
-
yn =
|
|
3019
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2959
3020
|
if yn != 'y':
|
|
2960
3021
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2961
3022
|
dump_ec_session(last_session_path, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
|
|
@@ -2965,7 +3026,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2965
3026
|
idx = int(choice)
|
|
2966
3027
|
if 1 <= idx <= len(files):
|
|
2967
3028
|
name = files[idx-1]
|
|
2968
|
-
yn =
|
|
3029
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
2969
3030
|
if yn != 'y':
|
|
2970
3031
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2971
3032
|
target = os.path.join(folder, name)
|
|
@@ -2982,7 +3043,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2982
3043
|
name = name + '.pkl'
|
|
2983
3044
|
target = name if os.path.isabs(name) else os.path.join(folder, name)
|
|
2984
3045
|
if os.path.exists(target):
|
|
2985
|
-
yn =
|
|
3046
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2986
3047
|
if yn != 'y':
|
|
2987
3048
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2988
3049
|
dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
|
|
@@ -2992,6 +3053,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2992
3053
|
_print_menu(len(all_cycles), is_dqdv)
|
|
2993
3054
|
continue
|
|
2994
3055
|
elif key == 'c':
|
|
3056
|
+
# Show current palette if one is applied (this is informational only)
|
|
3057
|
+
# Note: Individual cycles may use different colors, so we can't show a single "current" palette
|
|
2995
3058
|
print(f"Total cycles: {len(all_cycles)}")
|
|
2996
3059
|
print("Enter one of:")
|
|
2997
3060
|
print(_colorize_inline_commands(" - numbers: e.g. 1 5 10"))
|
|
@@ -3018,7 +3081,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3018
3081
|
for idx, color in enumerate(user_colors, 1):
|
|
3019
3082
|
print(f" {idx}: {color_block(color)} {color}")
|
|
3020
3083
|
print("Type 'u' to edit saved colors before assigning.")
|
|
3021
|
-
line =
|
|
3084
|
+
line = _safe_input("Selection: ").strip()
|
|
3022
3085
|
if not line:
|
|
3023
3086
|
continue
|
|
3024
3087
|
if line.lower() == 'u':
|
|
@@ -3163,14 +3226,14 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3163
3226
|
# X-axis submenu: number-of-ions vs capacity
|
|
3164
3227
|
while True:
|
|
3165
3228
|
print("X-axis menu: n=number of ions, c=capacity, q=back")
|
|
3166
|
-
sub =
|
|
3229
|
+
sub = _safe_input("X> ").strip().lower()
|
|
3167
3230
|
if not sub:
|
|
3168
3231
|
continue
|
|
3169
3232
|
if sub == 'q':
|
|
3170
3233
|
break
|
|
3171
3234
|
if sub == 'n':
|
|
3172
3235
|
print("Input the theoretical capacity per 1 active ion (mAh g^-1), e.g., 125")
|
|
3173
|
-
val =
|
|
3236
|
+
val = _safe_input("C_theoretical_per_ion: ").strip()
|
|
3174
3237
|
try:
|
|
3175
3238
|
c_th = float(val)
|
|
3176
3239
|
if c_th <= 0:
|
|
@@ -3307,7 +3370,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3307
3370
|
cur_size = plt.rcParams.get('font.size', None)
|
|
3308
3371
|
while True:
|
|
3309
3372
|
print(f"\nFont menu (current: family='{cur_family}', size={cur_size}): f=font family, s=size, q=back")
|
|
3310
|
-
sub =
|
|
3373
|
+
sub = _safe_input("Font> ").strip().lower()
|
|
3311
3374
|
if not sub:
|
|
3312
3375
|
continue
|
|
3313
3376
|
if sub == 'q':
|
|
@@ -3320,7 +3383,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3320
3383
|
for i, font in enumerate(fonts, 1):
|
|
3321
3384
|
print(f" {i}: {font}")
|
|
3322
3385
|
print("Or enter custom font name directly.")
|
|
3323
|
-
choice =
|
|
3386
|
+
choice = _safe_input(f"Font family (current: '{cur_family}', number or name): ").strip()
|
|
3324
3387
|
if not choice:
|
|
3325
3388
|
continue
|
|
3326
3389
|
# Check if it's a number
|
|
@@ -3352,7 +3415,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3352
3415
|
# Show current size and accept direct input
|
|
3353
3416
|
import matplotlib as mpl
|
|
3354
3417
|
cur_size = mpl.rcParams.get('font.size', None)
|
|
3355
|
-
choice =
|
|
3418
|
+
choice = _safe_input(f"Font size (current: {cur_size}): ").strip()
|
|
3356
3419
|
if not choice:
|
|
3357
3420
|
continue
|
|
3358
3421
|
try:
|
|
@@ -3377,7 +3440,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3377
3440
|
while True:
|
|
3378
3441
|
current_xlim = ax.get_xlim()
|
|
3379
3442
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3380
|
-
lim =
|
|
3443
|
+
lim = _safe_input("Set X limits (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3381
3444
|
if not lim or lim.lower() == 'q':
|
|
3382
3445
|
break
|
|
3383
3446
|
if lim.lower() == 'a':
|
|
@@ -3399,7 +3462,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3399
3462
|
while True:
|
|
3400
3463
|
current_xlim = ax.get_xlim()
|
|
3401
3464
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3402
|
-
val =
|
|
3465
|
+
val = _safe_input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
|
|
3403
3466
|
if not val or val.lower() == 'q':
|
|
3404
3467
|
break
|
|
3405
3468
|
try:
|
|
@@ -3425,7 +3488,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3425
3488
|
while True:
|
|
3426
3489
|
current_xlim = ax.get_xlim()
|
|
3427
3490
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3428
|
-
val =
|
|
3491
|
+
val = _safe_input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
|
|
3429
3492
|
if not val or val.lower() == 'q':
|
|
3430
3493
|
break
|
|
3431
3494
|
try:
|
|
@@ -3461,7 +3524,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3461
3524
|
while True:
|
|
3462
3525
|
current_ylim = ax.get_ylim()
|
|
3463
3526
|
print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3464
|
-
lim =
|
|
3527
|
+
lim = _safe_input("Set Y limits (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3465
3528
|
if not lim or lim.lower() == 'q':
|
|
3466
3529
|
break
|
|
3467
3530
|
if lim.lower() == 'a':
|
|
@@ -3483,7 +3546,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3483
3546
|
while True:
|
|
3484
3547
|
current_ylim = ax.get_ylim()
|
|
3485
3548
|
print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3486
|
-
val =
|
|
3549
|
+
val = _safe_input(f"Enter new upper Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
3487
3550
|
if not val or val.lower() == 'q':
|
|
3488
3551
|
break
|
|
3489
3552
|
try:
|
|
@@ -3509,7 +3572,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3509
3572
|
while True:
|
|
3510
3573
|
current_ylim = ax.get_ylim()
|
|
3511
3574
|
print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3512
|
-
val =
|
|
3575
|
+
val = _safe_input(f"Enter new lower Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3513
3576
|
if not val or val.lower() == 'q':
|
|
3514
3577
|
break
|
|
3515
3578
|
try:
|
|
@@ -3544,7 +3607,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3544
3607
|
# Geometry submenu: plot frame vs canvas (scales moved to separate keys)
|
|
3545
3608
|
while True:
|
|
3546
3609
|
print("Geometry menu: p=plot frame size, c=canvas size, q=back")
|
|
3547
|
-
sub =
|
|
3610
|
+
sub = _safe_input("Geom> ").strip().lower()
|
|
3548
3611
|
if not sub:
|
|
3549
3612
|
continue
|
|
3550
3613
|
if sub == 'q':
|
|
@@ -3583,7 +3646,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3583
3646
|
print(" o: remove outliers (removes abrupt dQ/dV spikes)")
|
|
3584
3647
|
print(" r: reset to original data")
|
|
3585
3648
|
print(" q: back to main menu")
|
|
3586
|
-
sub =
|
|
3649
|
+
sub = _safe_input("sm> ").strip().lower()
|
|
3587
3650
|
if not sub:
|
|
3588
3651
|
continue
|
|
3589
3652
|
if sub == 'q':
|
|
@@ -3618,7 +3681,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3618
3681
|
if sub == 'a':
|
|
3619
3682
|
try:
|
|
3620
3683
|
while True:
|
|
3621
|
-
threshold_input =
|
|
3684
|
+
threshold_input = _safe_input("Enter minimum voltage step in mV (default 0.5 mV, 'q'=quit, 'e'=explain): ").strip()
|
|
3622
3685
|
if threshold_input.lower() == 'q':
|
|
3623
3686
|
break
|
|
3624
3687
|
if threshold_input.lower() == 'e':
|
|
@@ -3692,7 +3755,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3692
3755
|
try:
|
|
3693
3756
|
print("DiffCap smoothing per Thompson et al. (2020): clean ΔV < threshold and apply Savitzky–Golay (order 3).")
|
|
3694
3757
|
while True:
|
|
3695
|
-
delta_input =
|
|
3758
|
+
delta_input = _safe_input("Minimum ΔV between points (mV, default 1.0, 'q'=quit, 'e'=explain): ").strip()
|
|
3696
3759
|
if delta_input.lower() == 'q':
|
|
3697
3760
|
break
|
|
3698
3761
|
if delta_input.lower() == 'e':
|
|
@@ -3715,7 +3778,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3715
3778
|
if delta_input and delta_input.lower() == 'q': # User quit at previous step
|
|
3716
3779
|
continue
|
|
3717
3780
|
while True:
|
|
3718
|
-
window_input =
|
|
3781
|
+
window_input = _safe_input("Savitzky–Golay window (odd, default 9, 'q'=quit, 'e'=explain): ").strip()
|
|
3719
3782
|
if window_input.lower() == 'q':
|
|
3720
3783
|
break
|
|
3721
3784
|
if window_input.lower() == 'e':
|
|
@@ -3734,7 +3797,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3734
3797
|
if window_input and window_input.lower() == 'q': # User quit at previous step
|
|
3735
3798
|
continue
|
|
3736
3799
|
while True:
|
|
3737
|
-
poly_input =
|
|
3800
|
+
poly_input = _safe_input("Polynomial order (default 3, 'q'=quit, 'e'=explain): ").strip()
|
|
3738
3801
|
if poly_input.lower() == 'q':
|
|
3739
3802
|
break
|
|
3740
3803
|
if poly_input.lower() == 'e':
|
|
@@ -3806,7 +3869,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3806
3869
|
print(" 1: Z-score (enter standard deviation threshold, default 5.0)")
|
|
3807
3870
|
print(" 2: MAD (median absolute deviation, default factor 6.0)")
|
|
3808
3871
|
while True:
|
|
3809
|
-
method =
|
|
3872
|
+
method = _safe_input("Method (1/2, blank=cancel, 'q'=quit, 'e'=explain): ").strip()
|
|
3810
3873
|
if not method or method.lower() == 'q':
|
|
3811
3874
|
break
|
|
3812
3875
|
if method.lower() == 'e':
|
|
@@ -3832,7 +3895,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3832
3895
|
continue
|
|
3833
3896
|
try:
|
|
3834
3897
|
while True:
|
|
3835
|
-
thresh_input =
|
|
3898
|
+
thresh_input = _safe_input("Enter threshold (blank=default, 'q'=quit, 'e'=explain): ").strip()
|
|
3836
3899
|
if thresh_input.lower() == 'q':
|
|
3837
3900
|
break
|
|
3838
3901
|
if thresh_input.lower() == 'e':
|
|
@@ -4364,7 +4427,7 @@ def _export_style_dialog(cfg: Dict, default_ext: str = '.bpcfg', base_path: Opti
|
|
|
4364
4427
|
for i, f in enumerate(bpcfg_files, 1):
|
|
4365
4428
|
print(f" {i}: {f}")
|
|
4366
4429
|
|
|
4367
|
-
choice =
|
|
4430
|
+
choice = _safe_input(f"Export to file? Enter filename or number to overwrite (q=cancel): ").strip()
|
|
4368
4431
|
if not choice or choice.lower() == 'q':
|
|
4369
4432
|
return
|
|
4370
4433
|
|