batplot 1.8.0__py3-none-any.whl → 1.8.2__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 +5 -3
  3. batplot/batplot.py +44 -4
  4. batplot/cpc_interactive.py +96 -3
  5. batplot/electrochem_interactive.py +28 -0
  6. batplot/interactive.py +18 -2
  7. batplot/modes.py +12 -12
  8. batplot/operando.py +2 -0
  9. batplot/operando_ec_interactive.py +112 -11
  10. batplot/session.py +35 -1
  11. batplot/utils.py +40 -0
  12. batplot/version_check.py +85 -6
  13. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/METADATA +1 -1
  14. batplot-1.8.2.dist-info/RECORD +75 -0
  15. {batplot-1.8.0.dist-info → batplot-1.8.2.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.0.dist-info/RECORD +0 -52
  40. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/WHEEL +0 -0
  41. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/entry_points.txt +0 -0
  42. {batplot-1.8.0.dist-info → batplot-1.8.2.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.0"
3
+ __version__ = "1.8.2"
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"
@@ -169,7 +171,7 @@ def _print_general_help() -> None:
169
171
  " batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
170
172
  " batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
171
173
  " [Operando]\n"
172
- " batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file) with interactive menu\n\n"
174
+ " batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)\n\n"
173
175
  "Features:\n"
174
176
  " • Quick plotting with sensible defaults, no config files needed\n"
175
177
  " • Supports many common file formats (see -h xy/ec/op)\n"
@@ -183,9 +185,9 @@ def _print_general_help() -> None:
183
185
  " batplot -h xy # XY file plotting guide\n"
184
186
  " batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
185
187
  " batplot -h op # Operando guide\n"
186
- " batplot -m # Open the illustrated txt manual with highlights\n"
188
+ " batplot -m # Open the illustrated txt manual with highlights\n\n"
187
189
 
188
- "Contact & Updates:\n\n"
190
+ "Contact & Updates:\n"
189
191
  " Subscribe to batplot-lab@kjemi.uio.no for updates\n"
190
192
  " (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
193
  " Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
batplot/batplot.py CHANGED
@@ -1890,10 +1890,31 @@ def batplot_main() -> int:
1890
1890
  x_full_list = []
1891
1891
  raw_y_full_list = []
1892
1892
  offsets_list = []
1893
- tick_state = {
1894
- 'bx': True,'tx': False,'ly': True,'ry': False,
1895
- 'mbx': False,'mtx': False,'mly': False,'mry': False
1896
- }
1893
+ # Load tick_state from wasd_state if available (version 2+), otherwise use defaults
1894
+ wasd_loaded = sess.get('wasd_state')
1895
+ if wasd_loaded and isinstance(wasd_loaded, dict):
1896
+ # Convert wasd_state to tick_state format
1897
+ tick_state = {}
1898
+ for side_key, prefix in [('top', 't'), ('bottom', 'b'), ('left', 'l'), ('right', 'r')]:
1899
+ s = wasd_loaded.get(side_key, {})
1900
+ tick_state[f'{prefix}_ticks'] = bool(s.get('ticks', side_key in ('bottom', 'left')))
1901
+ tick_state[f'{prefix}_labels'] = bool(s.get('labels', side_key in ('bottom', 'left')))
1902
+ tick_state[f'm{prefix}x' if prefix in 'tb' else f'm{prefix}y'] = bool(s.get('minor', False))
1903
+ # Legacy keys for backward compatibility
1904
+ tick_state['bx'] = tick_state.get('b_ticks', True)
1905
+ tick_state['tx'] = tick_state.get('t_ticks', False)
1906
+ tick_state['ly'] = tick_state.get('l_ticks', True)
1907
+ tick_state['ry'] = tick_state.get('r_ticks', False)
1908
+ tick_state['mbx'] = tick_state.get('mbx', False)
1909
+ tick_state['mtx'] = tick_state.get('mtx', False)
1910
+ tick_state['mly'] = tick_state.get('mly', False)
1911
+ tick_state['mry'] = tick_state.get('mry', False)
1912
+ else:
1913
+ # Fallback to legacy tick_state or defaults
1914
+ tick_state = sess.get('tick_state', {
1915
+ 'bx': True,'tx': False,'ly': True,'ry': False,
1916
+ 'mbx': False,'mtx': False,'mly': False,'mry': False
1917
+ })
1897
1918
  saved_stack = bool(sess.get('args_subset', {}).get('stack', False))
1898
1919
  # Pull data
1899
1920
  # --- Robust reconstruction of stored curves ---
@@ -1954,8 +1975,27 @@ def batplot_main() -> int:
1954
1975
  pass
1955
1976
  labels_list[:] = sess.get('labels', [f"Curve {i+1}" for i in range(len(y_data_list))])
1956
1977
  delta = sess.get('delta', 0.0)
1978
+ # Apply tick state (labels visibility) BEFORE setting axis labels
1979
+ try:
1980
+ ax.tick_params(axis='x',
1981
+ bottom=tick_state.get('b_ticks', tick_state.get('bx', True)),
1982
+ labelbottom=tick_state.get('b_labels', tick_state.get('bx', True)),
1983
+ top=tick_state.get('t_ticks', tick_state.get('tx', False)),
1984
+ labeltop=tick_state.get('t_labels', tick_state.get('tx', False)))
1985
+ ax.tick_params(axis='y',
1986
+ left=tick_state.get('l_ticks', tick_state.get('ly', True)),
1987
+ labelleft=tick_state.get('l_labels', tick_state.get('ly', True)),
1988
+ right=tick_state.get('r_ticks', tick_state.get('ry', False)),
1989
+ labelright=tick_state.get('r_labels', tick_state.get('ry', False)))
1990
+ except Exception:
1991
+ pass
1957
1992
  ax.set_xlabel(sess.get('axis', {}).get('xlabel', 'X'))
1958
1993
  ax.set_ylabel(sess.get('axis', {}).get('ylabel', 'Intensity'))
1994
+ # Store tick_state on axes for interactive menu
1995
+ try:
1996
+ ax._saved_tick_state = dict(tick_state)
1997
+ except Exception:
1998
+ pass
1959
1999
 
1960
2000
  # Restore normalization ranges (if saved)
1961
2001
  axis_cfg = sess.get('axis', {})
@@ -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
@@ -3381,6 +3458,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3381
3458
  if not cmd:
3382
3459
  continue
3383
3460
  if cmd == 'q':
3461
+ # Update ax._saved_tick_state before exiting so changes are persisted
3462
+ try:
3463
+ ax._saved_tick_state = dict(tick_state)
3464
+ except Exception:
3465
+ pass
3384
3466
  break
3385
3467
  if cmd == 'i':
3386
3468
  # Invert tick direction (toggle between 'out' and 'in')
@@ -3702,6 +3784,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3702
3784
  tick_state['mry'] = bool(wasd['right']['minor'])
3703
3785
  if changed:
3704
3786
  push_state("wasd-toggle")
3787
+ # Update ax._saved_tick_state so dump_session can read it
3788
+ try:
3789
+ ax._saved_tick_state = dict(tick_state)
3790
+ except Exception:
3791
+ pass
3705
3792
  _apply_wasd(changed_sides if changed_sides else None)
3706
3793
  # Single draw at the end after all positioning is complete
3707
3794
  try:
@@ -3738,6 +3825,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3738
3825
  print("Tip: Use LaTeX/mathtext for special characters:")
3739
3826
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
3740
3827
  print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
3828
+ print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
3741
3829
  while True:
3742
3830
  print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
3743
3831
  sub = _safe_input("Rename> ").strip().lower()
@@ -3788,6 +3876,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3788
3876
  print(f"Current file name in legend: '{base_name}'")
3789
3877
  new_name = _safe_input("Enter new file name (q=cancel): ").strip()
3790
3878
  if new_name and new_name.lower() != 'q':
3879
+ new_name = convert_label_shortcuts(new_name)
3791
3880
  try:
3792
3881
  push_state("rename-legend")
3793
3882
 
@@ -3895,6 +3984,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3895
3984
  print(f"Current file name in legend: '{base_name}'")
3896
3985
  new_name = _safe_input("Enter new file name (q=cancel): ").strip()
3897
3986
  if new_name and new_name.lower() != 'q':
3987
+ new_name = convert_label_shortcuts(new_name)
3898
3988
  try:
3899
3989
  push_state("rename-legend")
3900
3990
 
@@ -3966,6 +4056,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3966
4056
  print(f"Current x-axis title: '{current}'")
3967
4057
  new_title = _safe_input("Enter new x-axis title (q=cancel): ")
3968
4058
  if new_title and new_title.lower() != 'q':
4059
+ new_title = convert_label_shortcuts(new_title)
3969
4060
  try:
3970
4061
  push_state("rename-x")
3971
4062
  ax.set_xlabel(new_title)
@@ -3985,6 +4076,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3985
4076
  print(f"Current left y-axis title: '{current}'")
3986
4077
  new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
3987
4078
  if new_title and new_title.lower() != 'q':
4079
+ new_title = convert_label_shortcuts(new_title)
3988
4080
  try:
3989
4081
  push_state("rename-ly")
3990
4082
  ax.set_ylabel(new_title)
@@ -3999,6 +4091,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3999
4091
  print(f"Current right y-axis title: '{current}'")
4000
4092
  new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
4001
4093
  if new_title and new_title.lower() != 'q':
4094
+ new_title = convert_label_shortcuts(new_title)
4002
4095
  try:
4003
4096
  push_state("rename-ry")
4004
4097
  ax2.set_ylabel(new_title)
@@ -31,6 +31,7 @@ from .utils import (
31
31
  choose_style_file,
32
32
  list_files_in_subdirectory,
33
33
  get_organized_path,
34
+ convert_label_shortcuts,
34
35
  )
35
36
  import time
36
37
  from .color_utils import (
@@ -502,6 +503,28 @@ def _rebuild_legend(ax):
502
503
  fig = ax.figure
503
504
  # Capture existing title before any rebuild so it isn't lost
504
505
  _store_legend_title(fig, ax)
506
+ # If no stored position yet, try to capture the current legend location once
507
+ # so rebuilds (e.g., after renaming) don't jump to a new "best" spot.
508
+ try:
509
+ if getattr(fig, '_ec_legend_xy_in', None) is None:
510
+ leg0 = ax.get_legend()
511
+ if leg0 is not None and leg0.get_visible():
512
+ try:
513
+ renderer = fig.canvas.get_renderer()
514
+ except Exception:
515
+ fig.canvas.draw()
516
+ renderer = fig.canvas.get_renderer()
517
+ bb = leg0.get_window_extent(renderer=renderer)
518
+ cx = 0.5 * (bb.x0 + bb.x1)
519
+ cy = 0.5 * (bb.y0 + bb.y1)
520
+ fx, fy = fig.transFigure.inverted().transform((cx, cy))
521
+ fw, fh = fig.get_size_inches()
522
+ offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
523
+ offset = _sanitize_legend_offset(fig, offset)
524
+ if offset is not None:
525
+ fig._ec_legend_xy_in = offset
526
+ except Exception:
527
+ pass
505
528
  if not _get_legend_user_pref(fig):
506
529
  leg = ax.get_legend()
507
530
  if leg is not None:
@@ -2697,6 +2720,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2697
2720
  print("Tip: Use LaTeX/mathtext for special characters:")
2698
2721
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2699
2722
  print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2723
+ print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
2700
2724
  while True:
2701
2725
  print("Rename axis: x, y, both, q=back")
2702
2726
  sub = _safe_input("Rename> ").strip().lower()
@@ -2707,6 +2731,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2707
2731
  if sub in ('x','both'):
2708
2732
  txt = _safe_input("New X-axis label (blank=cancel): ")
2709
2733
  if txt:
2734
+ txt = convert_label_shortcuts(txt)
2710
2735
  push_state("rename-x")
2711
2736
  try:
2712
2737
  # Freeze layout and preserve existing pad for one-shot restore
@@ -2730,6 +2755,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2730
2755
  if sub in ('y','both'):
2731
2756
  txt = _safe_input("New Y-axis label (blank=cancel): ")
2732
2757
  if txt:
2758
+ txt = convert_label_shortcuts(txt)
2733
2759
  push_state("rename-y")
2734
2760
  base_ylabel = txt
2735
2761
  try:
@@ -3027,6 +3053,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3027
3053
  _print_menu(len(all_cycles), is_dqdv)
3028
3054
  continue
3029
3055
  elif key == 'c':
3056
+ # Show current palette if one is applied (this is informational only)
3057
+ # Note: Individual cycles may use different colors, so we can't show a single "current" palette
3030
3058
  print(f"Total cycles: {len(all_cycles)}")
3031
3059
  print("Enter one of:")
3032
3060
  print(_colorize_inline_commands(" - numbers: e.g. 1 5 10"))
batplot/interactive.py CHANGED
@@ -25,6 +25,7 @@ from .utils import (
25
25
  choose_save_path,
26
26
  choose_style_file,
27
27
  list_files_in_subdirectory,
28
+ convert_label_shortcuts,
28
29
  get_organized_path,
29
30
  )
30
31
  import time
@@ -1955,12 +1956,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1955
1956
  ax._cif_draw_func()
1956
1957
  fig.canvas.draw()
1957
1958
  elif sub == 'p':
1959
+ # Show current palette if one is applied
1958
1960
  history = getattr(fig, '_curve_palette_history', [])
1959
1961
  current_palette = history[-1]['palette'] if history else None
1960
1962
  if current_palette:
1961
1963
  print(f"Current palette: {current_palette}")
1962
- else:
1963
- print("Current palette: manual/custom")
1964
1964
  base_palettes = ['viridis', 'cividis', 'plasma', 'inferno', 'magma', 'batlow']
1965
1965
  extras = []
1966
1966
  def _palette_available(name: str) -> bool:
@@ -2211,6 +2211,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2211
2211
  print("Tip: Use LaTeX/mathtext for special characters:")
2212
2212
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2213
2213
  print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2214
+ print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
2214
2215
  idx_in = _safe_input("Curve number to rename (q=cancel): ").strip()
2215
2216
  if not idx_in or idx_in.lower() == 'q':
2216
2217
  print("Canceled.")
@@ -2227,6 +2228,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2227
2228
  if not new_label or new_label.lower() == 'q':
2228
2229
  print("Canceled.")
2229
2230
  continue
2231
+ new_label = convert_label_shortcuts(new_label)
2230
2232
  push_state("rename-curve")
2231
2233
  labels[idx] = new_label
2232
2234
  label_text_objects[idx].set_text(f"{idx+1}: {new_label}")
@@ -2250,9 +2252,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2250
2252
  print("Tip: Use LaTeX/mathtext for special characters:")
2251
2253
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2252
2254
  print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2255
+ print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
2253
2256
  new_name = _safe_input("New CIF tick label (q=cancel): ")
2254
2257
  if not new_name or new_name.lower()=='q':
2255
2258
  print("Canceled."); continue
2259
+ new_name = convert_label_shortcuts(new_name)
2256
2260
  lab,fname,peaksQ,wl,qmax_sim,color = cts[idx]
2257
2261
  # Suspend extension while updating label
2258
2262
  if _bp is not None:
@@ -2278,10 +2282,12 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2278
2282
  print("Tip: Use LaTeX/mathtext for special characters:")
2279
2283
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2280
2284
  print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2285
+ print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
2281
2286
  new_axis = _safe_input("New axis label: ")
2282
2287
  if not new_axis or new_axis.lower() == 'q':
2283
2288
  print("Canceled.")
2284
2289
  continue
2290
+ new_axis = convert_label_shortcuts(new_axis)
2285
2291
  new_axis = normalize_label_text(new_axis)
2286
2292
  push_state("rename-axis")
2287
2293
  # Freeze layout and preserve current pad via one-shot pending to avoid drift
@@ -3307,6 +3313,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3307
3313
  if not cmd:
3308
3314
  continue
3309
3315
  if cmd == 'q':
3316
+ # Update ax._saved_tick_state before exiting so changes are persisted
3317
+ try:
3318
+ ax._saved_tick_state = dict(tick_state)
3319
+ except Exception:
3320
+ pass
3310
3321
  break
3311
3322
  if cmd == 'i':
3312
3323
  # Invert tick direction (toggle between 'out' and 'in')
@@ -3578,6 +3589,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3578
3589
  continue
3579
3590
  # Unknown code
3580
3591
  print(f"Unknown code: {p}")
3592
+ # After tick toggles, update ax._saved_tick_state so dump_session can read it
3593
+ try:
3594
+ ax._saved_tick_state = dict(tick_state)
3595
+ except Exception:
3596
+ pass
3581
3597
  # After tick toggles, update visibility and reposition ALL axis labels for independence
3582
3598
  update_tick_visibility()
3583
3599
  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)
batplot/operando.py CHANGED
@@ -315,6 +315,8 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
315
315
  extent = (grid_x.min(), grid_x.max(), 0, Zm.shape[0]-1)
316
316
  # Bottom-to-top visual order (scan 0 at bottom) to match EC time progression -> origin='lower'
317
317
  im = ax.imshow(Zm, aspect='auto', origin='lower', extent=extent, cmap='viridis', interpolation='nearest')
318
+ # Store the colormap name explicitly so it can be retrieved reliably when saving
319
+ setattr(im, '_operando_cmap_name', 'viridis')
318
320
  # Create custom colorbar axes on the left (will be positioned by layout function)
319
321
  # Create a dummy axes that will be replaced by the custom colorbar in interactive menu
320
322
  cbar_ax = fig.add_axes([0.0, 0.0, 0.01, 0.01]) # Temporary position, will be repositioned