batplot 1.7.28__tar.gz → 1.8.1__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.
Files changed (60) hide show
  1. {batplot-1.7.28/batplot.egg-info → batplot-1.8.1}/PKG-INFO +1 -1
  2. {batplot-1.7.28 → batplot-1.8.1}/batplot/__init__.py +1 -1
  3. {batplot-1.7.28 → batplot-1.8.1}/batplot/args.py +3 -3
  4. {batplot-1.7.28 → batplot-1.8.1}/batplot/cpc_interactive.py +89 -3
  5. {batplot-1.7.28 → batplot-1.8.1}/batplot/electrochem_interactive.py +118 -55
  6. {batplot-1.7.28 → batplot-1.8.1}/batplot/interactive.py +100 -63
  7. {batplot-1.7.28 → batplot-1.8.1}/batplot/modes.py +12 -12
  8. {batplot-1.7.28 → batplot-1.8.1}/batplot/operando.py +2 -0
  9. {batplot-1.7.28 → batplot-1.8.1}/batplot/operando_ec_interactive.py +260 -89
  10. {batplot-1.7.28 → batplot-1.8.1}/batplot/session.py +18 -1
  11. {batplot-1.7.28 → batplot-1.8.1}/batplot/utils.py +40 -0
  12. {batplot-1.7.28 → batplot-1.8.1}/batplot/version_check.py +85 -6
  13. {batplot-1.7.28 → batplot-1.8.1/batplot.egg-info}/PKG-INFO +1 -1
  14. {batplot-1.7.28 → batplot-1.8.1}/pyproject.toml +1 -1
  15. {batplot-1.7.28 → batplot-1.8.1}/LICENSE +0 -0
  16. {batplot-1.7.28 → batplot-1.8.1}/MANIFEST.in +0 -0
  17. {batplot-1.7.28 → batplot-1.8.1}/README.md +0 -0
  18. {batplot-1.7.28 → batplot-1.8.1}/USER_MANUAL.md +0 -0
  19. {batplot-1.7.28 → batplot-1.8.1}/batplot/batch.py +0 -0
  20. {batplot-1.7.28 → batplot-1.8.1}/batplot/batplot.py +0 -0
  21. {batplot-1.7.28 → batplot-1.8.1}/batplot/cif.py +0 -0
  22. {batplot-1.7.28 → batplot-1.8.1}/batplot/cli.py +0 -0
  23. {batplot-1.7.28 → batplot-1.8.1}/batplot/color_utils.py +0 -0
  24. {batplot-1.7.28 → batplot-1.8.1}/batplot/config.py +0 -0
  25. {batplot-1.7.28 → batplot-1.8.1}/batplot/converters.py +0 -0
  26. {batplot-1.7.28 → batplot-1.8.1}/batplot/data/USER_MANUAL.md +0 -0
  27. {batplot-1.7.28 → batplot-1.8.1}/batplot/manual.py +0 -0
  28. {batplot-1.7.28 → batplot-1.8.1}/batplot/plotting.py +0 -0
  29. {batplot-1.7.28 → batplot-1.8.1}/batplot/readers.py +0 -0
  30. {batplot-1.7.28 → batplot-1.8.1}/batplot/style.py +0 -0
  31. {batplot-1.7.28 → batplot-1.8.1}/batplot/ui.py +0 -0
  32. {batplot-1.7.28 → batplot-1.8.1}/batplot.egg-info/SOURCES.txt +0 -0
  33. {batplot-1.7.28 → batplot-1.8.1}/batplot.egg-info/dependency_links.txt +0 -0
  34. {batplot-1.7.28 → batplot-1.8.1}/batplot.egg-info/entry_points.txt +0 -0
  35. {batplot-1.7.28 → batplot-1.8.1}/batplot.egg-info/requires.txt +0 -0
  36. {batplot-1.7.28 → batplot-1.8.1}/batplot.egg-info/top_level.txt +0 -0
  37. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/__init__.py +0 -0
  38. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/args.py +0 -0
  39. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/batch.py +0 -0
  40. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/batplot.py +0 -0
  41. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/cif.py +0 -0
  42. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/cli.py +0 -0
  43. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/color_utils.py +0 -0
  44. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/config.py +0 -0
  45. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/converters.py +0 -0
  46. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
  47. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
  48. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/interactive.py +0 -0
  49. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/modes.py +0 -0
  50. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/operando.py +0 -0
  51. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
  52. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/plotting.py +0 -0
  53. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/readers.py +0 -0
  54. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/session.py +0 -0
  55. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/style.py +0 -0
  56. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/ui.py +0 -0
  57. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/utils.py +0 -0
  58. {batplot-1.7.28 → batplot-1.8.1}/batplot_backup_20251121_223043/version_check.py +0 -0
  59. {batplot-1.7.28 → batplot-1.8.1}/setup.cfg +0 -0
  60. {batplot-1.7.28 → batplot-1.8.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.7.28
3
+ Version: 1.8.1
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.28"
3
+ __version__ = "1.8.1"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -169,7 +169,7 @@ def _print_general_help() -> None:
169
169
  " batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
170
170
  " batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
171
171
  " [Operando]\n"
172
- " batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file) with interactive menu\n\n"
172
+ " batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)\n\n"
173
173
  "Features:\n"
174
174
  " • Quick plotting with sensible defaults, no config files needed\n"
175
175
  " • Supports many common file formats (see -h xy/ec/op)\n"
@@ -183,9 +183,9 @@ def _print_general_help() -> None:
183
183
  " batplot -h xy # XY file plotting guide\n"
184
184
  " batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
185
185
  " batplot -h op # Operando guide\n"
186
- " batplot -m # Open the illustrated txt manual with highlights\n"
186
+ " batplot -m # Open the illustrated txt manual with highlights\n\n"
187
187
 
188
- "Contact & Updates:\n\n"
188
+ "Contact & Updates:\n"
189
189
  " Subscribe to batplot-lab@kjemi.uio.no for updates\n"
190
190
  " (If you are not from UiO, send an email to sympa@kjemi.uio.no with the subject line \"subscribe batplot-lab@kjemi.uio.no your-name\")\n"
191
191
  " Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
@@ -86,6 +86,7 @@ from .ui import (
86
86
  from .utils import (
87
87
  _confirm_overwrite,
88
88
  choose_save_path,
89
+ convert_label_shortcuts,
89
90
  choose_style_file,
90
91
  list_files_in_subdirectory,
91
92
  get_organized_path,
@@ -335,13 +336,36 @@ def _rebuild_legend(ax, ax2, file_data, preserve_position=True):
335
336
  """
336
337
  try:
337
338
  fig = ax.figure
338
- # Get stored position before rebuilding
339
+ # Get stored position before rebuilding. If none is stored yet, try to
340
+ # capture the current on-canvas position once so subsequent rebuilds
341
+ # (e.g., after renaming) do not jump to a new "best" location.
339
342
  xy_in = None
340
343
  if preserve_position:
341
344
  try:
342
345
  xy_in = getattr(fig, '_cpc_legend_xy_in', None)
343
346
  except Exception:
344
- pass
347
+ xy_in = None
348
+ if xy_in is None:
349
+ try:
350
+ leg0 = ax.get_legend()
351
+ if leg0 is not None and leg0.get_visible():
352
+ try:
353
+ renderer = fig.canvas.get_renderer()
354
+ except Exception:
355
+ fig.canvas.draw()
356
+ renderer = fig.canvas.get_renderer()
357
+ bb = leg0.get_window_extent(renderer=renderer)
358
+ cx = 0.5 * (bb.x0 + bb.x1)
359
+ cy = 0.5 * (bb.y0 + bb.y1)
360
+ fx, fy = fig.transFigure.inverted().transform((cx, cy))
361
+ fw, fh = fig.get_size_inches()
362
+ offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
363
+ offset = _sanitize_legend_offset(offset)
364
+ if offset is not None:
365
+ fig._cpc_legend_xy_in = offset
366
+ xy_in = offset
367
+ except Exception:
368
+ pass
345
369
 
346
370
  h1, l1 = ax.get_legend_handles_labels()
347
371
  h2, l2 = ax2.get_legend_handles_labels()
@@ -1643,6 +1667,31 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1643
1667
  else:
1644
1668
  # Single file mode: toggle efficiency
1645
1669
  push_state("visibility-eff")
1670
+ # Capture current legend position BEFORE toggling visibility
1671
+ try:
1672
+ if not hasattr(fig, '_cpc_legend_xy_in') or getattr(fig, '_cpc_legend_xy_in') is None:
1673
+ leg0 = ax.get_legend()
1674
+ if leg0 is not None and leg0.get_visible():
1675
+ try:
1676
+ # Ensure renderer exists
1677
+ try:
1678
+ renderer = fig.canvas.get_renderer()
1679
+ except Exception:
1680
+ fig.canvas.draw()
1681
+ renderer = fig.canvas.get_renderer()
1682
+ bb = leg0.get_window_extent(renderer=renderer)
1683
+ cx = 0.5 * (bb.x0 + bb.x1)
1684
+ cy = 0.5 * (bb.y0 + bb.y1)
1685
+ fx, fy = fig.transFigure.inverted().transform((cx, cy))
1686
+ fw, fh = fig.get_size_inches()
1687
+ offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
1688
+ offset = _sanitize_legend_offset(offset)
1689
+ if offset is not None:
1690
+ fig._cpc_legend_xy_in = offset
1691
+ except Exception:
1692
+ pass
1693
+ except Exception:
1694
+ pass
1646
1695
  vis = sc_eff.get_visible()
1647
1696
  sc_eff.set_visible(not vis)
1648
1697
  try:
@@ -1650,7 +1699,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1650
1699
  except Exception:
1651
1700
  pass
1652
1701
 
1653
- _rebuild_legend(ax, ax2, file_data)
1702
+ _rebuild_legend(ax, ax2, file_data, preserve_position=True)
1654
1703
  fig.canvas.draw_idle()
1655
1704
  except ValueError:
1656
1705
  print("Invalid input.")
@@ -1676,6 +1725,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1676
1725
  elif key == 'c':
1677
1726
  # Colors submenu: ly (left Y series) and ry (right Y efficiency), with user colors and palettes
1678
1727
  try:
1728
+ # Note: Individual series may use different colors, so we can't show a single "current" palette
1679
1729
  # Use same palettes as EC interactive
1680
1730
  palette_opts = ['tab10', 'Set2', 'Dark2', 'viridis', 'plasma']
1681
1731
  def _palette_color(name, idx=0, total=1, default_val=0.4):
@@ -2668,6 +2718,33 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
2668
2718
  try:
2669
2719
  push_state("toggle-eff")
2670
2720
 
2721
+ # Capture current legend position BEFORE toggling visibility
2722
+ # This ensures the position is preserved when legend is rebuilt
2723
+ try:
2724
+ if not hasattr(fig, '_cpc_legend_xy_in') or getattr(fig, '_cpc_legend_xy_in') is None:
2725
+ leg0 = ax.get_legend()
2726
+ if leg0 is not None and leg0.get_visible():
2727
+ try:
2728
+ # Ensure renderer exists
2729
+ try:
2730
+ renderer = fig.canvas.get_renderer()
2731
+ except Exception:
2732
+ fig.canvas.draw()
2733
+ renderer = fig.canvas.get_renderer()
2734
+ bb = leg0.get_window_extent(renderer=renderer)
2735
+ cx = 0.5 * (bb.x0 + bb.x1)
2736
+ cy = 0.5 * (bb.y0 + bb.y1)
2737
+ fx, fy = fig.transFigure.inverted().transform((cx, cy))
2738
+ fw, fh = fig.get_size_inches()
2739
+ offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
2740
+ offset = _sanitize_legend_offset(offset)
2741
+ if offset is not None:
2742
+ fig._cpc_legend_xy_in = offset
2743
+ except Exception:
2744
+ pass
2745
+ except Exception:
2746
+ pass
2747
+
2671
2748
  # Determine current visibility state (check if any efficiency is visible)
2672
2749
  if is_multi_file:
2673
2750
  # In multi-file mode, check if any efficiency is visible
@@ -3141,6 +3218,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3141
3218
  _print_menu(); continue
3142
3219
  elif key == 't':
3143
3220
  # Unified WASD toggles for spines/ticks/minor/labels/title per side
3221
+ # Import UI positioning functions locally to ensure they're accessible in nested functions
3222
+ from .ui import position_top_xlabel as _ui_position_top_xlabel, position_bottom_xlabel as _ui_position_bottom_xlabel, position_left_ylabel as _ui_position_left_ylabel, position_right_ylabel as _ui_position_right_ylabel
3223
+
3144
3224
  try:
3145
3225
  # Local WASD state stored on figure to persist across openings
3146
3226
  wasd = getattr(fig, '_cpc_wasd_state', None)
@@ -3735,6 +3815,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3735
3815
  print("Tip: Use LaTeX/mathtext for special characters:")
3736
3816
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
3737
3817
  print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
3818
+ print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
3738
3819
  while True:
3739
3820
  print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
3740
3821
  sub = _safe_input("Rename> ").strip().lower()
@@ -3785,6 +3866,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3785
3866
  print(f"Current file name in legend: '{base_name}'")
3786
3867
  new_name = _safe_input("Enter new file name (q=cancel): ").strip()
3787
3868
  if new_name and new_name.lower() != 'q':
3869
+ new_name = convert_label_shortcuts(new_name)
3788
3870
  try:
3789
3871
  push_state("rename-legend")
3790
3872
 
@@ -3892,6 +3974,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3892
3974
  print(f"Current file name in legend: '{base_name}'")
3893
3975
  new_name = _safe_input("Enter new file name (q=cancel): ").strip()
3894
3976
  if new_name and new_name.lower() != 'q':
3977
+ new_name = convert_label_shortcuts(new_name)
3895
3978
  try:
3896
3979
  push_state("rename-legend")
3897
3980
 
@@ -3963,6 +4046,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3963
4046
  print(f"Current x-axis title: '{current}'")
3964
4047
  new_title = _safe_input("Enter new x-axis title (q=cancel): ")
3965
4048
  if new_title and new_title.lower() != 'q':
4049
+ new_title = convert_label_shortcuts(new_title)
3966
4050
  try:
3967
4051
  push_state("rename-x")
3968
4052
  ax.set_xlabel(new_title)
@@ -3982,6 +4066,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3982
4066
  print(f"Current left y-axis title: '{current}'")
3983
4067
  new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
3984
4068
  if new_title and new_title.lower() != 'q':
4069
+ new_title = convert_label_shortcuts(new_title)
3985
4070
  try:
3986
4071
  push_state("rename-ly")
3987
4072
  ax.set_ylabel(new_title)
@@ -3996,6 +4081,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3996
4081
  print(f"Current right y-axis title: '{current}'")
3997
4082
  new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
3998
4083
  if new_title and new_title.lower() != 'q':
4084
+ new_title = convert_label_shortcuts(new_title)
3999
4085
  try:
4000
4086
  push_state("rename-ry")
4001
4087
  ax2.set_ylabel(new_title)