batplot 1.8.2__py3-none-any.whl → 1.8.4__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.
batplot/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.8.2"
3
+ __version__ = "1.8.4"
4
4
 
5
5
  __all__ = ["__version__"]
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,6 +1790,38 @@ 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
+ # Try to extract version info before the error
1797
+ from .session import _try_extract_version_from_pickle, _get_current_numpy_version
1798
+ saved_versions = _try_extract_version_from_pickle(sess_path)
1799
+ current_numpy = _get_current_numpy_version()
1800
+
1801
+ saved_numpy = saved_versions.get('numpy', 'unknown')
1802
+
1803
+ print(f"\nERROR: NumPy version mismatch detected when loading: {sess_path}")
1804
+ print("This session was saved with a different NumPy version.")
1805
+ print()
1806
+ print(f"Session was saved with: NumPy {saved_numpy}")
1807
+ print(f"Currently installed: NumPy {current_numpy}")
1808
+ print()
1809
+ print("The error 'No module named numpy._core' indicates:")
1810
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
1811
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
1812
+ print()
1813
+ print("Solutions:")
1814
+ if saved_numpy != 'unknown':
1815
+ print(f" 1. Install matching version: pip install 'numpy=={saved_numpy}'")
1816
+ else:
1817
+ print(" 1. Try installing NumPy <2.0: pip install 'numpy<2.0'")
1818
+ print(" OR try installing NumPy 2.0+: pip install 'numpy>=2.0'")
1819
+ print(" 2. Recreate the session from original data files")
1820
+ else:
1821
+ print(f"\nERROR: Module import error when loading: {sess_path}")
1822
+ print(f"Error: {e}")
1823
+ print("This usually indicates a package version mismatch.")
1824
+ exit(1)
1788
1825
  except Exception as e:
1789
1826
  print(f"Failed to load session: {e}")
1790
1827
  exit(1)
@@ -1882,6 +1919,11 @@ def batplot_main() -> int:
1882
1919
  # Reconstruct minimal state and go to interactive if requested
1883
1920
  plt.ion() if args.interactive else None
1884
1921
  fig, ax = plt.subplots(figsize=(8,6))
1922
+ # Restore ro flag from session (if present) so style/geom imports can enforce compatibility
1923
+ try:
1924
+ fig._ro_active = bool(sess.get('ro_active', False))
1925
+ except Exception:
1926
+ pass
1885
1927
  y_data_list = []
1886
1928
  x_data_list = []
1887
1929
  labels_list = []
@@ -2973,19 +3015,21 @@ def batplot_main() -> int:
2973
3015
  offset += increment
2974
3016
 
2975
3017
  # ---- Plot curve ----
2976
- # Swap x and y if --ro flag is set
3018
+ # Swap x and y if --ro flag is set (and keep lists aligned once)
2977
3019
  if getattr(args, 'ro', False):
2978
- ax.plot(y_plot_offset, x_plot, "-", lw=1, alpha=0.8)
2979
- y_data_list.append(x_plot.copy())
2980
- x_data_list.append(y_plot_offset)
3020
+ x_plotted = y_plot_offset # goes on x-axis when rotated
3021
+ y_plotted = x_plot # goes on y-axis when rotated
2981
3022
  else:
3023
+ x_plotted = x_plot
3024
+ y_plotted = y_plot_offset
2982
3025
 
2983
- ax.plot(x_plot, y_plot_offset, "-", lw=1, alpha=0.8)
2984
- y_data_list.append(y_plot_offset.copy())
2985
- x_data_list.append(x_plot)
3026
+ ax.plot(x_plotted, y_plotted, "-", lw=1, alpha=0.8)
3027
+ x_data_list.append(x_plotted)
3028
+ y_data_list.append(y_plotted.copy())
2986
3029
  labels_list.append(label)
2987
3030
  # Store current normalized (subset) (used by rearrange logic)
