batplot 1.7.26__tar.gz → 1.7.28__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.26/batplot.egg-info → batplot-1.7.28}/PKG-INFO +1 -1
  2. {batplot-1.7.26 → batplot-1.7.28}/batplot/__init__.py +1 -1
  3. {batplot-1.7.26 → batplot-1.7.28}/batplot/cpc_interactive.py +4 -0
  4. {batplot-1.7.26 → batplot-1.7.28}/batplot/electrochem_interactive.py +4 -0
  5. {batplot-1.7.26 → batplot-1.7.28}/batplot/interactive.py +4 -0
  6. {batplot-1.7.26 → batplot-1.7.28}/batplot/operando.py +3 -3
  7. {batplot-1.7.26 → batplot-1.7.28}/batplot/operando_ec_interactive.py +89 -2
  8. {batplot-1.7.26 → batplot-1.7.28}/batplot/session.py +18 -0
  9. {batplot-1.7.26 → batplot-1.7.28}/batplot/style.py +4 -0
  10. {batplot-1.7.26 → batplot-1.7.28}/batplot/utils.py +48 -0
  11. {batplot-1.7.26 → batplot-1.7.28/batplot.egg-info}/PKG-INFO +1 -1
  12. {batplot-1.7.26 → batplot-1.7.28}/pyproject.toml +1 -1
  13. {batplot-1.7.26 → batplot-1.7.28}/LICENSE +0 -0
  14. {batplot-1.7.26 → batplot-1.7.28}/MANIFEST.in +0 -0
  15. {batplot-1.7.26 → batplot-1.7.28}/README.md +0 -0
  16. {batplot-1.7.26 → batplot-1.7.28}/USER_MANUAL.md +0 -0
  17. {batplot-1.7.26 → batplot-1.7.28}/batplot/args.py +0 -0
  18. {batplot-1.7.26 → batplot-1.7.28}/batplot/batch.py +0 -0
  19. {batplot-1.7.26 → batplot-1.7.28}/batplot/batplot.py +0 -0
  20. {batplot-1.7.26 → batplot-1.7.28}/batplot/cif.py +0 -0
  21. {batplot-1.7.26 → batplot-1.7.28}/batplot/cli.py +0 -0
  22. {batplot-1.7.26 → batplot-1.7.28}/batplot/color_utils.py +0 -0
  23. {batplot-1.7.26 → batplot-1.7.28}/batplot/config.py +0 -0
  24. {batplot-1.7.26 → batplot-1.7.28}/batplot/converters.py +0 -0
  25. {batplot-1.7.26 → batplot-1.7.28}/batplot/data/USER_MANUAL.md +0 -0
  26. {batplot-1.7.26 → batplot-1.7.28}/batplot/manual.py +0 -0
  27. {batplot-1.7.26 → batplot-1.7.28}/batplot/modes.py +0 -0
  28. {batplot-1.7.26 → batplot-1.7.28}/batplot/plotting.py +0 -0
  29. {batplot-1.7.26 → batplot-1.7.28}/batplot/readers.py +0 -0
  30. {batplot-1.7.26 → batplot-1.7.28}/batplot/ui.py +0 -0
  31. {batplot-1.7.26 → batplot-1.7.28}/batplot/version_check.py +0 -0
  32. {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/SOURCES.txt +0 -0
  33. {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/dependency_links.txt +0 -0
  34. {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/entry_points.txt +0 -0
  35. {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/requires.txt +0 -0
  36. {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/top_level.txt +0 -0
  37. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/__init__.py +0 -0
  38. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/args.py +0 -0
  39. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/batch.py +0 -0
  40. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/batplot.py +0 -0
  41. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/cif.py +0 -0
  42. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/cli.py +0 -0
  43. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/color_utils.py +0 -0
  44. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/config.py +0 -0
  45. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/converters.py +0 -0
  46. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
  47. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
  48. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/interactive.py +0 -0
  49. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/modes.py +0 -0
  50. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/operando.py +0 -0
  51. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
  52. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/plotting.py +0 -0
  53. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/readers.py +0 -0
  54. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/session.py +0 -0
  55. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/style.py +0 -0
  56. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/ui.py +0 -0
  57. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/utils.py +0 -0
  58. {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/version_check.py +0 -0
  59. {batplot-1.7.26 → batplot-1.7.28}/setup.cfg +0 -0
  60. {batplot-1.7.26 → batplot-1.7.28}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.7.26
3
+ Version: 1.7.28
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
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.7.26"
3
+ __version__ = "1.7.28"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -2090,6 +2090,10 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
2090
2090
  if yn != 'y':
2091
2091
  _print_menu(); continue
2092
2092
  if target:
2093
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
2094
+ from .utils import ensure_exact_case_filename
2095
+ target = ensure_exact_case_filename(target)
2096
+
2093
2097
  # Save current legend position before export (savefig can change layout)
2094
2098
  saved_legend_pos = None
2095
2099
  try:
@@ -1684,6 +1684,10 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1684
1684
  if not already_confirmed and os.path.exists(target):
1685
1685
  target = _confirm_overwrite(target)
1686
1686
  if target:
1687
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
1688
+ from .utils import ensure_exact_case_filename
1689
+ target = ensure_exact_case_filename(target)
1690
+
1687
1691
  # Save current legend position before export (savefig can change layout)
1688
1692
  saved_legend_pos = None
1689
1693
  try:
@@ -3709,6 +3709,10 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3709
3709
  if not export_target:
3710
3710
  print("Export canceled.")
3711
3711
  else:
3712
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
3713
+ from .utils import ensure_exact_case_filename
3714
+ export_target = ensure_exact_case_filename(export_target)
3715
+
3712
3716
  # Temporarily remove numbering for export
3713
3717
  for i, txt in enumerate(label_text_objects):
3714
3718
  txt.set_text(labels[i])
@@ -386,7 +386,7 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
386
386
  x_data, y_data, current_mA, x_label, y_label = result
387
387
  # For EC-Lab files: x_label='Time (h)', y_label='Voltage (V)'
388
388
  # For simple files: x_label could be 'Time(h)', 'time', etc.
389
- # EC-Lab returns time in seconds, needs conversion to hours
389
+ # EC-Lab files: read_mpt_file already converts time from seconds to hours
390
390
  # operando plots with voltage on X-axis and time on Y-axis
391
391
 
392
392
  # Check if labels indicate time/voltage data (flexible matching)
@@ -400,8 +400,8 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
400
400
  is_time_voltage = (has_time_in_x or has_time_in_y) and (has_voltage_in_x or has_voltage_in_y)
401
401
 
402
402
  if x_label == 'Time (h)' and y_label == 'Voltage (V)':
403
- # EC-Lab file: convert time to hours and swap axes
404
- time_h = np.asarray(x_data, float) / 3600.0
403
+ # EC-Lab file: time is already in hours from read_mpt_file, just swap axes
404
+ time_h = np.asarray(x_data, float) # Already in hours, no conversion needed
405
405
  voltage_v = np.asarray(y_data, float)
406
406
  x_data = voltage_v
407
407
  y_data = time_h
@@ -344,6 +344,64 @@ def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
344
344
  _draw_custom_colorbar(cbar_ax, im, label, label_mode)
345
345
 
346
346
 
347
+ def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
348
+ """Detach a Matplotlib Colorbar from its mappable callbacks.
349
+
350
+ Why this exists:
351
+ - In this interactive menu we draw a *custom* colorbar by clearing/redrawing `cbar.ax`.
352
+ - If `cbar` is a real `matplotlib.colorbar.Colorbar` (e.g., loaded from a session),
353
+ it remains connected to `im` via `im.callbacksSM`. Subsequent `im.set_clim()` /
354
+ `im.set_cmap()` triggers `Colorbar.update_normal()`, which can crash after we
355
+ cleared/redrew the axes (observed as: NotImplementedError: cannot remove artist).
356
+ - We therefore disconnect that callback once and always update the custom colorbar
357
+ via `_update_custom_colorbar(...)`.
358
+ """
359
+ try:
360
+ if cbar is None or im is None:
361
+ return
362
+ cax = getattr(cbar, 'ax', None)
363
+ if cax is not None and getattr(cax, '_bp_detached_mpl_colorbar', False):
364
+ return
365
+
366
+ # Matplotlib's Colorbar stores the callback id as `_cid` (most versions).
367
+ cid = None
368
+ for attr in ('_cid', '_cid_colorbar', 'cid'):
369
+ try:
370
+ v = getattr(cbar, attr, None)
371
+ if isinstance(v, int):
372
+ cid = v
373
+ break
374
+ except Exception:
375
+ pass
376
+
377
+ # Disconnect from the ScalarMappable callback registry.
378
+ if cid is not None:
379
+ try:
380
+ cbreg = getattr(im, 'callbacksSM', None)
381
+ if cbreg is not None and hasattr(cbreg, 'disconnect'):
382
+ cbreg.disconnect(cid)
383
+ except Exception:
384
+ pass
385
+
386
+ # Prevent future built-in updates (best-effort; safe for mock colorbars too).
387
+ try:
388
+ if hasattr(cbar, 'mappable'):
389
+ cbar.mappable = None
390
+ except Exception:
391
+ pass
392
+ try:
393
+ if hasattr(cbar, 'solids'):
394
+ cbar.solids = None
395
+ except Exception:
396
+ pass
397
+
398
+ if cax is not None:
399
+ setattr(cax, '_bp_detached_mpl_colorbar', True)
400
+ except Exception:
401
+ # Never let detaching break the interactive menu.
402
+ return
403
+
404
+
347
405
  def _ensure_fixed_params(fig, ax, cbar_ax, ec_ax):
348
406
  """Initialize and return fixed geometry parameters in inches.
349
407
 
@@ -540,6 +598,11 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
540
598
  # Normalize file path list for downstream helpers
541
599
  file_paths = list(file_paths) if file_paths else []
542
600
 
601
+ # If we were given a real Matplotlib Colorbar (e.g. from session load),
602
+ # detach it from `im` immediately. This must happen before any function
603
+ # that may clear/redraw `cbar.ax` (custom colorbar) is called.
604
+ _detach_mpl_colorbar_callbacks(cbar, im)
605
+
543
606
  def _renormalize_to_visible():
544
607
  """Adjust color scale to match the intensity range of the currently visible region.
545
608
 
@@ -904,6 +967,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
904
967
  # Initialize custom colorbar (replaces matplotlib's colorbar)
905
968
  cbar_label = getattr(cbar.ax, '_colorbar_label', 'Intensity')
906
969
  cbar_label_mode = getattr(fig, '_colorbar_label_mode', 'normal')
970
+ # If we were given a real Matplotlib Colorbar (e.g. from session load),
971
+ # detach it from `im` before we clear/redraw the axes for the custom colorbar.
972
+ _detach_mpl_colorbar_callbacks(cbar, im)
907
973
  _draw_custom_colorbar(cbar.ax, im, cbar_label, cbar_label_mode)
908
974
  # Decrease distance between operando and EC plots once per session
909
975
  if not getattr(ec_ax, '_ec_gap_adjusted', False):
@@ -1204,7 +1270,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1204
1270
  if snap.get('cmap'):
1205
1271
  im.set_cmap(snap['cmap'])
1206
1272
  if cbar is not None:
1207
- cbar.update_normal(im)
1273
+ _update_custom_colorbar(cbar.ax, im)
1208
1274
  except Exception:
1209
1275
  pass
1210
1276
  # Restore colorbar side (ticks/label) and redraw custom colorbar to keep position
@@ -1765,6 +1831,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1765
1831
  target = _co(target)
1766
1832
  if not target:
1767
1833
  print_menu(); continue
1834
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
1835
+ from .utils import ensure_exact_case_filename
1836
+ target = ensure_exact_case_filename(target)
1837
+
1768
1838
  _, ext = os.path.splitext(target)
1769
1839
  if ext.lower() == '.svg':
1770
1840
  try:
@@ -2052,6 +2122,19 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2052
2122
  print_menu(); continue
2053
2123
  dump_operando_session(target, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
2054
2124
  fig._last_session_save_path = target
2125
+ # Show the actual filename that was saved (in case of case differences on macOS)
2126
+ actual_name = os.path.basename(target)
2127
+ if os.path.exists(target):
2128
+ # Get the actual filename as stored on disk (for case-sensitive display)
2129
+ try:
2130
+ dir_files = os.listdir(folder)
2131
+ for f in dir_files:
2132
+ if f.lower() == actual_name.lower():
2133
+ actual_name = f
2134
+ break
2135
+ except Exception:
2136
+ pass
2137
+ print(f"Operando session saved to {os.path.join(folder, actual_name)}")
2055
2138
  except Exception as e:
2056
2139
  print(f"Save failed: {e}")
2057
2140
  print_menu(); continue
@@ -3777,6 +3860,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3777
3860
  if yn != 'y':
3778
3861
  target = None
3779
3862
  if target:
3863
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
3864
+ from .utils import ensure_exact_case_filename
3865
+ target = ensure_exact_case_filename(target)
3866
+
3780
3867
  with open(target, 'w', encoding='utf-8') as f:
3781
3868
  json.dump(cfg, f, indent=2)
3782
3869
  print(f"Exported style to {target}")
@@ -3900,7 +3987,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3900
3987
  try:
3901
3988
  im.set_cmap(cmap)
3902
3989
  if cbar is not None:
3903
- cbar.update_normal(im)
3990
+ _update_custom_colorbar(cbar.ax, im)
3904
3991
  except Exception:
3905
3992
  pass
3906
3993
 
@@ -519,6 +519,10 @@ def dump_session(
519
519
  if not target:
520
520
  print("Session save canceled.")
521
521
  return
522
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
523
+ from .utils import ensure_exact_case_filename
524
+ target = ensure_exact_case_filename(target)
525
+
522
526
  with open(target, 'wb') as f:
523
527
  pickle.dump(sess, f)
524
528
  print(f"Session saved to {target}")
@@ -663,6 +667,16 @@ def dump_operando_session(
663
667
  # Capture operando WASD state, spines, and tick widths
664
668
  op_wasd_state = _capture_wasd_state(ax)
665
669
  op_spines, op_ticks = _capture_spine_tick_widths(ax)
670
+
671
+ # Capture operando title offsets
672
+ op_title_offsets = {
673
+ 'top_y': float(getattr(ax, '_top_xlabel_manual_offset_y_pts', 0.0) or 0.0),
674
+ 'top_x': float(getattr(ax, '_top_xlabel_manual_offset_x_pts', 0.0) or 0.0),
675
+ 'bottom_y': float(getattr(ax, '_bottom_xlabel_manual_offset_y_pts', 0.0) or 0.0),
676
+ 'left_x': float(getattr(ax, '_left_ylabel_manual_offset_x_pts', 0.0) or 0.0),
677
+ 'right_x': float(getattr(ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0),
678
+ 'right_y': float(getattr(ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0),
679
+ }
666
680
 
667
681
  # EC panel (optional)
668
682
  ec_state = None
@@ -783,6 +797,10 @@ def dump_operando_session(
783
797
  if not target:
784
798
  print("Session save canceled.")
785
799
  return
800
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
801
+ from .utils import ensure_exact_case_filename
802
+ target = ensure_exact_case_filename(target)
803
+
786
804
  with open(target, 'wb') as f:
787
805
  pickle.dump(sess, f)
788
806
  print(f"Operando session saved to {target}")
@@ -787,6 +787,10 @@ def export_style_config(
787
787
  print("Style export canceled.")
788
788
  return None
789
789
 
790
+ # Ensure exact case is preserved (important for macOS case-insensitive filesystem)
791
+ from .utils import ensure_exact_case_filename
792
+ target_path = ensure_exact_case_filename(target_path)
793
+
790
794
  with open(target_path, "w", encoding="utf-8") as f:
791
795
  json.dump(cfg, f, indent=2)
792
796
  print(f"Exported style to {target_path}")
@@ -777,6 +777,54 @@ def choose_save_path(file_paths: list, purpose: str = "saving") -> Optional[str]
777
777
  return os.getcwd()
778
778
 
779
779
 
780
+ def ensure_exact_case_filename(target_path: str) -> str:
781
+ """Ensure a file is saved with the exact case specified, even on case-insensitive filesystems.
782
+
783
+ This function handles case-insensitive filesystems (macOS, Windows) by ensuring that
784
+ if a file exists with different case, it is removed first so the new file can be created
785
+ with the exact case specified by the user.
786
+
787
+ On case-sensitive filesystems (Linux, Unix), this function is safe but has no effect
788
+ since files with different case are treated as different files.
789
+
790
+ Args:
791
+ target_path: The desired file path with exact case
792
+
793
+ Returns:
794
+ The same path (for compatibility)
795
+ """
796
+ folder = os.path.dirname(target_path)
797
+ desired_basename = os.path.basename(target_path)
798
+
799
+ if not folder or not desired_basename:
800
+ return target_path
801
+
802
+ try:
803
+ # Check if file already exists with exact case
804
+ if os.path.exists(target_path):
805
+ # Check if the actual filename on disk matches the desired case
806
+ existing_files = os.listdir(folder)
807
+ for existing_file in existing_files:
808
+ # If same name (case-insensitive) but different case, we need to fix it
809
+ if existing_file.lower() == desired_basename.lower() and existing_file != desired_basename:
810
+ existing_path = os.path.join(folder, existing_file)
811
+ # Delete the existing file with wrong case
812
+ # This is safe on case-insensitive filesystems and has no effect on case-sensitive ones
813
+ try:
814
+ if os.path.exists(existing_path):
815
+ os.remove(existing_path)
816
+ except Exception:
817
+ # Ignore errors (e.g., permission issues, file in use)
818
+ pass
819
+ break
820
+ except Exception:
821
+ # If we can't check/list the directory, just return the path as-is
822
+ # This is safe and ensures we don't break on permission errors
823
+ pass
824
+
825
+ return target_path
826
+
827
+
780
828
  def _normalize_extension(ext: str) -> str:
781
829
  if not ext:
782
830
  return ext
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.7.26
3
+ Version: 1.7.28
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "batplot"
7
- version = "1.7.26"
7
+ version = "1.7.28"
8
8
  description = "Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing"
9
9
  authors = [
10
10
  { name = "Tian Dai", email = "tianda@uio.no" }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes