batplot 1.8.4__py3-none-any.whl → 1.8.11__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.
@@ -167,6 +167,55 @@ batplot data.xye:0.25:1.54 --xaxis 2theta --interactive
167
167
  # Multiple files with different wavelengths
168
168
  batplot file1.xye:1.5406 file2.xye:0.7093 pattern.cif:1.5406 --xaxis 2theta
169
169
  # Each file uses its own wavelength; CIF ticks use 1.5406 Å
170
+
171
+ ## Data Conversion (`--convert`)
172
+
173
+ The `--convert` flag allows you to convert XRD data files between different representations and export them to a new `converted` subfolder. This is useful for batch conversion of files without opening them in the interactive plotter.
174
+
175
+ ### Conversion Modes
176
+
177
+ 1. **Wavelength-to-wavelength conversion**:
178
+ - Convert 2θ values from one wavelength to another
179
+ - Syntax: `batplot file.xye --convert <wavelength1> <wavelength2>`
180
+ - Example: `batplot file.xye --convert 1.54 0.25`
181
+ - Converts 2θ values measured with λ=1.54 Å to equivalent 2θ values for λ=0.25 Å
182
+ - Process: 2θ(λ=1.54) → Q → 2θ(λ=0.25)
183
+
184
+ 2. **Wavelength-to-Q conversion**:
185
+ - Convert 2θ values (with given wavelength) to Q space
186
+ - Syntax: `batplot file.xye --convert <wavelength> q`
187
+ - Example: `batplot file.xye --convert 1.54 q`
188
+ - Converts 2θ values measured with λ=1.54 Å to Q space
189
+ - Output file: `converted/file.qye`
190
+
191
+ 3. **Q-to-wavelength conversion**:
192
+ - Convert Q space values to 2θ (with given wavelength)
193
+ - Syntax: `batplot file.qye --convert q <wavelength>`
194
+ - Example: `batplot file.qye --convert q 1.54`
195
+ - Converts Q values to 2θ values for λ=1.54 Å
196
+ - Output file: `converted/file.xy`
197
+
198
+ ### Output Location
199
+
200
+ All converted files are saved in a `converted` subfolder within the directory containing the input files. The original file names are preserved, but extensions may change:
201
+ - Q-space files: `.qye` extension
202
+ - 2θ files: `.xy` or original extension
203
+
204
+ ### Examples
205
+
206
+ ```bash
207
+ # Convert 2θ from Cu Kα (1.54 Å) to Mo Kα (0.709 Å)
208
+ batplot pattern.xye --convert 1.54 0.709
209
+
210
+ # Convert 2θ to Q space
211
+ batplot pattern.xye --convert 1.54 q
212
+
213
+ # Convert Q to 2θ
214
+ batplot pattern.qye --convert q 1.54
215
+
216
+ # Convert multiple files
217
+ batplot file1.xye file2.xye --convert 1.54 0.25
218
+ ```
170
219
  ```
171
220
 
172
221
  **Note:** When using dual wavelength conversion, the crosshair (press `n` in interactive mode) will automatically display both the original 2theta (calculated from λ₁) and the current 2theta (displayed axis, calculated from λ₂), along with Q and d-spacing values.
@@ -374,7 +374,7 @@ def _apply_stored_smooth_settings(cycle_lines: Dict[int, Dict[str, Optional[obje
374
374
  ln._smooth_applied = True
375
375
 
376
376
 
377
- def _print_menu(n_cycles: int, is_dqdv: bool = False):
377
+ def _print_menu(n_cycles: int, is_dqdv: bool = False, fig=None):
378
378
  # Three-column menu similar to operando: Styles | Geometries | Options
379
379
  # Use dynamic column widths for clean alignment.
380
380
  col1 = [
@@ -405,6 +405,19 @@ def _print_menu(n_cycles: int, is_dqdv: bool = False):
405
405
  "b: undo",
406
406
  "q: quit",
407
407
  ]
408
+
409
+ # Conditional overwrite shortcuts under (Options)
410
+ if fig is not None:
411
+ last_session = getattr(fig, "_last_session_save_path", None)
412
+ last_style = getattr(fig, "_last_style_export_path", None)
413
+ last_figure = getattr(fig, "_last_figure_export_path", None)
414
+ if last_session:
415
+ col3.append("os: overwrite session")
416
+ if last_style:
417
+ col3.append("ops: overwrite style")
418
+ col3.append("opsg: overwrite style+geom")
419
+ if last_figure:
420
+ col3.append("oe: overwrite figure")
408
421
  # Compute widths (min width prevents overly narrow columns)
409
422
  w1 = max(len("(Styles)"), *(len(s) for s in col1), 18)
410
423
  w2 = max(len("(Geometries)"), *(len(s) for s in col2), 12)
@@ -1640,7 +1653,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1640
1653
  print("Undo: restored previous state.")
1641
1654
  except Exception as e:
1642
1655
  print(f"Undo failed: {e}")
1643
- _print_menu(len(all_cycles), is_dqdv)
1656
+ _print_menu(len(all_cycles), is_dqdv, fig)
1644
1657
  while True:
1645
1658
  try:
1646
1659
  key = _safe_input("Press a key: ").strip().lower()
@@ -1657,18 +1670,18 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1657
1670
  if confirm == 'y':
1658
1671
  break
1659
1672
  else:
1660
- _print_menu(len(all_cycles), is_dqdv)
1673
+ _print_menu(len(all_cycles), is_dqdv, fig)
1661
1674
  continue
1662
1675
  elif key == 'b':
1663
1676
  restore_state()
1664
- _print_menu(len(all_cycles), is_dqdv)
1677
+ _print_menu(len(all_cycles), is_dqdv, fig)
1665
1678
  continue
1666
1679
  elif key == 'e':
1667
1680
  # Export current figure to a file; default extension .svg if missing
1668
1681
  try:
1669
1682
  base_path = choose_save_path(source_paths, purpose="figure export")
1670
1683
  if not base_path:
1671
- _print_menu(len(all_cycles), is_dqdv)
1684
+ _print_menu(len(all_cycles), is_dqdv, fig)
1672
1685
  continue
1673
1686
  # List existing figure files in Figures/ subdirectory
1674
1687
  fig_extensions = ('.svg', '.png', '.jpg', '.jpeg', '.pdf', '.eps', '.tif', '.tiff')
@@ -1690,7 +1703,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1690
1703
  else:
1691
1704
  fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1692
1705
  if not fname or fname.lower() == 'q':
1693
- _print_menu(len(all_cycles), is_dqdv)
1706
+ _print_menu(len(all_cycles), is_dqdv, fig)
1694
1707
  continue
1695
1708
 
1696
1709
  already_confirmed = False # Initialize for new filename case
@@ -1698,15 +1711,15 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1698
1711
  if fname.lower() == 'o':
1699
1712
  if not last_figure_path:
1700
1713
  print("No previous export found.")
1701
- _print_menu(len(all_cycles), is_dqdv)
1714
+ _print_menu(len(all_cycles), is_dqdv, fig)
1702
1715
  continue
1703
1716
  if not os.path.exists(last_figure_path):
1704
1717
  print(f"Previous export file not found: {last_figure_path}")
1705
- _print_menu(len(all_cycles), is_dqdv)
1718
+ _print_menu(len(all_cycles), is_dqdv, fig)
1706
1719
  continue
1707
1720
  yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
1708
1721
  if yn != 'y':
1709
- _print_menu(len(all_cycles), is_dqdv)
1722
+ _print_menu(len(all_cycles), is_dqdv, fig)
1710
1723
  continue
1711
1724
  target = last_figure_path
1712
1725
  already_confirmed = True
@@ -1718,13 +1731,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1718
1731
  name = files[idx-1]
1719
1732
  yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1720
1733
  if yn != 'y':
1721
- _print_menu(len(all_cycles), is_dqdv)
1734
+ _print_menu(len(all_cycles), is_dqdv, fig)
1722
1735
  continue
1723
1736
  target = file_list[idx-1][1] # Full path from list
1724
1737
  already_confirmed = True
1725
1738
  else:
1726
1739
  print("Invalid number.")
1727
- _print_menu(len(all_cycles), is_dqdv)
1740
+ _print_menu(len(all_cycles), is_dqdv, fig)
1728
1741
  continue
1729
1742
  else:
1730
1743
  root, ext = os.path.splitext(fname)
@@ -1807,7 +1820,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1807
1820
  print(f"Export failed: {e}")
1808
1821
  except Exception as e:
1809
1822
  print(f"Export failed: {e}")
1810
- _print_menu(len(all_cycles), is_dqdv)
1823
+ _print_menu(len(all_cycles), is_dqdv, fig)
1811
1824
  continue
1812
1825
  elif key == 'h':
1813
1826
  # Legend submenu: toggle visibility and move legend in inches relative to canvas center
@@ -1896,18 +1909,27 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1896
1909
  continue
1897
1910
  push_state("legend-position")
1898
1911
  try:
1899
- fig._ec_legend_xy_in = _sanitize_legend_offset(fig, (x_in, xy_in[1]))
1900
- # If legend visible, reposition now
1901
- leg = ax.get_legend()
1902
- if leg is not None and leg.get_visible():
1903
- if not _apply_legend_position(fig, ax):
1904
- handles, labels = _visible_legend_entries(ax)
1905
- if handles:
1906
- _legend_no_frame(ax, handles, labels, loc='best', borderaxespad=1.0)
1907
- fig.canvas.draw_idle()
1908
- print(f"Legend position updated: x={x_in:.2f}, y={xy_in[1]:.2f}")
1909
- except Exception:
1910
- pass
1912
+ # Store title before updating position
1913
+ _store_legend_title(fig, ax)
1914
+ # Sanitize and store the new position
1915
+ new_pos = _sanitize_legend_offset(fig, (x_in, xy_in[1]))
1916
+ if new_pos is not None:
1917
+ fig._ec_legend_xy_in = new_pos
1918
+ # If legend visible, reposition now
1919
+ leg = ax.get_legend()
1920
+ if leg is not None and leg.get_visible():
1921
+ if not _apply_legend_position(fig, ax):
1922
+ # Fallback: rebuild with title preserved
1923
+ handles, labels = _visible_legend_entries(ax)
1924
+ if handles:
1925
+ legend_title = _get_legend_title(fig)
1926
+ _legend_no_frame(ax, handles, labels, loc='best', borderaxespad=1.0, title=legend_title)
1927
+ fig.canvas.draw_idle()
1928
+ print(f"Legend position updated: x={new_pos[0]:.2f}, y={new_pos[1]:.2f}")
1929
+ else:
1930
+ print(f"Invalid position: x={x_in:.2f} is out of bounds. Position not updated.")
1931
+ except Exception as e:
1932
+ print(f"Error updating legend position: {e}")
1911
1933
  elif pos_cmd == 'y':
1912
1934
  # Y only: stay in loop
1913
1935
  while True:
@@ -1923,18 +1945,27 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1923
1945
  continue
1924
1946
  push_state("legend-position")
1925
1947
  try:
1926
- fig._ec_legend_xy_in = _sanitize_legend_offset(fig, (xy_in[0], y_in))
1927
- # If legend visible, reposition now
1928
- leg = ax.get_legend()
1929
- if leg is not None and leg.get_visible():
1930
- if not _apply_legend_position(fig, ax):
1931
- handles, labels = _visible_legend_entries(ax)
1932
- if handles:
1933
- _legend_no_frame(ax, handles, labels, loc='best', borderaxespad=1.0)
1934
- fig.canvas.draw_idle()
1935
- print(f"Legend position updated: x={xy_in[0]:.2f}, y={y_in:.2f}")
1936
- except Exception:
1937
- pass
1948
+ # Store title before updating position
1949
+ _store_legend_title(fig, ax)
1950
+ # Sanitize and store the new position
1951
+ new_pos = _sanitize_legend_offset(fig, (xy_in[0], y_in))
1952
+ if new_pos is not None:
1953
+ fig._ec_legend_xy_in = new_pos
1954
+ # If legend visible, reposition now
1955
+ leg = ax.get_legend()
1956
+ if leg is not None and leg.get_visible():
1957
+ if not _apply_legend_position(fig, ax):
1958
+ # Fallback: rebuild with title preserved
1959
+ handles, labels = _visible_legend_entries(ax)
1960
+ if handles:
1961
+ legend_title = _get_legend_title(fig)
1962
+ _legend_no_frame(ax, handles, labels, loc='best', borderaxespad=1.0, title=legend_title)
1963
+ fig.canvas.draw_idle()
1964
+ print(f"Legend position updated: x={new_pos[0]:.2f}, y={new_pos[1]:.2f}")
1965
+ else:
1966
+ print(f"Invalid position: y={y_in:.2f} is out of bounds. Position not updated.")
1967
+ except Exception as e:
1968
+ print(f"Error updating legend position: {e}")
1938
1969
  else:
1939
1970
  # Try to parse as "x y" format
1940
1971
  parts = pos_cmd.replace(',', ' ').split()
@@ -1946,23 +1977,32 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1946
1977
  print("Invalid numbers."); continue
1947
1978
  push_state("legend-position")
1948
1979
  try:
1949
- fig._ec_legend_xy_in = _sanitize_legend_offset(fig, (x_in, y_in))
1950
- # If legend visible, reposition now
1951
- leg = ax.get_legend()
1952
- if leg is not None and leg.get_visible():
1953
- if not _apply_legend_position(fig, ax):
1954
- handles, labels = _visible_legend_entries(ax)
1955
- if handles:
1956
- _legend_no_frame(ax, handles, labels, loc='best', borderaxespad=1.0)
1957
- fig.canvas.draw_idle()
1958
- print(f"Legend position updated: x={x_in:.2f}, y={y_in:.2f}")
1959
- except Exception:
1960
- pass
1980
+ # Store title before updating position
1981
+ _store_legend_title(fig, ax)
1982
+ # Sanitize and store the new position
1983
+ new_pos = _sanitize_legend_offset(fig, (x_in, y_in))
1984
+ if new_pos is not None:
1985
+ fig._ec_legend_xy_in = new_pos
1986
+ # If legend visible, reposition now
1987
+ leg = ax.get_legend()
1988
+ if leg is not None and leg.get_visible():
1989
+ if not _apply_legend_position(fig, ax):
1990
+ # Fallback: rebuild with title preserved
1991
+ handles, labels = _visible_legend_entries(ax)
1992
+ if handles:
1993
+ legend_title = _get_legend_title(fig)
1994
+ _legend_no_frame(ax, handles, labels, loc='best', borderaxespad=1.0, title=legend_title)
1995
+ fig.canvas.draw_idle()
1996
+ print(f"Legend position updated: x={new_pos[0]:.2f}, y={new_pos[1]:.2f}")
1997
+ else:
1998
+ print(f"Invalid position: x={x_in:.2f}, y={y_in:.2f} is out of bounds. Position not updated.")
1999
+ except Exception as e:
2000
+ print(f"Error updating legend position: {e}")
1961
2001
  else:
1962
2002
  print("Unknown option.")
1963
2003
  except Exception:
1964
2004
  pass
1965
- _print_menu(len(all_cycles), is_dqdv)
2005
+ _print_menu(len(all_cycles), is_dqdv, fig)
1966
2006
  continue
1967
2007
  elif key == 'p':
1968
2008
  # Print/export style or style+geometry
@@ -2070,14 +2110,14 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2070
2110
  print("Unknown choice.")
2071
2111
  except Exception as e:
2072
2112
  print(f"Error in style submenu: {e}")
2073
- _print_menu(len(all_cycles), is_dqdv)
2113
+ _print_menu(len(all_cycles), is_dqdv, fig)
2074
2114
  continue
2075
2115
  elif key == 'i':
2076
2116
  # Import style from .bps/.bpsg/.bpcfg
2077
2117
  try:
2078
2118
  path = choose_style_file(source_paths, purpose="style import")
2079
2119
  if not path:
2080
- _print_menu(len(all_cycles), is_dqdv)
2120
+ _print_menu(len(all_cycles), is_dqdv, fig)
2081
2121
  continue
2082
2122
  push_state("import-style")
2083
2123
  with open(path, 'r', encoding='utf-8') as f:
@@ -2087,7 +2127,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2087
2127
  kind = cfg.get('kind', '')
2088
2128
  if kind not in ('ec_style', 'ec_style_geom'):
2089
2129
  print("Not an EC style file.")
2090
- _print_menu(len(all_cycles), is_dqdv)
2130
+ _print_menu(len(all_cycles), is_dqdv, fig)
2091
2131
  continue
2092
2132
 
2093
2133
  # Enforce compatibility between style/geom ro state and current figure ro state
@@ -2099,7 +2139,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2099
2139
  else:
2100
2140
  print("Warning: EC style/geometry file was saved without --ro; current plot was created with --ro.")
2101
2141
  print("Not applying EC style/geometry to avoid corrupting axis orientation.")
2102
- _print_menu(len(all_cycles), is_dqdv)
2142
+ _print_menu(len(all_cycles), is_dqdv, fig)
2103
2143
  continue
2104
2144
 
2105
2145
  has_geometry = (kind == 'ec_style_geom' and 'geometry' in cfg)
@@ -2436,7 +2476,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2436
2476
 
2437
2477
  except Exception as e:
2438
2478
  print(f"Error importing style: {e}")
2439
- _print_menu(len(all_cycles), is_dqdv)
2479
+ _print_menu(len(all_cycles), is_dqdv, fig)
2440
2480
  continue
2441
2481
  elif key == 'l':
2442
2482
  # Line widths submenu: curves vs frame/ticks
@@ -2664,7 +2704,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2664
2704
  print("Unknown option.")
2665
2705
  except Exception as e:
2666
2706
  print(f"Error in line submenu: {e}")
2667
- _print_menu(len(all_cycles), is_dqdv)
2707
+ _print_menu(len(all_cycles), is_dqdv, fig)
2668
2708
  continue
2669
2709
  elif key == 'k':
2670
2710
  # Spine colors (w=top, a=left, s=bottom, d=right)
@@ -2722,7 +2762,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2722
2762
  fig.canvas.draw()
2723
2763
  except Exception as e:
2724
2764
  print(f"Error in spine color menu: {e}")
2725
- _print_menu(len(all_cycles), is_dqdv)
2765
+ _print_menu(len(all_cycles), is_dqdv, fig)
2726
2766
  continue
2727
2767
  elif key == 'r':
2728
2768
  # Rename axis labels
@@ -2792,7 +2832,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2792
2832
  fig.canvas.draw_idle()
2793
2833
  except Exception as e:
2794
2834
  print(f"Error renaming axes: {e}")
2795
- _print_menu(len(all_cycles), is_dqdv)
2835
+ _print_menu(len(all_cycles), is_dqdv, fig)
2796
2836
  continue
2797
2837
  elif key == 't':
2798
2838
  # Unified WASD: w/a/s/d x 1..5 => spine, ticks, minor, labels, title
@@ -2988,7 +3028,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2988
3028
  fig.canvas.draw_idle()
2989
3029
  except Exception as e:
2990
3030
  print(f"Error in WASD tick visibility menu: {e}")
2991
- _print_menu(len(all_cycles), is_dqdv)
3031
+ _print_menu(len(all_cycles), is_dqdv, fig)
2992
3032
  continue
2993
3033
  elif key == 's':
2994
3034
  try:
@@ -2996,7 +3036,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2996
3036
  last_session_path = getattr(fig, '_last_session_save_path', None)
2997
3037
  folder = choose_save_path(source_paths, purpose="EC session save")
2998
3038
  if not folder:
2999
- _print_menu(len(all_cycles), is_dqdv); continue
3039
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3000
3040
  print(f"\nChosen path: {folder}")
3001
3041
  try:
3002
3042
  files = sorted([f for f in os.listdir(folder) if f.lower().endswith('.pkl')])
@@ -3017,35 +3057,35 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3017
3057
  prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
3018
3058
  choice = _safe_input(prompt).strip()
3019
3059
  if not choice or choice.lower() == 'q':
3020
- _print_menu(len(all_cycles), is_dqdv); continue
3060
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3021
3061
  if choice.lower() == 'o':
3022
3062
  # Overwrite last saved session
3023
3063
  if not last_session_path:
3024
3064
  print("No previous save found.")
3025
- _print_menu(len(all_cycles), is_dqdv); continue
3065
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3026
3066
  if not os.path.exists(last_session_path):
3027
3067
  print(f"Previous save file not found: {last_session_path}")
3028
- _print_menu(len(all_cycles), is_dqdv); continue
3068
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3029
3069
  yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
3030
3070
  if yn != 'y':
3031
- _print_menu(len(all_cycles), is_dqdv); continue
3071
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3032
3072
  dump_ec_session(last_session_path, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
3033
3073
  print(f"Overwritten session to {last_session_path}")
3034
- _print_menu(len(all_cycles), is_dqdv); continue
3074
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3035
3075
  if choice.isdigit() and files:
3036
3076
  idx = int(choice)
3037
3077
  if 1 <= idx <= len(files):
3038
3078
  name = files[idx-1]
3039
3079
  yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3040
3080
  if yn != 'y':
3041
- _print_menu(len(all_cycles), is_dqdv); continue
3081
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3042
3082
  target = os.path.join(folder, name)
3043
3083
  dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
3044
3084
  fig._last_session_save_path = target
3045
- _print_menu(len(all_cycles), is_dqdv); continue
3085
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3046
3086
  else:
3047
3087
  print("Invalid number.")
3048
- _print_menu(len(all_cycles), is_dqdv); continue
3088
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3049
3089
  if choice.lower() != 'o':
3050
3090
  name = choice
3051
3091
  root, ext = os.path.splitext(name)
@@ -3055,12 +3095,12 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3055
3095
  if os.path.exists(target):
3056
3096
  yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
3057
3097
  if yn != 'y':
3058
- _print_menu(len(all_cycles), is_dqdv); continue
3098
+ _print_menu(len(all_cycles), is_dqdv, fig); continue
3059
3099
  dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
3060
3100
  fig._last_session_save_path = target
3061
3101
  except Exception as e:
3062
3102
  print(f"Save failed: {e}")
3063
- _print_menu(len(all_cycles), is_dqdv)
3103
+ _print_menu(len(all_cycles), is_dqdv, fig)
3064
3104
  continue
3065
3105
  elif key == 'c':
3066
3106
  # Show current palette if one is applied (this is informational only)
@@ -3096,7 +3136,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3096
3136
  continue
3097
3137
  if line.lower() == 'u':
3098
3138
  manage_user_colors(fig)
3099
- _print_menu(len(all_cycles), is_dqdv)
3139
+ _print_menu(len(all_cycles), is_dqdv, fig)
3100
3140
  continue
3101
3141
  tokens = line.replace(',', ' ').split()
3102
3142
  mode, cycles, mapping, palette, use_all = _parse_cycle_tokens(tokens, fig)
@@ -3225,13 +3265,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3225
3265
  if ignored:
3226
3266
  print("Ignored cycles:", ", ".join(str(c) for c in ignored))
3227
3267
  # Show the menu again after completing the command
3228
- _print_menu(len(all_cycles), is_dqdv)
3268
+ _print_menu(len(all_cycles), is_dqdv, fig)
3229
3269
  continue
3230
3270
  elif key == 'a':
3231
3271
  # X-axis submenu: number-of-ions vs capacity (not available in dQdV mode)
3232
3272
  if is_dqdv:
3233
3273
  print("Capacity/ion conversion is not available in dQ/dV mode.")
3234
- _print_menu(len(all_cycles), is_dqdv)
3274
+ _print_menu(len(all_cycles), is_dqdv, fig)
3235
3275
  continue
3236
3276
  # X-axis submenu: number-of-ions vs capacity
3237
3277
  while True:
@@ -3372,7 +3412,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3372
3412
  fig.canvas.draw()
3373
3413
  except Exception:
3374
3414
  fig.canvas.draw_idle()
3375
- _print_menu(len(all_cycles), is_dqdv)
3415
+ _print_menu(len(all_cycles), is_dqdv, fig)
3376
3416
  continue
3377
3417
  elif key == 'f':
3378
3418
  # Font submenu with numbered options
@@ -3443,7 +3483,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3443
3483
  print("Size must be positive.")
3444
3484
  except Exception:
3445
3485
  print("Invalid size.")
3446
- _print_menu(len(all_cycles), is_dqdv)
3486
+ _print_menu(len(all_cycles), is_dqdv, fig)
3447
3487
  continue
3448
3488
  elif key == 'x':
3449
3489
  # X-axis: set limits only
@@ -3527,7 +3567,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3527
3567
  fig.canvas.draw()
3528
3568
  except Exception:
3529
3569
  print("Invalid limits, ignored.")
3530
- _print_menu(len(all_cycles), is_dqdv)
3570
+ _print_menu(len(all_cycles), is_dqdv, fig)
3531
3571
  continue
3532
3572
  elif key == 'y':
3533
3573
  # Y-axis: set limits only
@@ -3611,7 +3651,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3611
3651
  fig.canvas.draw()
3612
3652
  except Exception:
3613
3653
  print("Invalid limits, ignored.")
3614
- _print_menu(len(all_cycles), is_dqdv)
3654
+ _print_menu(len(all_cycles), is_dqdv, fig)
3615
3655
  continue
3616
3656
  elif key == 'g':
3617
3657
  # Geometry submenu: plot frame vs canvas (scales moved to separate keys)
@@ -3640,13 +3680,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3640
3680
  fig.canvas.draw()
3641
3681
  except Exception:
3642
3682
  fig.canvas.draw_idle()
3643
- _print_menu(len(all_cycles), is_dqdv)
3683
+ _print_menu(len(all_cycles), is_dqdv, fig)
3644
3684
  continue
3645
3685
  elif key == 'sm':
3646
3686
  # dQ/dV smoothing utilities (only available in dQdV mode)
3647
3687
  if not is_dqdv:
3648
3688
  print("Smoothing is only available in dQ/dV mode.")
3649
- _print_menu(len(all_cycles), is_dqdv)
3689
+ _print_menu(len(all_cycles), is_dqdv, fig)
3650
3690
  continue
3651
3691
  while True:
3652
3692
  print("\n\033[1mdQ/dV Data Filtering (Neware method)\033[0m")
@@ -4006,11 +4046,11 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
4006
4046
  print("Invalid number.")
4007
4047
  continue
4008
4048
  print("Unknown command. Use a/o/r/q.")
4009
- _print_menu(len(all_cycles), is_dqdv)
4049
+ _print_menu(len(all_cycles), is_dqdv, fig)
4010
4050
  continue
4011
4051
  else:
4012
4052
  print("Unknown command.")
4013
- _print_menu(len(all_cycles), is_dqdv)
4053
+ _print_menu(len(all_cycles), is_dqdv, fig)
4014
4054
 
4015
4055
 
4016
4056
  def _get_geometry_snapshot(fig, ax) -> Dict: