batplot 1.7.22__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.

Files changed (60) hide show
  1. {batplot-1.7.22/batplot.egg-info → batplot-1.7.24}/PKG-INFO +26 -3
  2. {batplot-1.7.22 → batplot-1.7.24}/README.md +3 -1
  3. {batplot-1.7.22/batplot/data → batplot-1.7.24}/USER_MANUAL.md +2 -2
  4. {batplot-1.7.22 → batplot-1.7.24}/batplot/__init__.py +1 -1
  5. {batplot-1.7.22 → batplot-1.7.24}/batplot/args.py +1 -0
  6. {batplot-1.7.22 → batplot-1.7.24}/batplot/cpc_interactive.py +168 -11
  7. {batplot-1.7.22 → batplot-1.7.24}/batplot/electrochem_interactive.py +152 -14
  8. {batplot-1.7.22 → batplot-1.7.24}/batplot/interactive.py +156 -30
  9. {batplot-1.7.22 → batplot-1.7.24}/batplot/operando_ec_interactive.py +198 -21
  10. {batplot-1.7.22 → batplot-1.7.24}/batplot/session.py +92 -1
  11. {batplot-1.7.22 → batplot-1.7.24}/batplot/style.py +109 -47
  12. {batplot-1.7.22 → batplot-1.7.24/batplot.egg-info}/PKG-INFO +26 -3
  13. {batplot-1.7.22 → batplot-1.7.24}/pyproject.toml +2 -3
  14. {batplot-1.7.22 → batplot-1.7.24}/LICENSE +0 -0
  15. {batplot-1.7.22 → batplot-1.7.24}/MANIFEST.in +0 -0
  16. {batplot-1.7.22 → batplot-1.7.24}/batplot/batch.py +0 -0
  17. {batplot-1.7.22 → batplot-1.7.24}/batplot/batplot.py +0 -0
  18. {batplot-1.7.22 → batplot-1.7.24}/batplot/cif.py +0 -0
  19. {batplot-1.7.22 → batplot-1.7.24}/batplot/cli.py +0 -0
  20. {batplot-1.7.22 → batplot-1.7.24}/batplot/color_utils.py +0 -0
  21. {batplot-1.7.22 → batplot-1.7.24}/batplot/config.py +0 -0
  22. {batplot-1.7.22 → batplot-1.7.24}/batplot/converters.py +0 -0
  23. {batplot-1.7.22 → batplot-1.7.24/batplot/data}/USER_MANUAL.md +0 -0
  24. {batplot-1.7.22 → batplot-1.7.24}/batplot/manual.py +0 -0
  25. {batplot-1.7.22 → batplot-1.7.24}/batplot/modes.py +0 -0
  26. {batplot-1.7.22 → batplot-1.7.24}/batplot/operando.py +0 -0
  27. {batplot-1.7.22 → batplot-1.7.24}/batplot/plotting.py +0 -0
  28. {batplot-1.7.22 → batplot-1.7.24}/batplot/readers.py +0 -0
  29. {batplot-1.7.22 → batplot-1.7.24}/batplot/ui.py +0 -0
  30. {batplot-1.7.22 → batplot-1.7.24}/batplot/utils.py +0 -0
  31. {batplot-1.7.22 → batplot-1.7.24}/batplot/version_check.py +0 -0
  32. {batplot-1.7.22 → batplot-1.7.24}/batplot.egg-info/SOURCES.txt +0 -0
  33. {batplot-1.7.22 → batplot-1.7.24}/batplot.egg-info/dependency_links.txt +0 -0
  34. {batplot-1.7.22 → batplot-1.7.24}/batplot.egg-info/entry_points.txt +0 -0
  35. {batplot-1.7.22 → batplot-1.7.24}/batplot.egg-info/requires.txt +0 -0
  36. {batplot-1.7.22 → batplot-1.7.24}/batplot.egg-info/top_level.txt +0 -0
  37. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/__init__.py +0 -0
  38. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/args.py +0 -0
  39. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/batch.py +0 -0
  40. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/batplot.py +0 -0
  41. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/cif.py +0 -0
  42. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/cli.py +0 -0
  43. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/color_utils.py +0 -0
  44. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/config.py +0 -0
  45. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/converters.py +0 -0
  46. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
  47. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
  48. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/interactive.py +0 -0
  49. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/modes.py +0 -0
  50. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/operando.py +0 -0
  51. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
  52. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/plotting.py +0 -0
  53. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/readers.py +0 -0
  54. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/session.py +0 -0
  55. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/style.py +0 -0
  56. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/ui.py +0 -0
  57. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/utils.py +0 -0
  58. {batplot-1.7.22 → batplot-1.7.24}/batplot_backup_20251121_223043/version_check.py +0 -0
  59. {batplot-1.7.22 → batplot-1.7.24}/setup.cfg +0 -0
  60. {batplot-1.7.22 → 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.22
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-Expression: MIT
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
@@ -171,7 +192,9 @@ See [LICENSE](LICENSE)
171
192
 
172
193
  ## Author & Contact
173
194
 
174
- Tian Dai (tianda@uio.no)
195
+ Tian Dai
196
+ tianda@uio.no
175
197
  University of Oslo
198
+ https://www.mn.uio.no/kjemi/english/people/aca/tianda/
176
199
 
177
200
  **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
@@ -141,7 +141,9 @@ See [LICENSE](LICENSE)
141
141
 
142
142
  ## Author & Contact
143
143
 
144
- Tian Dai (tianda@uio.no)
144
+ Tian Dai
145
+ tianda@uio.no
145
146
  University of Oslo
147
+ https://www.mn.uio.no/kjemi/english/people/aca/tianda/
146
148
 
147
149
  **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
@@ -456,8 +456,8 @@ If no `.mpt` file is present, operando mode displays only the contour plot. The
456
456
 
457
457
  For questions, bug reports, or feature requests:
458
458
 
459
- - **GitHub**: https://github.com/tiandai-chem/batplot
459
+ Tian Dai
460
460
  - **Email**: tianda@uio.no
461
461
  - **Mailing List**: Subscribe to batplot-lab@kjemi.uio.no for updates, feature announcements, and community discussions
462
462
 
463
- Feel free to open an issue on GitHub or reach out via email!
463
+ Feel free to reach out via email!
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.7.22"
3
+ __version__ = "1.7.24"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -190,6 +190,7 @@ def _print_general_help() -> None:
190
190
  " (If you are not from UiO, send an email to sympa@kjemi.uio.no with the subject line \"subscribe batplot-lab@kjemi.uio.no your-name\")\n"
191
191
  " Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
192
192
  " Email: tianda@uio.no\n"
193
+ " Personal page: https://www.mn.uio.no/kjemi/english/people/aca/tianda/\n"
193
194
  )
194
195
  _print_help(msg)
195
196
 
@@ -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 or leg 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(ax, h1 + h2, l1 + l2, loc='center', bbox_to_anchor=(fx, fy), bbox_transform=fig.transFigure, borderaxespad=1.0)
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
- fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
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
- if fname.isdigit() and files:
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
- prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
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
- else:
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
- 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)
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
- sub = input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
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
- choice = input("Enter new filename or number to overwrite (q=cancel): ").strip()
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: