batplot 1.7.23__tar.gz → 1.7.24__tar.gz
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-1.7.23/batplot.egg-info → batplot-1.7.24}/PKG-INFO +23 -2
- {batplot-1.7.23 → batplot-1.7.24}/batplot/__init__.py +1 -1
- {batplot-1.7.23 → batplot-1.7.24}/batplot/cpc_interactive.py +168 -11
- {batplot-1.7.23 → batplot-1.7.24}/batplot/electrochem_interactive.py +152 -14
- {batplot-1.7.23 → batplot-1.7.24}/batplot/interactive.py +156 -30
- {batplot-1.7.23 → batplot-1.7.24}/batplot/operando_ec_interactive.py +162 -11
- {batplot-1.7.23 → batplot-1.7.24}/batplot/session.py +84 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/style.py +109 -47
- {batplot-1.7.23 → batplot-1.7.24/batplot.egg-info}/PKG-INFO +23 -2
- {batplot-1.7.23 → batplot-1.7.24}/pyproject.toml +2 -3
- {batplot-1.7.23 → batplot-1.7.24}/LICENSE +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/MANIFEST.in +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/README.md +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/USER_MANUAL.md +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/args.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/batch.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/batplot.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/cif.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/cli.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/color_utils.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/config.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/converters.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/data/USER_MANUAL.md +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/manual.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/modes.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/operando.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/plotting.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/readers.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/ui.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/utils.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot/version_check.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/__init__.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/args.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/batch.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/batplot.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/cif.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/cli.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/color_utils.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/config.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/converters.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/interactive.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/modes.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/operando.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/plotting.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/readers.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/session.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/style.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/ui.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/utils.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/batplot_backup_20251121_223043/version_check.py +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/setup.cfg +0 -0
- {batplot-1.7.23 → batplot-1.7.24}/setup.py +0 -0
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.24
|
|
4
4
|
Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
|
|
5
5
|
Author-email: Tian Dai <tianda@uio.no>
|
|
6
|
-
License
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Tian Dai
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
7
28
|
Project-URL: Homepage, https://github.com/TianDai1729/batplot
|
|
8
29
|
Project-URL: Repository, https://github.com/TianDai1729/batplot
|
|
9
30
|
Project-URL: Issues, https://github.com/TianDai1729/batplot/issues
|
|
@@ -79,6 +79,46 @@ def _colorize_menu(text):
|
|
|
79
79
|
return f"\033[96m{cmd}\033[0m: {desc}" # Cyan for command, default for description
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _color_of(artist):
|
|
83
|
+
"""Return a representative color for a Line2D/PathCollection."""
|
|
84
|
+
try:
|
|
85
|
+
if artist is None:
|
|
86
|
+
return None
|
|
87
|
+
if hasattr(artist, 'get_color'):
|
|
88
|
+
c = artist.get_color()
|
|
89
|
+
if isinstance(c, (list, tuple)) and c and not isinstance(c, str):
|
|
90
|
+
return c[0]
|
|
91
|
+
return c
|
|
92
|
+
if hasattr(artist, 'get_facecolors'):
|
|
93
|
+
arr = artist.get_facecolors()
|
|
94
|
+
if arr is not None and len(arr):
|
|
95
|
+
from matplotlib.colors import to_hex
|
|
96
|
+
return to_hex(arr[0])
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _get_legend_title(fig, default: str = "Legend") -> str:
|
|
103
|
+
"""Fetch stored legend title, falling back to current legend text or default."""
|
|
104
|
+
try:
|
|
105
|
+
title = getattr(fig, '_cpc_legend_title', None)
|
|
106
|
+
if isinstance(title, str) and title:
|
|
107
|
+
return title
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
try:
|
|
111
|
+
for ax in getattr(fig, 'axes', []):
|
|
112
|
+
leg = ax.get_legend()
|
|
113
|
+
if leg is not None:
|
|
114
|
+
t = leg.get_title().get_text()
|
|
115
|
+
if t:
|
|
116
|
+
return t
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
return default
|
|
120
|
+
|
|
121
|
+
|
|
82
122
|
def _colorize_prompt(text):
|
|
83
123
|
"""Colorize commands within input prompts. Handles formats like (s=size, f=family, q=return) or (y/n)."""
|
|
84
124
|
import re
|
|
@@ -234,7 +274,7 @@ def _rebuild_legend(ax, ax2, file_data):
|
|
|
234
274
|
h_all.append(h)
|
|
235
275
|
l_all.append(l)
|
|
236
276
|
if h_all:
|
|
237
|
-
_legend_no_frame(ax, h_all, l_all, loc='best', borderaxespad=1.0)
|
|
277
|
+
_legend_no_frame(ax, h_all, l_all, loc='best', borderaxespad=1.0, title=_get_legend_title(ax.figure))
|
|
238
278
|
else:
|
|
239
279
|
leg = ax.get_legend()
|
|
240
280
|
if leg:
|
|
@@ -398,7 +438,8 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
398
438
|
'font': {'family': fam0, 'size': fsize},
|
|
399
439
|
'legend': {
|
|
400
440
|
'visible': legend_visible,
|
|
401
|
-
'position_inches': legend_xy_in # [x, y] offset from canvas center in inches
|
|
441
|
+
'position_inches': legend_xy_in, # [x, y] offset from canvas center in inches
|
|
442
|
+
'title': _get_legend_title(fig),
|
|
402
443
|
},
|
|
403
444
|
'ticks': {
|
|
404
445
|
'widths': {
|
|
@@ -671,6 +712,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
671
712
|
if leg_cfg:
|
|
672
713
|
leg_visible = leg_cfg.get('visible', True)
|
|
673
714
|
leg_xy_in = leg_cfg.get('position_inches')
|
|
715
|
+
if 'title' in leg_cfg:
|
|
716
|
+
fig._cpc_legend_title = leg_cfg.get('title') or _get_legend_title(fig)
|
|
674
717
|
if leg_xy_in is not None:
|
|
675
718
|
fig._cpc_legend_xy_in = _sanitize_legend_offset(tuple(leg_xy_in))
|
|
676
719
|
leg = ax.get_legend()
|
|
@@ -1197,7 +1240,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1197
1240
|
try:
|
|
1198
1241
|
xy_in = _sanitize_legend_offset(getattr(fig, '_cpc_legend_xy_in', None))
|
|
1199
1242
|
leg = ax.get_legend()
|
|
1200
|
-
if xy_in is None
|
|
1243
|
+
if xy_in is None:
|
|
1201
1244
|
return
|
|
1202
1245
|
# Compute figure-fraction anchor from inches
|
|
1203
1246
|
fw, fh = fig.get_size_inches()
|
|
@@ -1209,7 +1252,16 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1209
1252
|
h1, l1 = ax.get_legend_handles_labels()
|
|
1210
1253
|
h2, l2 = ax2.get_legend_handles_labels()
|
|
1211
1254
|
if h1 or h2:
|
|
1212
|
-
_legend_no_frame(
|
|
1255
|
+
_legend_no_frame(
|
|
1256
|
+
ax,
|
|
1257
|
+
h1 + h2,
|
|
1258
|
+
l1 + l2,
|
|
1259
|
+
loc='center',
|
|
1260
|
+
bbox_to_anchor=(fx, fy),
|
|
1261
|
+
bbox_transform=fig.transFigure,
|
|
1262
|
+
borderaxespad=1.0,
|
|
1263
|
+
title=_get_legend_title(fig),
|
|
1264
|
+
)
|
|
1213
1265
|
except Exception:
|
|
1214
1266
|
pass
|
|
1215
1267
|
|
|
@@ -1680,12 +1732,28 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1680
1732
|
else:
|
|
1681
1733
|
print(f" {i}: {fname}")
|
|
1682
1734
|
|
|
1683
|
-
|
|
1735
|
+
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
1736
|
+
if last_figure_path:
|
|
1737
|
+
fname = input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
1738
|
+
else:
|
|
1739
|
+
fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
1684
1740
|
if not fname or fname.lower() == 'q':
|
|
1685
1741
|
_print_menu(); continue
|
|
1686
1742
|
|
|
1743
|
+
# Check for 'o' option
|
|
1744
|
+
if fname.lower() == 'o':
|
|
1745
|
+
if not last_figure_path:
|
|
1746
|
+
print("No previous export found.")
|
|
1747
|
+
_print_menu(); continue
|
|
1748
|
+
if not os.path.exists(last_figure_path):
|
|
1749
|
+
print(f"Previous export file not found: {last_figure_path}")
|
|
1750
|
+
_print_menu(); continue
|
|
1751
|
+
yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
1752
|
+
if yn != 'y':
|
|
1753
|
+
_print_menu(); continue
|
|
1754
|
+
target = last_figure_path
|
|
1687
1755
|
# Check if user selected a number
|
|
1688
|
-
|
|
1756
|
+
elif fname.isdigit() and files:
|
|
1689
1757
|
idx = int(fname)
|
|
1690
1758
|
if 1 <= idx <= len(files):
|
|
1691
1759
|
name = files[idx-1]
|
|
@@ -1734,6 +1802,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1734
1802
|
# Export the figure
|
|
1735
1803
|
fig.savefig(target, bbox_inches='tight')
|
|
1736
1804
|
print(f"Exported figure to {target}")
|
|
1805
|
+
fig._last_figure_export_path = target
|
|
1737
1806
|
|
|
1738
1807
|
# Restore original labels
|
|
1739
1808
|
if is_multi_file and original_labels:
|
|
@@ -1767,10 +1836,28 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1767
1836
|
print(f" {i}: {f} ({timestamp})")
|
|
1768
1837
|
else:
|
|
1769
1838
|
print(f" {i}: {f}")
|
|
1770
|
-
|
|
1839
|
+
last_session_path = getattr(fig, '_last_session_save_path', None)
|
|
1840
|
+
if last_session_path:
|
|
1841
|
+
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
1842
|
+
else:
|
|
1843
|
+
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
1771
1844
|
choice = input(prompt).strip()
|
|
1772
1845
|
if not choice or choice.lower() == 'q':
|
|
1773
1846
|
_print_menu(); continue
|
|
1847
|
+
if choice.lower() == 'o':
|
|
1848
|
+
# Overwrite last saved session
|
|
1849
|
+
if not last_session_path:
|
|
1850
|
+
print("No previous save found.")
|
|
1851
|
+
_print_menu(); continue
|
|
1852
|
+
if not os.path.exists(last_session_path):
|
|
1853
|
+
print(f"Previous save file not found: {last_session_path}")
|
|
1854
|
+
_print_menu(); continue
|
|
1855
|
+
yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
1856
|
+
if yn != 'y':
|
|
1857
|
+
_print_menu(); continue
|
|
1858
|
+
dump_cpc_session(last_session_path, fig=fig, ax=ax, ax2=ax2, sc_charge=sc_charge, sc_discharge=sc_discharge, sc_eff=sc_eff, file_data=file_data, skip_confirm=True)
|
|
1859
|
+
print(f"Overwritten session to {last_session_path}")
|
|
1860
|
+
_print_menu(); continue
|
|
1774
1861
|
if choice.isdigit() and files:
|
|
1775
1862
|
idx = int(choice)
|
|
1776
1863
|
if 1 <= idx <= len(files):
|
|
@@ -1779,10 +1866,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1779
1866
|
if yn != 'y':
|
|
1780
1867
|
_print_menu(); continue
|
|
1781
1868
|
target = os.path.join(folder, name)
|
|
1869
|
+
dump_cpc_session(target, fig=fig, ax=ax, ax2=ax2, sc_charge=sc_charge, sc_discharge=sc_discharge, sc_eff=sc_eff, file_data=file_data, skip_confirm=True)
|
|
1870
|
+
fig._last_session_save_path = target
|
|
1871
|
+
_print_menu(); continue
|
|
1782
1872
|
else:
|
|
1783
1873
|
print("Invalid number.")
|
|
1784
1874
|
_print_menu(); continue
|
|
1785
|
-
|
|
1875
|
+
if choice.lower() != 'o':
|
|
1786
1876
|
name = choice
|
|
1787
1877
|
root, ext = os.path.splitext(name)
|
|
1788
1878
|
if ext == '':
|
|
@@ -1792,7 +1882,8 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1792
1882
|
yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
1793
1883
|
if yn != 'y':
|
|
1794
1884
|
_print_menu(); continue
|
|
1795
|
-
|
|
1885
|
+
dump_cpc_session(target, fig=fig, ax=ax, ax2=ax2, sc_charge=sc_charge, sc_discharge=sc_discharge, sc_eff=sc_eff, file_data=file_data, skip_confirm=True)
|
|
1886
|
+
fig._last_session_save_path = target
|
|
1796
1887
|
except Exception as e:
|
|
1797
1888
|
print(f"Save failed: {e}")
|
|
1798
1889
|
_print_menu(); continue
|
|
@@ -1916,11 +2007,44 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1916
2007
|
else:
|
|
1917
2008
|
print(f" {_i}: {fname}")
|
|
1918
2009
|
|
|
1919
|
-
|
|
2010
|
+
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
2011
|
+
if last_style_path:
|
|
2012
|
+
sub = input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
2013
|
+
else:
|
|
2014
|
+
sub = input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
1920
2015
|
if sub == 'q':
|
|
1921
2016
|
break
|
|
1922
2017
|
if sub == 'r' or sub == '':
|
|
1923
2018
|
continue
|
|
2019
|
+
if sub == 'o':
|
|
2020
|
+
# Overwrite last exported style file
|
|
2021
|
+
if not last_style_path:
|
|
2022
|
+
print("No previous export found.")
|
|
2023
|
+
continue
|
|
2024
|
+
if not os.path.exists(last_style_path):
|
|
2025
|
+
print(f"Previous export file not found: {last_style_path}")
|
|
2026
|
+
continue
|
|
2027
|
+
yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2028
|
+
if yn != 'y':
|
|
2029
|
+
continue
|
|
2030
|
+
# Rebuild config based on current state
|
|
2031
|
+
snap = _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data)
|
|
2032
|
+
# Determine if last export was style-only or style+geometry
|
|
2033
|
+
try:
|
|
2034
|
+
with open(last_style_path, 'r', encoding='utf-8') as f:
|
|
2035
|
+
old_cfg = json.load(f)
|
|
2036
|
+
if old_cfg.get('kind') == 'cpc_style_geom':
|
|
2037
|
+
snap['kind'] = 'cpc_style_geom'
|
|
2038
|
+
snap['geometry'] = _get_geometry_snapshot(ax, ax2)
|
|
2039
|
+
else:
|
|
2040
|
+
snap['kind'] = 'cpc_style'
|
|
2041
|
+
except Exception:
|
|
2042
|
+
snap['kind'] = 'cpc_style'
|
|
2043
|
+
with open(last_style_path, 'w', encoding='utf-8') as f:
|
|
2044
|
+
json.dump(snap, f, indent=2)
|
|
2045
|
+
print(f"Overwritten style to {last_style_path}")
|
|
2046
|
+
style_menu_active = False
|
|
2047
|
+
break
|
|
1924
2048
|
if sub == 'e':
|
|
1925
2049
|
# Ask for ps or psg
|
|
1926
2050
|
print("Export options:")
|
|
@@ -1978,10 +2102,42 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1978
2102
|
print(f" {i}: {fname} ({timestamp})")
|
|
1979
2103
|
else:
|
|
1980
2104
|
print(f" {i}: {fname}")
|
|
1981
|
-
|
|
2105
|
+
if last_style_path:
|
|
2106
|
+
choice = input("Enter new filename, number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
2107
|
+
else:
|
|
2108
|
+
choice = input("Enter new filename or number to overwrite (q=cancel): ").strip()
|
|
1982
2109
|
if not choice or choice.lower() == 'q':
|
|
1983
2110
|
print("Style export canceled.")
|
|
1984
2111
|
continue
|
|
2112
|
+
if choice.lower() == 'o':
|
|
2113
|
+
# Overwrite last exported style file
|
|
2114
|
+
if not last_style_path:
|
|
2115
|
+
print("No previous export found.")
|
|
2116
|
+
continue
|
|
2117
|
+
if not os.path.exists(last_style_path):
|
|
2118
|
+
print(f"Previous export file not found: {last_style_path}")
|
|
2119
|
+
continue
|
|
2120
|
+
yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2121
|
+
if yn != 'y':
|
|
2122
|
+
continue
|
|
2123
|
+
# Rebuild config based on current state
|
|
2124
|
+
snap = _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data)
|
|
2125
|
+
# Determine if last export was style-only or style+geometry
|
|
2126
|
+
try:
|
|
2127
|
+
with open(last_style_path, 'r', encoding='utf-8') as f:
|
|
2128
|
+
old_cfg = json.load(f)
|
|
2129
|
+
if old_cfg.get('kind') == 'cpc_style_geom':
|
|
2130
|
+
snap['kind'] = 'cpc_style_geom'
|
|
2131
|
+
snap['geometry'] = _get_geometry_snapshot(ax, ax2)
|
|
2132
|
+
else:
|
|
2133
|
+
snap['kind'] = 'cpc_style'
|
|
2134
|
+
except Exception:
|
|
2135
|
+
snap['kind'] = 'cpc_style'
|
|
2136
|
+
with open(last_style_path, 'w', encoding='utf-8') as f:
|
|
2137
|
+
json.dump(snap, f, indent=2)
|
|
2138
|
+
print(f"Overwritten style to {last_style_path}")
|
|
2139
|
+
style_menu_active = False
|
|
2140
|
+
break
|
|
1985
2141
|
target = None
|
|
1986
2142
|
if choice.isdigit() and files:
|
|
1987
2143
|
idx = int(choice)
|
|
@@ -2011,6 +2167,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2011
2167
|
with open(target, 'w', encoding='utf-8') as f:
|
|
2012
2168
|
json.dump(snap, f, indent=2)
|
|
2013
2169
|
print(f"Exported CPC style to {target}")
|
|
2170
|
+
fig._last_style_export_path = target
|
|
2014
2171
|
style_menu_active = False # Exit style submenu and return to main menu
|
|
2015
2172
|
break
|
|
2016
2173
|
else:
|
|
@@ -468,6 +468,8 @@ def _get_legend_title(fig, default: str = "Cycle") -> str:
|
|
|
468
468
|
def _rebuild_legend(ax):
|
|
469
469
|
"""Rebuild legend using only visible lines, anchoring to absolute inches from canvas center if available."""
|
|
470
470
|
fig = ax.figure
|
|
471
|
+
# Capture existing title before any rebuild so it isn't lost
|
|
472
|
+
_store_legend_title(fig, ax)
|
|
471
473
|
if not _get_legend_user_pref(fig):
|
|
472
474
|
leg = ax.get_legend()
|
|
473
475
|
if leg is not None:
|
|
@@ -1271,6 +1273,10 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1271
1273
|
snap['legend']['visible'] = bool(leg_obj.get_visible()) if leg_obj is not None else False
|
|
1272
1274
|
except Exception:
|
|
1273
1275
|
pass
|
|
1276
|
+
try:
|
|
1277
|
+
snap['legend']['title'] = _get_legend_title(fig)
|
|
1278
|
+
except Exception:
|
|
1279
|
+
snap['legend']['title'] = None
|
|
1274
1280
|
try:
|
|
1275
1281
|
legend_xy = getattr(fig, '_ec_legend_xy_in', None)
|
|
1276
1282
|
if legend_xy is not None:
|
|
@@ -1299,7 +1305,33 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1299
1305
|
if len(state_history) > 40:
|
|
1300
1306
|
state_history.pop(0)
|
|
1301
1307
|
except Exception:
|
|
1302
|
-
|
|
1308
|
+
# Minimal fallback so undo still works if full snapshot fails
|
|
1309
|
+
try:
|
|
1310
|
+
fallback = {
|
|
1311
|
+
'note': f"{note}-fallback",
|
|
1312
|
+
'xlim': ax.get_xlim(),
|
|
1313
|
+
'ylim': ax.get_ylim(),
|
|
1314
|
+
'legend': {
|
|
1315
|
+
'visible': bool(ax.get_legend().get_visible()) if ax.get_legend() else False,
|
|
1316
|
+
'position_inches': getattr(fig, '_ec_legend_xy_in', None),
|
|
1317
|
+
'title': _get_legend_title(fig),
|
|
1318
|
+
},
|
|
1319
|
+
'lines': []
|
|
1320
|
+
}
|
|
1321
|
+
for i, ln in enumerate(ax.lines):
|
|
1322
|
+
try:
|
|
1323
|
+
fallback['lines'].append({
|
|
1324
|
+
'index': i,
|
|
1325
|
+
'color': ln.get_color(),
|
|
1326
|
+
'visible': ln.get_visible(),
|
|
1327
|
+
})
|
|
1328
|
+
except Exception:
|
|
1329
|
+
fallback['lines'].append({'index': i})
|
|
1330
|
+
state_history.append(fallback)
|
|
1331
|
+
if len(state_history) > 40:
|
|
1332
|
+
state_history.pop(0)
|
|
1333
|
+
except Exception:
|
|
1334
|
+
pass
|
|
1303
1335
|
|
|
1304
1336
|
def restore_state():
|
|
1305
1337
|
if not state_history:
|
|
@@ -1529,6 +1561,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1529
1561
|
legend_snap = snap.get('legend', {})
|
|
1530
1562
|
if legend_snap:
|
|
1531
1563
|
try:
|
|
1564
|
+
if 'title' in legend_snap:
|
|
1565
|
+
fig._ec_legend_title = legend_snap.get('title') or _get_legend_title(fig)
|
|
1532
1566
|
xy = legend_snap.get('position_inches')
|
|
1533
1567
|
fig._ec_legend_xy_in = _sanitize_legend_offset(fig, xy) if xy is not None else None
|
|
1534
1568
|
except Exception:
|
|
@@ -1594,14 +1628,35 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1594
1628
|
else:
|
|
1595
1629
|
print(f" {i}: {fname}")
|
|
1596
1630
|
|
|
1597
|
-
|
|
1631
|
+
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
1632
|
+
if last_figure_path:
|
|
1633
|
+
fname = input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
1634
|
+
else:
|
|
1635
|
+
fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
1598
1636
|
if not fname or fname.lower() == 'q':
|
|
1599
1637
|
_print_menu(len(all_cycles), is_dqdv)
|
|
1600
1638
|
continue
|
|
1601
1639
|
|
|
1640
|
+
already_confirmed = False # Initialize for new filename case
|
|
1641
|
+
# Check for 'o' option
|
|
1642
|
+
if fname.lower() == 'o':
|
|
1643
|
+
if not last_figure_path:
|
|
1644
|
+
print("No previous export found.")
|
|
1645
|
+
_print_menu(len(all_cycles), is_dqdv)
|
|
1646
|
+
continue
|
|
1647
|
+
if not os.path.exists(last_figure_path):
|
|
1648
|
+
print(f"Previous export file not found: {last_figure_path}")
|
|
1649
|
+
_print_menu(len(all_cycles), is_dqdv)
|
|
1650
|
+
continue
|
|
1651
|
+
yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
1652
|
+
if yn != 'y':
|
|
1653
|
+
_print_menu(len(all_cycles), is_dqdv)
|
|
1654
|
+
continue
|
|
1655
|
+
target = last_figure_path
|
|
1656
|
+
already_confirmed = True
|
|
1602
1657
|
# Check if user selected a number
|
|
1603
|
-
|
|
1604
|
-
|
|
1658
|
+
elif fname.isdigit() and files:
|
|
1659
|
+
already_confirmed = False
|
|
1605
1660
|
idx = int(fname)
|
|
1606
1661
|
if 1 <= idx <= len(files):
|
|
1607
1662
|
name = files[idx-1]
|
|
@@ -1671,6 +1726,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1671
1726
|
else:
|
|
1672
1727
|
fig.savefig(target, bbox_inches='tight')
|
|
1673
1728
|
print(f"Exported figure to {target}")
|
|
1729
|
+
fig._last_figure_export_path = target
|
|
1674
1730
|
except Exception as e:
|
|
1675
1731
|
print(f"Export failed: {e}")
|
|
1676
1732
|
except Exception as e:
|
|
@@ -1854,11 +1910,45 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1854
1910
|
else:
|
|
1855
1911
|
print(f" {_i}: {fname}")
|
|
1856
1912
|
|
|
1857
|
-
|
|
1913
|
+
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
1914
|
+
if last_style_path:
|
|
1915
|
+
sub = input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
1916
|
+
else:
|
|
1917
|
+
sub = input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
1858
1918
|
if sub == 'q':
|
|
1859
1919
|
break
|
|
1860
1920
|
if sub == 'r' or sub == '':
|
|
1861
1921
|
continue
|
|
1922
|
+
if sub == 'o':
|
|
1923
|
+
# Overwrite last exported style file
|
|
1924
|
+
if not last_style_path:
|
|
1925
|
+
print("No previous export found.")
|
|
1926
|
+
continue
|
|
1927
|
+
if not os.path.exists(last_style_path):
|
|
1928
|
+
print(f"Previous export file not found: {last_style_path}")
|
|
1929
|
+
continue
|
|
1930
|
+
yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
1931
|
+
if yn != 'y':
|
|
1932
|
+
continue
|
|
1933
|
+
# Rebuild config based on current state
|
|
1934
|
+
cfg = _get_style_snapshot(fig, ax, cycle_lines, tick_state)
|
|
1935
|
+
# Determine if last export was style-only or style+geometry
|
|
1936
|
+
try:
|
|
1937
|
+
with open(last_style_path, 'r', encoding='utf-8') as f:
|
|
1938
|
+
old_cfg = json.load(f)
|
|
1939
|
+
if old_cfg.get('kind') == 'ec_style_geom':
|
|
1940
|
+
geom = _get_geometry_snapshot(fig, ax)
|
|
1941
|
+
cfg['kind'] = 'ec_style_geom'
|
|
1942
|
+
cfg['geometry'] = geom
|
|
1943
|
+
else:
|
|
1944
|
+
cfg['kind'] = 'ec_style'
|
|
1945
|
+
except Exception:
|
|
1946
|
+
cfg['kind'] = 'ec_style'
|
|
1947
|
+
with open(last_style_path, 'w', encoding='utf-8') as f:
|
|
1948
|
+
json.dump(cfg, f, indent=2)
|
|
1949
|
+
print(f"Overwritten style to {last_style_path}")
|
|
1950
|
+
style_menu_active = False
|
|
1951
|
+
break
|
|
1862
1952
|
if sub == 'e':
|
|
1863
1953
|
# Ask for ps or psg
|
|
1864
1954
|
print("Export options:")
|
|
@@ -1895,7 +1985,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1895
1985
|
print("Style export canceled.")
|
|
1896
1986
|
continue
|
|
1897
1987
|
print(f"\nChosen path: {save_base}")
|
|
1898
|
-
_export_style_dialog(cfg, default_ext=default_ext, base_path=save_base)
|
|
1988
|
+
exported_path = _export_style_dialog(cfg, default_ext=default_ext, base_path=save_base)
|
|
1989
|
+
if exported_path:
|
|
1990
|
+
fig._last_style_export_path = exported_path
|
|
1899
1991
|
style_menu_active = False # Exit style submenu and return to main menu
|
|
1900
1992
|
break
|
|
1901
1993
|
else:
|
|
@@ -2197,6 +2289,19 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2197
2289
|
except Exception as e:
|
|
2198
2290
|
print(f"Warning: Could not apply geometry: {e}")
|
|
2199
2291
|
|
|
2292
|
+
# Restore title offsets
|
|
2293
|
+
try:
|
|
2294
|
+
offsets = cfg.get('title_offsets', {})
|
|
2295
|
+
if offsets:
|
|
2296
|
+
ax._top_xlabel_manual_offset_y_pts = float(offsets.get('top_y', 0.0) or 0.0)
|
|
2297
|
+
ax._top_xlabel_manual_offset_x_pts = float(offsets.get('top_x', 0.0) or 0.0)
|
|
2298
|
+
ax._bottom_xlabel_manual_offset_y_pts = float(offsets.get('bottom_y', 0.0) or 0.0)
|
|
2299
|
+
ax._left_ylabel_manual_offset_x_pts = float(offsets.get('left_x', 0.0) or 0.0)
|
|
2300
|
+
ax._right_ylabel_manual_offset_x_pts = float(offsets.get('right_x', 0.0) or 0.0)
|
|
2301
|
+
ax._right_ylabel_manual_offset_y_pts = float(offsets.get('right_y', 0.0) or 0.0)
|
|
2302
|
+
except Exception:
|
|
2303
|
+
pass
|
|
2304
|
+
|
|
2200
2305
|
# Final label positioning - do this AFTER all style changes to prevent drift
|
|
2201
2306
|
# Set pending labelpad before repositioning to preserve original values
|
|
2202
2307
|
try:
|
|
@@ -2210,12 +2315,11 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2210
2315
|
font_cfg = cfg.get('font', {})
|
|
2211
2316
|
font_changed = (font_cfg.get('family') is not None or font_cfg.get('size') is not None)
|
|
2212
2317
|
|
|
2213
|
-
if
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
_ui_position_right_ylabel(ax, fig, tick_state)
|
|
2318
|
+
# Always reposition titles to apply offsets (even if nothing else changed)
|
|
2319
|
+
_ui_position_top_xlabel(ax, fig, tick_state)
|
|
2320
|
+
_ui_position_bottom_xlabel(ax, fig, tick_state)
|
|
2321
|
+
_ui_position_left_ylabel(ax, fig, tick_state)
|
|
2322
|
+
_ui_position_right_ylabel(ax, fig, tick_state)
|
|
2219
2323
|
|
|
2220
2324
|
# Always ensure labelpad is exactly as it was before style import
|
|
2221
2325
|
# This is a final safeguard against any drift
|
|
@@ -2798,6 +2902,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2798
2902
|
elif key == 's':
|
|
2799
2903
|
try:
|
|
2800
2904
|
from .session import dump_ec_session
|
|
2905
|
+
last_session_path = getattr(fig, '_last_session_save_path', None)
|
|
2801
2906
|
folder = choose_save_path(source_paths, purpose="EC session save")
|
|
2802
2907
|
if not folder:
|
|
2803
2908
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
@@ -2815,10 +2920,27 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2815
2920
|
print(f" {i}: {f} ({timestamp})")
|
|
2816
2921
|
else:
|
|
2817
2922
|
print(f" {i}: {f}")
|
|
2818
|
-
|
|
2923
|
+
if last_session_path:
|
|
2924
|
+
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
2925
|
+
else:
|
|
2926
|
+
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
2819
2927
|
choice = input(prompt).strip()
|
|
2820
2928
|
if not choice or choice.lower() == 'q':
|
|
2821
2929
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2930
|
+
if choice.lower() == 'o':
|
|
2931
|
+
# Overwrite last saved session
|
|
2932
|
+
if not last_session_path:
|
|
2933
|
+
print("No previous save found.")
|
|
2934
|
+
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2935
|
+
if not os.path.exists(last_session_path):
|
|
2936
|
+
print(f"Previous save file not found: {last_session_path}")
|
|
2937
|
+
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2938
|
+
yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2939
|
+
if yn != 'y':
|
|
2940
|
+
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2941
|
+
dump_ec_session(last_session_path, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
|
|
2942
|
+
print(f"Overwritten session to {last_session_path}")
|
|
2943
|
+
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2822
2944
|
if choice.isdigit() and files:
|
|
2823
2945
|
idx = int(choice)
|
|
2824
2946
|
if 1 <= idx <= len(files):
|
|
@@ -2827,10 +2949,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2827
2949
|
if yn != 'y':
|
|
2828
2950
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2829
2951
|
target = os.path.join(folder, name)
|
|
2952
|
+
dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
|
|
2953
|
+
fig._last_session_save_path = target
|
|
2954
|
+
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2830
2955
|
else:
|
|
2831
2956
|
print("Invalid number.")
|
|
2832
2957
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2833
|
-
|
|
2958
|
+
if choice.lower() != 'o':
|
|
2834
2959
|
name = choice
|
|
2835
2960
|
root, ext = os.path.splitext(name)
|
|
2836
2961
|
if ext == '':
|
|
@@ -2841,6 +2966,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2841
2966
|
if yn != 'y':
|
|
2842
2967
|
_print_menu(len(all_cycles), is_dqdv); continue
|
|
2843
2968
|
dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
|
|
2969
|
+
fig._last_session_save_path = target
|
|
2844
2970
|
except Exception as e:
|
|
2845
2971
|
print(f"Save failed: {e}")
|
|
2846
2972
|
_print_menu(len(all_cycles), is_dqdv)
|
|
@@ -4008,6 +4134,14 @@ def _get_style_snapshot(fig, ax, cycle_lines: Dict, tick_state: Dict) -> Dict:
|
|
|
4008
4134
|
'ticks': {'widths': tick_widths, 'direction': tick_direction},
|
|
4009
4135
|
'grid': grid_enabled,
|
|
4010
4136
|
'wasd_state': wasd_state,
|
|
4137
|
+
'title_offsets': {
|
|
4138
|
+
'top_y': float(getattr(ax, '_top_xlabel_manual_offset_y_pts', 0.0) or 0.0),
|
|
4139
|
+
'top_x': float(getattr(ax, '_top_xlabel_manual_offset_x_pts', 0.0) or 0.0),
|
|
4140
|
+
'bottom_y': float(getattr(ax, '_bottom_xlabel_manual_offset_y_pts', 0.0) or 0.0),
|
|
4141
|
+
'left_x': float(getattr(ax, '_left_ylabel_manual_offset_x_pts', 0.0) or 0.0),
|
|
4142
|
+
'right_x': float(getattr(ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0),
|
|
4143
|
+
'right_y': float(getattr(ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0),
|
|
4144
|
+
},
|
|
4011
4145
|
'curve_linewidth': curve_linewidth,
|
|
4012
4146
|
'curve_markers': curve_marker_props,
|
|
4013
4147
|
'rotation_angle': getattr(fig, '_ec_rotation_angle', 0),
|
|
@@ -4236,9 +4370,11 @@ def _export_style_dialog(cfg: Dict, default_ext: str = '.bpcfg', base_path: Opti
|
|
|
4236
4370
|
with open(target_path, 'w', encoding='utf-8') as f:
|
|
4237
4371
|
json.dump(cfg, f, indent=2)
|
|
4238
4372
|
print(f"Style exported to {target_path}")
|
|
4373
|
+
return target_path
|
|
4239
4374
|
|
|
4240
4375
|
except Exception as e:
|
|
4241
4376
|
print(f"Export failed: {e}")
|
|
4377
|
+
return None
|
|
4242
4378
|
def _legend_no_frame(ax, *args, title: Optional[str] = None, **kwargs):
|
|
4243
4379
|
leg = ax.legend(*args, **kwargs)
|
|
4244
4380
|
if leg is not None:
|
|
@@ -4258,6 +4394,8 @@ def _apply_legend_position(fig, ax):
|
|
|
4258
4394
|
xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', None))
|
|
4259
4395
|
if xy_in is None:
|
|
4260
4396
|
return False
|
|
4397
|
+
# Preserve current title before rebuilding the legend
|
|
4398
|
+
_store_legend_title(fig, ax)
|
|
4261
4399
|
handles, labels = _visible_legend_entries(ax)
|
|
4262
4400
|
if not handles:
|
|
4263
4401
|
return False
|