batplot 1.8.1__py3-none-any.whl → 1.8.3__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.

Files changed (42) hide show
  1. batplot/__init__.py +1 -1
  2. batplot/args.py +2 -0
  3. batplot/batch.py +23 -0
  4. batplot/batplot.py +101 -12
  5. batplot/cpc_interactive.py +25 -3
  6. batplot/electrochem_interactive.py +20 -4
  7. batplot/interactive.py +19 -15
  8. batplot/modes.py +12 -12
  9. batplot/operando_ec_interactive.py +4 -4
  10. batplot/session.py +218 -0
  11. batplot/style.py +21 -2
  12. batplot/version_check.py +1 -1
  13. {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/METADATA +1 -1
  14. batplot-1.8.3.dist-info/RECORD +75 -0
  15. {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/top_level.txt +1 -0
  16. batplot_backup_20251221_101150/__init__.py +5 -0
  17. batplot_backup_20251221_101150/args.py +625 -0
  18. batplot_backup_20251221_101150/batch.py +1176 -0
  19. batplot_backup_20251221_101150/batplot.py +3589 -0
  20. batplot_backup_20251221_101150/cif.py +823 -0
  21. batplot_backup_20251221_101150/cli.py +149 -0
  22. batplot_backup_20251221_101150/color_utils.py +547 -0
  23. batplot_backup_20251221_101150/config.py +198 -0
  24. batplot_backup_20251221_101150/converters.py +204 -0
  25. batplot_backup_20251221_101150/cpc_interactive.py +4409 -0
  26. batplot_backup_20251221_101150/electrochem_interactive.py +4520 -0
  27. batplot_backup_20251221_101150/interactive.py +3894 -0
  28. batplot_backup_20251221_101150/manual.py +323 -0
  29. batplot_backup_20251221_101150/modes.py +799 -0
  30. batplot_backup_20251221_101150/operando.py +603 -0
  31. batplot_backup_20251221_101150/operando_ec_interactive.py +5487 -0
  32. batplot_backup_20251221_101150/plotting.py +228 -0
  33. batplot_backup_20251221_101150/readers.py +2607 -0
  34. batplot_backup_20251221_101150/session.py +2951 -0
  35. batplot_backup_20251221_101150/style.py +1441 -0
  36. batplot_backup_20251221_101150/ui.py +790 -0
  37. batplot_backup_20251221_101150/utils.py +1046 -0
  38. batplot_backup_20251221_101150/version_check.py +253 -0
  39. batplot-1.8.1.dist-info/RECORD +0 -52
  40. {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/WHEEL +0 -0
  41. {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/entry_points.txt +0 -0
  42. {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/licenses/LICENSE +0 -0
batplot/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.8.1"
3
+ __version__ = "1.8.3"
4
4
 
5
5
  __all__ = ["__version__"]
batplot/args.py CHANGED
@@ -152,6 +152,8 @@ def _print_general_help() -> None:
152
152
  " batplot --all style.bps # Batch with style: apply style.bps to all files\n"
153
153
  " batplot --all ./Style/style.bps # Batch with style: use relative path to style file\n"
154
154
  " batplot --all config.bpsg # Batch with style+geom: apply to all XY files\n"
155
+ " batplot file1.xy:1.54 file2.qye --stack # Stack mode: stack all files vertically\n"
156
+ " batplot file1.xy:1.54 file2.qye structure.cif --stack --i # Stack mode: stack all files vertically with cif ticks\n"
155
157
  " batplot file1.qye file2.qye style.bps # Apply style to multiple files and export\n"
156
158
  " batplot file1.xy file2.xye ./Style/style.bps # Apply style from relative path\n\n"
157
159
  " [Electrochemistry]\n"
batplot/batch.py CHANGED
@@ -157,6 +157,18 @@ def _apply_xy_style(fig, ax, cfg: dict):
157
157
  except Exception:
158
158
  pass
159
159
 
160
+ # Enforce compatibility between style/geom ro state and current figure ro state.
161
+ # Styles saved from a plot using --ro (swapped x/y) must not be applied to a non-ro plot, and vice versa.
162
+ file_ro = bool(cfg.get('ro_active', False))
163
+ current_ro = bool(getattr(fig, '_ro_active', False))
164
+ if file_ro != current_ro:
165
+ if file_ro:
166
+ print("Warning: XY style/geometry file was saved with --ro (swapped x/y axes); batch plot is not using --ro.")
167
+ else:
168
+ print("Warning: XY style/geometry file was saved without --ro; batch plot is treated as non-ro.")
169
+ print("Skipping style/geometry in batch mode to avoid corrupting axis orientation.")
170
+ return
171
+
160
172
  # Apply WASD state (tick visibility)
161
173
  wasd_cfg = cfg.get('wasd_state', {})
162
174
  if wasd_cfg:
@@ -223,6 +235,17 @@ def _apply_ec_style(fig, ax, cfg: dict):
223
235
  cfg: Style configuration dictionary
224
236
  """
225
237
  try:
238
+ # Enforce compatibility between style/geom ro state and current figure ro state.
239
+ file_ro = bool(cfg.get('ro_active', False))
240
+ current_ro = bool(getattr(fig, '_ro_active', False))
241
+ if file_ro != current_ro:
242
+ if file_ro:
243
+ print("Warning: EC style/geometry file was saved with --ro (swapped x/y axes); batch EC plot is not using --ro.")
244
+ else:
245
+ print("Warning: EC style/geometry file was saved without --ro; batch EC plot is treated as non-ro.")
246
+ print("Skipping EC style/geometry in batch mode to avoid corrupting axis orientation.")
247
+ return
248
+
226
249
  # Apply fonts
227
250
  font_cfg = cfg.get('font', {})
228
251
  if font_cfg:
batplot/batplot.py CHANGED
@@ -507,6 +507,11 @@ def batplot_main() -> int:
507
507
  except Exception:
508
508
  pass
509
509
  _plt.show(block=False)
510
+ # Track whether data axes were swapped via --ro for this EC figure
511
+ try:
512
+ fig._ro_active = bool(getattr(args, "ro", False))
513
+ except Exception:
514
+ pass
510
515
  try:
511
516
  fig._bp_source_paths = [_os.path.abspath(ec_file)]
512
517
  except Exception:
@@ -1785,9 +1790,40 @@ def batplot_main() -> int:
1785
1790
  if not isinstance(sess, dict) or 'version' not in sess:
1786
1791
  print("Not a valid batplot session file.")
1787
1792
  exit(1)
1793
+ except ModuleNotFoundError as e:
1794
+ # Handle numpy._core and other module import errors
1795
+ if '_core' in str(e) or 'numpy' in str(e).lower():
1796
+ print(f"\nERROR: NumPy version mismatch detected when loading: {sess_path}")
1797
+ print("This session was saved with a different NumPy version.")
1798
+ print("The error 'No module named numpy._core' indicates:")
1799
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
1800
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
1801
+ print("\nSolutions:")
1802
+ print(" 1. Check NumPy version: python3 -c 'import numpy; print(numpy.__version__)'")
1803
+ print(" 2. Install matching version:")
1804
+ print(" - If session was saved with NumPy 2.0+: pip install 'numpy>=2.0'")
1805
+ print(" - If session was saved with NumPy <2.0: pip install 'numpy<2.0'")
1806
+ print(" 3. Recreate the session from original data files")
1807
+ else:
1808
+ print(f"\nERROR: Module import error when loading: {sess_path}")
1809
+ print(f"Error: {e}")
1810
+ print("This usually indicates a package version mismatch.")
1811
+ print("Try installing matching package versions or recreate the session.")
1812
+ exit(1)
1788
1813
  except Exception as e:
1789
1814
  print(f"Failed to load session: {e}")
1790
1815
  exit(1)
1816
+
1817
+ # Check package version compatibility (if version info is available)
1818
+ try:
1819
+ from .session import _get_package_versions, _check_package_compatibility
1820
+ saved_versions = sess.get('package_versions', {})
1821
+ current_versions = _get_package_versions()
1822
+ if saved_versions and not _check_package_compatibility(saved_versions, current_versions, sess_path):
1823
+ exit(1)
1824
+ except Exception:
1825
+ # If compatibility checking fails, continue anyway (backward compatibility)
1826
+ pass
1791
1827
  # If it's an EC GC session, load and open EC interactive menu directly
1792
1828
  if isinstance(sess, dict) and sess.get('kind') == 'ec_gc':
1793
1829
  try:
@@ -1882,6 +1918,11 @@ def batplot_main() -> int:
1882
1918
  # Reconstruct minimal state and go to interactive if requested
1883
1919
  plt.ion() if args.interactive else None
1884
1920
  fig, ax = plt.subplots(figsize=(8,6))
1921
+ # Restore ro flag from session (if present) so style/geom imports can enforce compatibility
1922
+ try:
1923
+ fig._ro_active = bool(sess.get('ro_active', False))
1924
+ except Exception:
1925
+ pass
1885
1926
  y_data_list = []
1886
1927
  x_data_list = []
1887
1928
  labels_list = []
@@ -1890,10 +1931,31 @@ def batplot_main() -> int:
1890
1931
  x_full_list = []
1891
1932
  raw_y_full_list = []
1892
1933
  offsets_list = []
1893
- tick_state = {
1894
- 'bx': True,'tx': False,'ly': True,'ry': False,
1895
- 'mbx': False,'mtx': False,'mly': False,'mry': False
1896
- }
1934
+ # Load tick_state from wasd_state if available (version 2+), otherwise use defaults
1935
+ wasd_loaded = sess.get('wasd_state')
1936
+ if wasd_loaded and isinstance(wasd_loaded, dict):
1937
+ # Convert wasd_state to tick_state format
1938
+ tick_state = {}
1939
+ for side_key, prefix in [('top', 't'), ('bottom', 'b'), ('left', 'l'), ('right', 'r')]:
1940
+ s = wasd_loaded.get(side_key, {})
1941
+ tick_state[f'{prefix}_ticks'] = bool(s.get('ticks', side_key in ('bottom', 'left')))
1942
+ tick_state[f'{prefix}_labels'] = bool(s.get('labels', side_key in ('bottom', 'left')))
1943
+ tick_state[f'm{prefix}x' if prefix in 'tb' else f'm{prefix}y'] = bool(s.get('minor', False))
1944
+ # Legacy keys for backward compatibility
1945
+ tick_state['bx'] = tick_state.get('b_ticks', True)
1946
+ tick_state['tx'] = tick_state.get('t_ticks', False)
1947
+ tick_state['ly'] = tick_state.get('l_ticks', True)
1948
+ tick_state['ry'] = tick_state.get('r_ticks', False)
1949
+ tick_state['mbx'] = tick_state.get('mbx', False)
1950
+ tick_state['mtx'] = tick_state.get('mtx', False)
1951
+ tick_state['mly'] = tick_state.get('mly', False)
1952
+ tick_state['mry'] = tick_state.get('mry', False)
1953
+ else:
1954
+ # Fallback to legacy tick_state or defaults
1955
+ tick_state = sess.get('tick_state', {
1956
+ 'bx': True,'tx': False,'ly': True,'ry': False,
1957
+ 'mbx': False,'mtx': False,'mly': False,'mry': False
1958
+ })
1897
1959
  saved_stack = bool(sess.get('args_subset', {}).get('stack', False))
1898
1960
  # Pull data
1899
1961
  # --- Robust reconstruction of stored curves ---
@@ -1954,8 +2016,27 @@ def batplot_main() -> int:
1954
2016
  pass
1955
2017
  labels_list[:] = sess.get('labels', [f"Curve {i+1}" for i in range(len(y_data_list))])
1956
2018
  delta = sess.get('delta', 0.0)
2019
+ # Apply tick state (labels visibility) BEFORE setting axis labels
2020
+ try:
2021
+ ax.tick_params(axis='x',
2022
+ bottom=tick_state.get('b_ticks', tick_state.get('bx', True)),
2023
+ labelbottom=tick_state.get('b_labels', tick_state.get('bx', True)),
2024
+ top=tick_state.get('t_ticks', tick_state.get('tx', False)),
2025
+ labeltop=tick_state.get('t_labels', tick_state.get('tx', False)))
2026
+ ax.tick_params(axis='y',
2027
+ left=tick_state.get('l_ticks', tick_state.get('ly', True)),
2028
+ labelleft=tick_state.get('l_labels', tick_state.get('ly', True)),
2029
+ right=tick_state.get('r_ticks', tick_state.get('ry', False)),
2030
+ labelright=tick_state.get('r_labels', tick_state.get('ry', False)))
2031
+ except Exception:
2032
+ pass
1957
2033
  ax.set_xlabel(sess.get('axis', {}).get('xlabel', 'X'))
1958
2034
  ax.set_ylabel(sess.get('axis', {}).get('ylabel', 'Intensity'))
2035
+ # Store tick_state on axes for interactive menu
2036
+ try:
2037
+ ax._saved_tick_state = dict(tick_state)
2038
+ except Exception:
2039
+ pass
1959
2040
 
1960
2041
  # Restore normalization ranges (if saved)
1961
2042
  axis_cfg = sess.get('axis', {})
@@ -2933,19 +3014,21 @@ def batplot_main() -> int:
2933
3014
  offset += increment
2934
3015
 
2935
3016
  # ---- Plot curve ----
2936
- # Swap x and y if --ro flag is set
3017
+ # Swap x and y if --ro flag is set (and keep lists aligned once)
2937
3018
  if getattr(args, 'ro', False):
2938
- ax.plot(y_plot_offset, x_plot, "-", lw=1, alpha=0.8)
2939
- y_data_list.append(x_plot.copy())
2940
- x_data_list.append(y_plot_offset)
3019
+ x_plotted = y_plot_offset # goes on x-axis when rotated
3020
+ y_plotted = x_plot # goes on y-axis when rotated
2941
3021
  else:
3022
+ x_plotted = x_plot
3023
+ y_plotted = y_plot_offset
2942
3024
 
2943
- ax.plot(x_plot, y_plot_offset, "-", lw=1, alpha=0.8)
2944
- y_data_list.append(y_plot_offset.copy())
2945
- x_data_list.append(x_plot)
3025
+ ax.plot(x_plotted, y_plotted, "-", lw=1, alpha=0.8)
3026
+ x_data_list.append(x_plotted)
3027
+ y_data_list.append(y_plotted.copy())
2946
3028
  labels_list.append(label)
2947
3029
  # Store current normalized (subset) (used by rearrange logic)
2948
- orig_y.append(y_norm.copy())
3030
+ # Keep orig_y aligned with the plotted y data to avoid length mismatch on undo/relabel.
3031
+ orig_y.append(y_plotted.copy())
2949
3032
 
2950
3033
  # ---------------- Force axis to fit all data before labels ----------------
2951
3034
  ax.relim()
@@ -3458,6 +3541,12 @@ def batplot_main() -> int:
3458
3541
  fig.set_tight_layout(False)
3459
3542
  except Exception:
3460
3543
  pass
3544
+
3545
+ # Track whether data axes were swapped via --ro for this figure
3546
+ try:
3547
+ fig._ro_active = bool(getattr(args, "ro", False))
3548
+ except Exception:
3549
+ pass
3461
3550
 
3462
3551
  # Build CIF globals dict for explicit passing
3463
3552
  cif_globals = {
@@ -564,6 +564,8 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
564
564
  'frame_size': [frame_w_in, frame_h_in],
565
565
  'axes_fraction': [ax_bbox.x0, ax_bbox.y0, ax_bbox.width, ax_bbox.height]
566
566
  },
567
+ # Track whether data axes were swapped via --ro when this style was saved
568
+ 'ro_active': bool(getattr(fig, '_ro_active', False)),
567
569
  'font': {'family': fam0, 'size': fsize},
568
570
  'legend': {
569
571
  'visible': legend_visible,
@@ -1533,9 +1535,8 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1533
1535
  ax2.tick_params(axis='y', which='minor', right=True, labelright=False)
1534
1536
  else:
1535
1537
  ax2.tick_params(axis='y', which='minor', right=False, labelright=False)
1536
- # Position label spacings (bottom/left) for consistency
1537
- _ui_position_bottom_xlabel(ax, fig, tick_state)
1538
- _ui_position_left_ylabel(ax, fig, tick_state)
1538
+ # Note: Do NOT call position functions during undo restore as it causes title drift
1539
+ # Title offsets are already restored from snapshot in restore_state()
1539
1540
  try:
1540
1541
  for spine_name, color in getattr(fig, '_cpc_spine_colors', {}).items():
1541
1542
  _set_spine_color(spine_name, color)
@@ -2683,6 +2684,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
2683
2684
  kind = cfg.get('kind', '')
2684
2685
  if kind not in ('cpc_style', 'cpc_style_geom'):
2685
2686
  print("Not a CPC style file."); _print_menu(); continue
2687
+
2688
+ # Enforce compatibility between style/geom ro state and current figure ro state
2689
+ file_ro = bool(cfg.get('ro_active', False))
2690
+ current_ro = bool(getattr(fig, '_ro_active', False))
2691
+ if file_ro != current_ro:
2692
+ if file_ro:
2693
+ print("Warning: Style/geometry file was saved with --ro (swapped x/y axes); current plot is not using --ro.")
2694
+ else:
2695
+ print("Warning: Style/geometry file was saved without --ro; current plot was created with --ro.")
2696
+ print("Not applying CPC style/geometry to avoid corrupting axis orientation.")
2697
+ _print_menu(); continue
2686
2698
 
2687
2699
  has_geometry = (kind == 'cpc_style_geom' and 'geometry' in cfg)
2688
2700
 
@@ -3458,6 +3470,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3458
3470
  if not cmd:
3459
3471
  continue
3460
3472
  if cmd == 'q':
3473
+ # Update ax._saved_tick_state before exiting so changes are persisted
3474
+ try:
3475
+ ax._saved_tick_state = dict(tick_state)
3476
+ except Exception:
3477
+ pass
3461
3478
  break
3462
3479
  if cmd == 'i':
3463
3480
  # Invert tick direction (toggle between 'out' and 'in')
@@ -3779,6 +3796,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3779
3796
  tick_state['mry'] = bool(wasd['right']['minor'])
3780
3797
  if changed:
3781
3798
  push_state("wasd-toggle")
3799
+ # Update ax._saved_tick_state so dump_session can read it
3800
+ try:
3801
+ ax._saved_tick_state = dict(tick_state)
3802
+ except Exception:
3803
+ pass
3782
3804
  _apply_wasd(changed_sides if changed_sides else None)
3783
3805
  # Single draw at the end after all positioning is complete
3784
3806
  try:
@@ -1551,10 +1551,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1551
1551
  ax._right_ylabel_manual_offset_y_pts = 0.0
1552
1552
  ax._top_xlabel_on = bool(snap.get('titles',{}).get('top_x', False))
1553
1553
  ax._right_ylabel_on = bool(snap.get('titles',{}).get('right_y', False))
1554
- _ui_position_top_xlabel(ax, fig, tick_state)
1555
- _ui_position_bottom_xlabel(ax, fig, tick_state)
1556
- _ui_position_left_ylabel(ax, fig, tick_state)
1557
- _ui_position_right_ylabel(ax, fig, tick_state)
1554
+ # Note: Do NOT call position functions during undo restore as it causes title drift
1555
+ # Title offsets are already restored from snapshot above
1558
1556
  except Exception:
1559
1557
  pass
1560
1558
  # Restore labelpads (for title positioning)
@@ -2091,6 +2089,18 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2091
2089
  print("Not an EC style file.")
2092
2090
  _print_menu(len(all_cycles), is_dqdv)
2093
2091
  continue
2092
+
2093
+ # Enforce compatibility between style/geom ro state and current figure ro state
2094
+ file_ro = bool(cfg.get('ro_active', False))
2095
+ current_ro = bool(getattr(fig, '_ro_active', False))
2096
+ if file_ro != current_ro:
2097
+ if file_ro:
2098
+ print("Warning: EC style/geometry file was saved with --ro (swapped x/y axes); current plot is not using --ro.")
2099
+ else:
2100
+ print("Warning: EC style/geometry file was saved without --ro; current plot was created with --ro.")
2101
+ print("Not applying EC style/geometry to avoid corrupting axis orientation.")
2102
+ _print_menu(len(all_cycles), is_dqdv)
2103
+ continue
2094
2104
 
2095
2105
  has_geometry = (kind == 'ec_style_geom' and 'geometry' in cfg)
2096
2106
 
@@ -4231,6 +4241,8 @@ def _get_style_snapshot(fig, ax, cycle_lines: Dict, tick_state: Dict) -> Dict:
4231
4241
  'curve_markers': curve_marker_props,
4232
4242
  'rotation_angle': getattr(fig, '_ec_rotation_angle', 0),
4233
4243
  'cycle_styles': cycle_styles,
4244
+ # Track whether data axes were swapped via --ro when this style was saved
4245
+ 'ro_active': bool(getattr(fig, '_ro_active', False)),
4234
4246
  }
4235
4247
 
4236
4248
 
@@ -4322,6 +4334,10 @@ def _print_style_snapshot(cfg: Dict):
4322
4334
  if rotation_angle != 0:
4323
4335
  print(f"Rotation angle: {rotation_angle}°")
4324
4336
 
4337
+ # ro / axis-swap state
4338
+ ro_active = bool(cfg.get('ro_active', False))
4339
+ print(f"Data axes swapped via --ro: {'YES' if ro_active else 'no'}")
4340
+
4325
4341
  # Per-side matrix summary (spine, major, minor, labels, title)
4326
4342
  def _onoff(v):
4327
4343
  return 'ON ' if bool(v) else 'off'
batplot/interactive.py CHANGED
@@ -1218,8 +1218,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1218
1218
  fig.set_size_inches(snap["fig_size"][0], snap["fig_size"][1], forward=True)
1219
1219
  except Exception:
1220
1220
  pass
1221
- else:
1222
- print("(Canvas fixed) Ignoring undo figure size restore.")
1221
+ # No message needed - canvas size is managed by system
1223
1222
  # Don't restore DPI from undo - use system default to avoid display-dependent issues
1224
1223
 
1225
1224
  # Restore axes (plot frame) via stored bbox if present
@@ -1287,15 +1286,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1287
1286
  position_right_ylabel()
1288
1287
  except Exception:
1289
1288
  pass
1290
- # Also reposition bottom/left titles to consume pending pads and match tick label visibility
1291
- try:
1292
- position_bottom_xlabel()
1293
- except Exception:
1294
- pass
1295
- try:
1296
- position_left_ylabel()
1297
- except Exception:
1298
- pass
1289
+ # Note: Do NOT call position_bottom_xlabel() / position_left_ylabel() here
1290
+ # as it causes title drift when combined with fig.canvas.draw() below.
1291
+ # Title offsets are already restored from snapshot above.
1299
1292
 
1300
1293
  # Spines (linewidth, color, visibility)
1301
1294
  for name, spec in snap.get("spines", {}).items():
@@ -1737,7 +1730,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1737
1730
  show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1738
1731
  skip_confirm=skip_confirm,
1739
1732
  )
1740
- print(f"Saved session to {target_path}")
1733
+ # Message already printed by dump_session
1741
1734
  fig._last_session_save_path = target_path
1742
1735
  continue
1743
1736
  else:
@@ -1777,7 +1770,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1777
1770
  show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1778
1771
  skip_confirm=skip_confirm,
1779
1772
  )
1780
- print(f"Saved session to {target_path}")
1773
+ # Message already printed by dump_session
1781
1774
  fig._last_session_save_path = target_path
1782
1775
  except Exception as e:
1783
1776
  print(f"Error saving session: {e}")
@@ -2697,10 +2690,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2697
2690
  eps = abs(y_min)*1e-6 if y_min != 0 else 1e-6
2698
2691
  y_min -= eps
2699
2692
  y_max += eps
2700
- ax.set_ylim(y_min, y_max)
2693
+ ax.set_ylim(y_min, y_max)
2701
2694
  update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
2702
2695
  fig.canvas.draw_idle()
2703
- print(f"Y range set to {ax.get_ylim()}")
2696
+ ymin, ymax = ax.get_ylim()
2697
+ print(f"Y range set to ({float(ymin)}, {float(ymax)})")
2704
2698
  except Exception as e:
2705
2699
  print(f"Error setting Y-axis range: {e}")
2706
2700
  elif key == 'd': # <-- DELTA / OFFSET HANDLER (now only reachable if not args.stack)
@@ -3313,6 +3307,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3313
3307
  if not cmd:
3314
3308
  continue
3315
3309
  if cmd == 'q':
3310
+ # Update ax._saved_tick_state before exiting so changes are persisted
3311
+ try:
3312
+ ax._saved_tick_state = dict(tick_state)
3313
+ except Exception:
3314
+ pass
3316
3315
  break
3317
3316
  if cmd == 'i':
3318
3317
  # Invert tick direction (toggle between 'out' and 'in')
@@ -3584,6 +3583,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3584
3583
  continue
3585
3584
  # Unknown code
3586
3585
  print(f"Unknown code: {p}")
3586
+ # After tick toggles, update ax._saved_tick_state so dump_session can read it
3587
+ try:
3588
+ ax._saved_tick_state = dict(tick_state)
3589
+ except Exception:
3590
+ pass
3587
3591
  # After tick toggles, update visibility and reposition ALL axis labels for independence
3588
3592
  update_tick_visibility()
3589
3593
  update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
batplot/modes.py CHANGED
@@ -280,8 +280,8 @@ def handle_cv_mode(args) -> int:
280
280
  ax.set_xlabel('Current (mA)', labelpad=8.0)
281
281
  ax.set_ylabel('Voltage (V)', labelpad=8.0)
282
282
  else:
283
- ax.set_xlabel('Voltage (V)', labelpad=8.0)
284
- ax.set_ylabel('Current (mA)', labelpad=8.0)
283
+ ax.set_xlabel('Voltage (V)', labelpad=8.0)
284
+ ax.set_ylabel('Current (mA)', labelpad=8.0)
285
285
  legend = ax.legend(title='Cycle')
286
286
  legend.get_title().set_fontsize('medium')
287
287
  # Adjust margins to prevent label clipping
@@ -642,8 +642,8 @@ def handle_gc_mode(args) -> int:
642
642
  ln_c, = ax.plot(y_b, x_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
643
643
  linewidth=2.0, label=str(cyc), alpha=0.8)
644
644
  else:
645
- ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
646
- linewidth=2.0, label=str(cyc), alpha=0.8)
645
+ ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
646
+ linewidth=2.0, label=str(cyc), alpha=0.8)
647
647
  else:
648
648
  ln_c = None
649
649
  mask_d = (cyc_int == cyc) & discharge_mask
@@ -656,8 +656,8 @@ def handle_gc_mode(args) -> int:
656
656
  ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
657
657
  linewidth=2.0, label=lbl, alpha=0.8)
658
658
  else:
659
- ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
660
- linewidth=2.0, label=lbl, alpha=0.8)
659
+ ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
660
+ linewidth=2.0, label=lbl, alpha=0.8)
661
661
  else:
662
662
  ln_d = None
663
663
  cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
@@ -677,8 +677,8 @@ def handle_gc_mode(args) -> int:
677
677
  ln_c, = ax.plot(y_b, x_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
678
678
  linewidth=2.0, label=str(cyc), alpha=0.8)
679
679
  else:
680
- ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
681
- linewidth=2.0, label=str(cyc), alpha=0.8)
680
+ ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
681
+ linewidth=2.0, label=str(cyc), alpha=0.8)
682
682
  ln_d = None
683
683
  if i < len(dch_blocks):
684
684
  a, b = dch_blocks[i]
@@ -690,8 +690,8 @@ def handle_gc_mode(args) -> int:
690
690
  ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
691
691
  linewidth=2.0, label=lbl, alpha=0.8)
692
692
  else:
693
- ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
694
- linewidth=2.0, label=lbl, alpha=0.8)
693
+ ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
694
+ linewidth=2.0, label=lbl, alpha=0.8)
695
695
  cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
696
696
 
697
697
  # Swap x and y if --ro flag is set
@@ -699,8 +699,8 @@ def handle_gc_mode(args) -> int:
699
699
  ax.set_xlabel('Voltage (V)', labelpad=8.0)
700
700
  ax.set_ylabel(x_label_gc, labelpad=8.0)
701
701
  else:
702
- ax.set_xlabel(x_label_gc, labelpad=8.0)
703
- ax.set_ylabel('Voltage (V)', labelpad=8.0)
702
+ ax.set_xlabel(x_label_gc, labelpad=8.0)
703
+ ax.set_ylabel('Voltage (V)', labelpad=8.0)
704
704
  legend = ax.legend(title='Cycle')
705
705
  legend.get_title().set_fontsize('medium')
706
706
  fig.subplots_adjust(left=0.12, right=0.95, top=0.88, bottom=0.15)
@@ -354,7 +354,7 @@ def _draw_custom_colorbar(cbar_ax, im, label='Intensity', label_mode='normal'):
354
354
 
355
355
  def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
356
356
  """Update the custom colorbar when colormap or limits change.
357
-
357
+
358
358
  Args:
359
359
  cbar_ax: Axes object containing the colorbar
360
360
  im: Optional AxesImage object (if None, uses stored reference)
@@ -365,13 +365,13 @@ def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
365
365
  im = getattr(cbar_ax, '_colorbar_im', None)
366
366
  if im is None:
367
367
  return
368
-
368
+
369
369
  if label is None:
370
370
  label = getattr(cbar_ax, '_colorbar_label', 'Intensity')
371
-
371
+
372
372
  if label_mode is None:
373
373
  label_mode = getattr(cbar_ax, '_colorbar_label_mode', 'normal')
374
-
374
+
375
375
  # Redraw the colorbar
376
376
  _draw_custom_colorbar(cbar_ax, im, label, label_mode)
377
377