2988
- orig_y.append(y_norm.copy())
3031
+ # Keep orig_y aligned with the plotted y data to avoid length mismatch on undo/relabel.
3032
+ orig_y.append(y_plotted.copy())
2989
3033
 
2990
3034
  # ---------------- Force axis to fit all data before labels ----------------
2991
3035
  ax.relim()
@@ -3498,6 +3542,12 @@ def batplot_main() -> int:
3498
3542
  fig.set_tight_layout(False)
3499
3543
  except Exception:
3500
3544
  pass
3545
+
3546
+ # Track whether data axes were swapped via --ro for this figure
3547
+ try:
3548
+ fig._ro_active = bool(getattr(args, "ro", False))
3549
+ except Exception:
3550
+ pass
3501
3551
 
3502
3552
  # Build CIF globals dict for explicit passing
3503
3553
  cif_globals = {
batplot/cli.py CHANGED
@@ -59,6 +59,20 @@ def main(argv: Optional[list] = None) -> int:
59
59
  >>> main()
60
60
  0
61
61
  """
62
+ # ====================================================================
63
+ # STEP 0: PYTHON VERSION CHECK
64
+ # ====================================================================
65
+ # Check if Python version is 3.13 (required for batplot)
66
+ # ====================================================================
67
+ if sys.version_info.major != 3 or sys.version_info.minor != 13:
68
+ current_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
69
+ print(f"\n⚠️ WARNING: Python version mismatch detected!")
70
+ print(f" batplot requires Python 3.13")
71
+ print(f" Currently running: Python {current_version}")
72
+ print(f"\n This may cause compatibility issues.")
73
+ print(f" Please install Python 3.13 and use it to run batplot.")
74
+ print(f" Continuing anyway, but expect potential issues...\n")
75
+
62
76
  # ====================================================================
63
77
  # STEP 1: VERSION CHECK (NON-BLOCKING)
64
78
  # ====================================================================
@@ -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
 
@@ -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)
batplot/session.py CHANGED
@@ -41,6 +41,68 @@ from .utils import _confirm_overwrite
41
41
  from .color_utils import ensure_colormap
42
42
 
43
43
 
44
+ def _try_extract_version_from_pickle(filename: str) -> Dict[str, str]:
45
+ """Try to extract package_versions from a pickle file even if it fails to fully load.
46
+
47
+ Note: This may not work if pickle.load() fails completely due to missing modules.
48
+ In that case, we can't extract version info, but we can still show current version.
49
+
50
+ Returns:
51
+ dict with package versions, or empty dict if extraction fails
52
+ """
53
+ try:
54
+ with open(filename, 'rb') as f:
55
+ # Try to load the pickle
56
+ # This will fail if numpy._core is missing, but we try anyway
57
+ sess = pickle.load(f)
58
+ if isinstance(sess, dict):
59
+ return sess.get('package_versions', {})
60
+ except Exception:
61
+ # If loading fails completely (e.g., ModuleNotFoundError for numpy._core),
62
+ # we can't extract version info. This is expected in version mismatch cases.
63
+ pass
64
+ return {}
65
+
66
+
67
+ def _get_current_numpy_version() -> str:
68
+ """Get current numpy version, even if import fails.
69
+
70
+ Tries multiple methods:
71
+ 1. Direct import (fastest)
72
+ 2. pip show (works even if import fails)
73
+ 3. Returns 'unknown' if all fail
74
+
75
+ Returns:
76
+ Version string or 'unknown'
77
+ """
78
+ # Method 1: Try direct import
79
+ try:
80
+ import numpy
81
+ return numpy.__version__
82
+ except Exception:
83
+ pass
84
+
85
+ # Method 2: Try pip show
86
+ try:
87
+ import subprocess
88
+ import sys
89
+ result = subprocess.run(
90
+ [sys.executable, '-m', 'pip', 'show', 'numpy'],
91
+ capture_output=True,
92
+ text=True,
93
+ timeout=5,
94
+ check=False
95
+ )
96
+ if result.returncode == 0:
97
+ for line in result.stdout.split('\n'):
98
+ if line.startswith('Version:'):
99
+ return line.split(':', 1)[1].strip()
100
+ except Exception:
101
+ pass
102
+
103
+ return 'unknown'
104
+
105
+
44
106
  def _current_tick_width(axis_obj, which: str):
45
107
  """
46
108
  Return the configured tick width for the given X/Y axis.
@@ -507,6 +569,8 @@ def dump_session(
507
569
  }
508
570
  # Save curve names visibility
509
571
  sess['curve_names_visible'] = bool(getattr(fig, '_curve_names_visible', True))
572
+ # Save whether data were plotted with swapped axes via --ro
573
+ sess['ro_active'] = bool(getattr(fig, '_ro_active', False))
510
574
  # Save stack/legend anchor preferences
511
575
  sess['stack_label_at_bottom'] = bool(getattr(fig, '_stack_label_at_bottom', False))
512
576
  sess['label_anchor_left'] = bool(getattr(fig, '_label_anchor_left', False))
@@ -819,6 +883,37 @@ def load_operando_session(filename: str):
819
883
  try:
820
884
  with open(filename, 'rb') as f:
821
885
  sess = pickle.load(f)
886
+ except ModuleNotFoundError as e:
887
+ # Handle numpy._core and other module import errors
888
+ if '_core' in str(e) or 'numpy' in str(e).lower():
889
+ # Try to extract version info before the error
890
+ saved_versions = _try_extract_version_from_pickle(filename)
891
+ current_numpy = _get_current_numpy_version()
892
+
893
+ saved_numpy = saved_versions.get('numpy', 'unknown')
894
+
895
+ print(f"\nERROR: NumPy version mismatch detected when loading: {filename}")
896
+ print("This session was saved with a different NumPy version.")
897
+ print()
898
+ print(f"Session was saved with: NumPy {saved_numpy}")
899
+ print(f"Currently installed: NumPy {current_numpy}")
900
+ print()
901
+ print("The error 'No module named numpy._core' indicates:")
902
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
903
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
904
+ print()
905
+ print("Solutions:")
906
+ if saved_numpy != 'unknown':
907
+ print(f" 1. Install matching version: pip install 'numpy=={saved_numpy}'")
908
+ else:
909
+ print(" 1. Try installing NumPy <2.0: pip install 'numpy<2.0'")
910
+ print(" OR try installing NumPy 2.0+: pip install 'numpy>=2.0'")
911
+ print(" 2. Recreate the session from original data files")
912
+ else:
913
+ print(f"\nERROR: Module import error when loading: {filename}")
914
+ print(f"Error: {e}")
915
+ print("This usually indicates a package version mismatch.")
916
+ return None
822
917
  except Exception as e:
823
918
  print(f"Failed to load session: {e}")
824
919
  return None
@@ -1657,6 +1752,37 @@ def load_ec_session(filename: str):
1657
1752
  try:
1658
1753
  with open(filename, 'rb') as f:
1659
1754
  sess = pickle.load(f)
1755
+ except ModuleNotFoundError as e:
1756
+ # Handle numpy._core and other module import errors
1757
+ if '_core' in str(e) or 'numpy' in str(e).lower():
1758
+ # Try to extract version info before the error
1759
+ saved_versions = _try_extract_version_from_pickle(filename)
1760
+ current_numpy = _get_current_numpy_version()
1761
+
1762
+ saved_numpy = saved_versions.get('numpy', 'unknown')
1763
+
1764
+ print(f"\nERROR: NumPy version mismatch detected when loading: {filename}")
1765
+ print("This session was saved with a different NumPy version.")
1766
+ print()
1767
+ print(f"Session was saved with: NumPy {saved_numpy}")
1768
+ print(f"Currently installed: NumPy {current_numpy}")
1769
+ print()
1770
+ print("The error 'No module named numpy._core' indicates:")
1771
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
1772
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
1773
+ print()
1774
+ print("Solutions:")
1775
+ if saved_numpy != 'unknown':
1776
+ print(f" 1. Install matching version: pip install 'numpy=={saved_numpy}'")
1777
+ else:
1778
+ print(" 1. Try installing NumPy <2.0: pip install 'numpy<2.0'")
1779
+ print(" OR try installing NumPy 2.0+: pip install 'numpy>=2.0'")
1780
+ print(" 2. Recreate the session from original data files")
1781
+ else:
1782
+ print(f"\nERROR: Module import error when loading: {filename}")
1783
+ print(f"Error: {e}")
1784
+ print("This usually indicates a package version mismatch.")
1785
+ return None
1660
1786
  except Exception as e:
1661
1787
  print(f"Failed to load EC session: {e}")
1662
1788
  return None
@@ -2526,6 +2652,37 @@ def load_cpc_session(filename: str):
2526
2652
  try:
2527
2653
  with open(filename, 'rb') as f:
2528
2654
  sess = pickle.load(f)
2655
+ except ModuleNotFoundError as e:
2656
+ # Handle numpy._core and other module import errors
2657
+ if '_core' in str(e) or 'numpy' in str(e).lower():
2658
+ # Try to extract version info before the error
2659
+ saved_versions = _try_extract_version_from_pickle(filename)
2660
+ current_numpy = _get_current_numpy_version()
2661
+
2662
+ saved_numpy = saved_versions.get('numpy', 'unknown')
2663
+
2664
+ print(f"\nERROR: NumPy version mismatch detected when loading: {filename}")
2665
+ print("This session was saved with a different NumPy version.")
2666
+ print()
2667
+ print(f"Session was saved with: NumPy {saved_numpy}")
2668
+ print(f"Currently installed: NumPy {current_numpy}")
2669
+ print()
2670
+ print("The error 'No module named numpy._core' indicates:")
2671
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
2672
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
2673
+ print()
2674
+ print("Solutions:")
2675
+ if saved_numpy != 'unknown':
2676
+ print(f" 1. Install matching version: pip install 'numpy=={saved_numpy}'")
2677
+ else:
2678
+ print(" 1. Try installing NumPy <2.0: pip install 'numpy<2.0'")
2679
+ print(" OR try installing NumPy 2.0+: pip install 'numpy>=2.0'")
2680
+ print(" 2. Recreate the session from original data files")
2681
+ else:
2682
+ print(f"\nERROR: Module import error when loading: {filename}")
2683
+ print(f"Error: {e}")
2684
+ print("This usually indicates a package version mismatch.")
2685
+ return None
2529
2686
  except Exception as e:
2530
2687
  print(f"Failed to load session: {e}")
2531
2688
  return None
batplot/style.py CHANGED
@@ -358,6 +358,13 @@ def print_style_info(
358
358
  print(f"Font family chain (rcParams['font.sans-serif']): {plt.rcParams.get('font.sans-serif')}")
359
359
  print(f"Mathtext fontset: {plt.rcParams.get('mathtext.fontset')}")
360
360
 
361
+ # Report whether data axes were swapped via --ro when this figure was created
362
+ try:
363
+ ro_active = bool(getattr(fig, "_ro_active", False))
364
+ except Exception:
365
+ ro_active = False
366
+ print(f"Data axes swapped via --ro: {'YES' if ro_active else 'no'}")
367
+
361
368
  # Rotation angle
362
369
  rotation_angle = getattr(ax, '_rotation_angle', 0)
363
370
  if rotation_angle != 0:
@@ -646,6 +653,8 @@ def export_style_config(
646
653
  }
647
654
  # Save rotation angle
648
655
  cfg["rotation_angle"] = getattr(ax, '_rotation_angle', 0)
656
+ # Track whether data axes were swapped via --ro when this style was saved
657
+ cfg["ro_active"] = bool(getattr(fig, '_ro_active', False))
649
658
 
650
659
  # Save curve names visibility
651
660
  cfg["curve_names_visible"] = True # Default to visible
@@ -853,6 +862,17 @@ def apply_style_config(
853
862
  except Exception as e:
854
863
  print(f"Could not read config: {e}")
855
864
  return
865
+ # Enforce compatibility between style/geometry ro state and current figure ro state.
866
+ # Styles saved from a plot using --ro (swapped x/y) must not be applied to a non-ro plot, and vice versa.
867
+ file_ro = bool(cfg.get("ro_active", False))
868
+ current_ro = bool(getattr(fig, "_ro_active", False))
869
+ if file_ro != current_ro:
870
+ if file_ro:
871
+ print("Warning: Style/geometry file was saved with --ro (swapped x/y axes); current plot is not using --ro.")
872
+ else:
873
+ print("Warning: Style/geometry file was saved without --ro; current plot was created with --ro.")
874
+ print("Not applying style/geometry to avoid corrupting axis orientation.")
875
+ return
856
876
  # Save current labelpad values BEFORE any style changes
857
877
  saved_xlabelpad = None
858
878
  saved_ylabelpad = None
@@ -878,8 +898,7 @@ def apply_style_config(
878
898
  if not keep_canvas_fixed:
879
899
  # Use forward=False to prevent automatic subplot adjustment that can shift the plot
880
900
  fig.set_size_inches(fw, fh, forward=False)
881
- else:
882
- print("(Canvas fixed) Ignoring style figure size request.")
901
+ # No message needed when canvas is fixed - this is normal behavior
883
902
  except Exception as e:
884
903
  print(f"Warning: could not parse figure size: {e}")
885
904
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.8.2
3
+ Version: 1.8.4
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
6
  License: MIT License
@@ -31,20 +31,15 @@ Project-URL: Issues, https://github.com/TianDai1729/batplot/issues
31
31
  Classifier: Programming Language :: Python
32
32
  Classifier: Programming Language :: Python :: 3
33
33
  Classifier: Programming Language :: Python :: 3 :: Only
34
- Classifier: Programming Language :: Python :: 3.8
35
- Classifier: Programming Language :: Python :: 3.9
36
- Classifier: Programming Language :: Python :: 3.10
37
- Classifier: Programming Language :: Python :: 3.11
38
- Classifier: Programming Language :: Python :: 3.12
39
34
  Classifier: Programming Language :: Python :: 3.13
40
35
  Classifier: Operating System :: OS Independent
41
36
  Classifier: Intended Audience :: Science/Research
42
37
  Classifier: Topic :: Scientific/Engineering :: Visualization
43
- Requires-Python: >=3.7
38
+ Requires-Python: <3.14,>=3.13
44
39
  Description-Content-Type: text/markdown
45
40
  License-File: LICENSE
46
- Requires-Dist: numpy
47
- Requires-Dist: matplotlib
41
+ Requires-Dist: numpy==2.3.4
42
+ Requires-Dist: matplotlib==3.10.7
48
43
  Requires-Dist: rich>=10.0.0
49
44
  Requires-Dist: openpyxl>=3.0.0
50
45
  Dynamic: license-file
@@ -1,28 +1,28 @@
1
- batplot/__init__.py,sha256=cmYE6SKId-NqANj2k5EUEXFUnzP58FNBG7XRF6CLfgQ,118
1
+ batplot/__init__.py,sha256=A2c5RdJQEC4-Snfk1leoeq38dCyGoAxBuEvXo3Kb50A,118
2
2
  batplot/args.py,sha256=mrDjMURp_OQnXrAwl9WnE_FW4HJu940O7NmL9QWQnD4,35189
3
- batplot/batch.py,sha256=YQ7obCIqLCObwDbM7TXpOBh7g7BO95wZNsa2Fy84c6o,53858
4
- batplot/batplot.py,sha256=Avb0Txs-VaG1UU-bL-7HWnZf_8RS_n-KTnZz9O_3GPA,174374
3
+ batplot/batch.py,sha256=P2HNBYqrSzDr2fruYz8BFKwNFX6vdJpHDZpaG_hmEc8,55253
4
+ batplot/batplot.py,sha256=IDnc-aGIMo1oMfXsQjU_koKqq2E9xXNn1Bm92xsmeYE,177109
5
5
  batplot/cif.py,sha256=JfHwNf3SHrcpALc_F5NjJmQ3lg71MBRSaIUJjGYPTx8,30120
6
- batplot/cli.py,sha256=ScDb2je8VQ0mz_z0SLCHEigiTuFPY5pb1snnzCouKms,5828
6
+ batplot/cli.py,sha256=2-7NtxBlyOUfzHVwP7vZK7OZlyKyPVy-3daKN-MPWyU,6657
7
7
  batplot/color_utils.py,sha256=7InQLVo1XTg7sgAbltM2KeDSFJgr787YEaV9vJbIoWY,20460
8
8
  batplot/config.py,sha256=6nGY7fKN4T5KZUGQS2ArUBgEkLAL0j37XwG5SCVQgKA,6420
9
9
  batplot/converters.py,sha256=rR2WMPM0nR5E3eZI3gWbaJf_AfbdQx3urVSbJmZXNzo,8237
10
- batplot/cpc_interactive.py,sha256=r70RVu75QAPEWkK_bR4c3MWDXNSwHYxOAYGEVf0TgUg,238755
11
- batplot/electrochem_interactive.py,sha256=ti7V8BoAxUk4BD_vDRKAu5ydlHMl75htLvdVYFUUVsw,221778
12
- batplot/interactive.py,sha256=2hQrvV-7VEz0cscIb3Bc7qQ9MpRxP4hjZ6raEnpl5yg,206779
10
+ batplot/cpc_interactive.py,sha256=qadWV2PaQMsqM16mSp5r1-WP7di0JCnzNI4RJy27alo,239616
11
+ batplot/electrochem_interactive.py,sha256=8mFr5vtWb_ZDiJmZWXjkARq21D2GflT8-J2xvCGvDIc,222769
12
+ batplot/interactive.py,sha256=5u2ulhTzRr7fA-INc5hfcz7xaHObnnigiXjrgdtE7XE,206728
13
13
  batplot/manual.py,sha256=pbRI6G4Pm12pOW8LrOLWWu7IEOtqWN3tRHtgge50LlA,11556
14
14
  batplot/modes.py,sha256=qE2OsOQQKhwOWene5zxJeuuewTrZxubtahQuz5je7ok,37252
15
15
  batplot/operando.py,sha256=p2Ug1mFUQxaU702cTBGgJKb3_v1C2p3LLUwfXaVBpPY,28311
16
16
  batplot/operando_ec_interactive.py,sha256=8GQ47-I8SLTS88sFEk8m3vDxFEjSfD3hao62Qke7SxA,305137
17
17
  batplot/plotting.py,sha256=hG2_EdDhF1Qpn1XfZKdCQ5-w_m9gUYFbr804UQ5QjsU,10841
18
18
  batplot/readers.py,sha256=kAI0AvYrdfGRZkvADJ4riN96IWtrH24aAoZpBtONTbw,112960
19
- batplot/session.py,sha256=WuBZR-HKDr4VlqjwynyK8gCkx6xLoHzNaGBH948p9jU,135638
20
- batplot/style.py,sha256=ig1ozX4dhEsXf5JKaPZOvgVS3CWx-BTFSc3vfAH3Y-E,62274
19
+ batplot/session.py,sha256=sYhY3WH-Kks5SMO91kfGDkSs32SX_iFS62ZkUm3jwzY,142722
20
+ batplot/style.py,sha256=jXtFaJR1aa6vIHupmDNqY2NY5Rgtw49UxF7cS4y8fCA,63375
21
21
  batplot/ui.py,sha256=ifpbK74juUzLMCt-sJGVaWtpDb1NMRJzs2YyiwwafzY,35302
22
22
  batplot/utils.py,sha256=LY2-Axr3DsQMTxuXe48vSjrLJKEnkzkZjdSFdQizbpg,37599
23
23
  batplot/version_check.py,sha256=--U_74DKgHbGtVdBsg9DfUJ10S5-OMXT-rzaYjK0JBc,9997
24
24
  batplot/data/USER_MANUAL.md,sha256=VYPvNZt3Fy8Z4Izr2FnQBw9vEaFTPkybhHDnF-OuKws,17694
25
- batplot-1.8.2.dist-info/licenses/LICENSE,sha256=2PAnHeCiTfgI7aKZLWr0G56HI9fGKQ0CEbQ02H-yExQ,1065
25
+ batplot-1.8.4.dist-info/licenses/LICENSE,sha256=2PAnHeCiTfgI7aKZLWr0G56HI9fGKQ0CEbQ02H-yExQ,1065
26
26
  batplot_backup_20251121_223043/__init__.py,sha256=3s2DUQuTbWs65hoN9cQQ8IiJbaFJY8fNxiCpwRBYoOA,118
27
27
  batplot_backup_20251121_223043/args.py,sha256=OH-h84QhN-IhMS8sPAsSEqccHD3wpeMgmXa_fqv5xtg,21215
28
28
  batplot_backup_20251121_223043/batch.py,sha256=oI7PONJyciHDOqNPq-8fnOQMyn9CpAdVznKaEdsy0ig,48650
@@ -68,8 +68,8 @@ batplot_backup_20251221_101150/style.py,sha256=ig1ozX4dhEsXf5JKaPZOvgVS3CWx-BTFS
68
68
  batplot_backup_20251221_101150/ui.py,sha256=ifpbK74juUzLMCt-sJGVaWtpDb1NMRJzs2YyiwwafzY,35302
69
69
  batplot_backup_20251221_101150/utils.py,sha256=LY2-Axr3DsQMTxuXe48vSjrLJKEnkzkZjdSFdQizbpg,37599
70
70
  batplot_backup_20251221_101150/version_check.py,sha256=ztTHwqgWd8OlS9PLLY5A_TabWxBASDA_-5yyN15PZC8,9996
71
- batplot-1.8.2.dist-info/METADATA,sha256=ZNkBWCjEFRJVKuq1FtQ9JCovRWnEm3lmqWs8eYsvMzU,7406
72
- batplot-1.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- batplot-1.8.2.dist-info/entry_points.txt,sha256=73GgH3Zs-qGIvgiyQLgGsSW-ryOwPPKHveOW6TDIR5Q,82
74
- batplot-1.8.2.dist-info/top_level.txt,sha256=Z5Q4sAiT_FDqZqhlLsYn9avRTuFAEEf3AVfkswxOb18,70
75
- batplot-1.8.2.dist-info/RECORD,,
71
+ batplot-1.8.4.dist-info/METADATA,sha256=da51wuDhz7bqCTInJ_tjZzZy2wC6ujSfsrgYIi-GYgI,7175
72
+ batplot-1.8.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
+ batplot-1.8.4.dist-info/entry_points.txt,sha256=73GgH3Zs-qGIvgiyQLgGsSW-ryOwPPKHveOW6TDIR5Q,82
74
+ batplot-1.8.4.dist-info/top_level.txt,sha256=Z5Q4sAiT_FDqZqhlLsYn9avRTuFAEEf3AVfkswxOb18,70
75
+ batplot-1.8.4.dist-info/RECORD,,