batplot 1.8.5__py3-none-any.whl → 1.8.6__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/modes.py CHANGED
@@ -151,7 +151,8 @@ def handle_cv_mode(args) -> int:
151
151
  # Configure fonts to match other modes (consistent across batplot)
152
152
  plt.rcParams.update({
153
153
  'font.family': 'sans-serif',
154
- 'font.sans-serif': ['Arial', 'DejaVu Sans', 'Helvetica', 'STIXGeneral', 'Liberation Sans', 'Arial Unicode MS'],
154
+ # Prefer DejaVu Sans first for full Unicode coverage (subscripts, etc.)
155
+ 'font.sans-serif': ['DejaVu Sans', 'Arial', 'Helvetica', 'STIXGeneral', 'Liberation Sans', 'Arial Unicode MS'],
155
156
  'mathtext.fontset': 'dejavusans',
156
157
  'font.size': 16
157
158
  })
@@ -280,8 +281,8 @@ def handle_cv_mode(args) -> int:
280
281
  ax.set_xlabel('Current (mA)', labelpad=8.0)
281
282
  ax.set_ylabel('Voltage (V)', labelpad=8.0)
282
283
  else:
283
- ax.set_xlabel('Voltage (V)', labelpad=8.0)
284
- ax.set_ylabel('Current (mA)', labelpad=8.0)
284
+ ax.set_xlabel('Voltage (V)', labelpad=8.0)
285
+ ax.set_ylabel('Current (mA)', labelpad=8.0)
285
286
  legend = ax.legend(title='Cycle')
286
287
  legend.get_title().set_fontsize('medium')
287
288
  # Adjust margins to prevent label clipping
@@ -642,8 +643,8 @@ def handle_gc_mode(args) -> int:
642
643
  ln_c, = ax.plot(y_b, x_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
643
644
  linewidth=2.0, label=str(cyc), alpha=0.8)
644
645
  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)
646
+ ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
647
+ linewidth=2.0, label=str(cyc), alpha=0.8)
647
648
  else:
648
649
  ln_c = None
649
650
  mask_d = (cyc_int == cyc) & discharge_mask
@@ -656,8 +657,8 @@ def handle_gc_mode(args) -> int:
656
657
  ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
657
658
  linewidth=2.0, label=lbl, alpha=0.8)
658
659
  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)
660
+ ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
661
+ linewidth=2.0, label=lbl, alpha=0.8)
661
662
  else:
662
663
  ln_d = None
663
664
  cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
@@ -690,8 +691,8 @@ def handle_gc_mode(args) -> int:
690
691
  ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
691
692
  linewidth=2.0, label=lbl, alpha=0.8)
692
693
  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)
694
+ ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
695
+ linewidth=2.0, label=lbl, alpha=0.8)
695
696
  cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
696
697
 
697
698
  # Swap x and y if --ro flag is set
@@ -699,8 +700,8 @@ def handle_gc_mode(args) -> int:
699
700
  ax.set_xlabel('Voltage (V)', labelpad=8.0)
700
701
  ax.set_ylabel(x_label_gc, labelpad=8.0)
701
702
  else:
702
- ax.set_xlabel(x_label_gc, labelpad=8.0)
703
- ax.set_ylabel('Voltage (V)', labelpad=8.0)
703
+ ax.set_xlabel(x_label_gc, labelpad=8.0)
704
+ ax.set_ylabel('Voltage (V)', labelpad=8.0)
704
705
  legend = ax.legend(title='Cycle')
705
706
  legend.get_title().set_fontsize('medium')
706
707
  fig.subplots_adjust(left=0.12, right=0.95, top=0.88, bottom=0.15)
@@ -850,6 +850,17 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
850
850
  "b: undo",
851
851
  "q: quit",
852
852
  ]
853
+ # Conditional overwrite shortcuts under (Options)
854
+ last_session = getattr(fig, "_last_session_save_path", None)
855
+ last_style = getattr(fig, "_last_style_export_path", None)
856
+ last_figure = getattr(fig, "_last_figure_export_path", None)
857
+ if last_session:
858
+ col4.append("os: overwrite session")
859
+ if last_style:
860
+ col4.append("ops: overwrite style")
861
+ col4.append("opsg: overwrite style+geom")
862
+ if last_figure:
863
+ col4.append("oe: overwrite figure")
853
864
  # Dynamic column widths
854
865
  w1 = max(len("(Styles)"), *(len(s) for s in col1), 12)
855
866
  w2 = max(len("(Operando)"), *(len(s) for s in col2), 14)
@@ -898,6 +909,17 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
898
909
  "b: undo",
899
910
  "q: quit",
900
911
  ]
912
+ # Conditional overwrite shortcuts under (Options)
913
+ last_session = getattr(fig, "_last_session_save_path", None)
914
+ last_style = getattr(fig, "_last_style_export_path", None)
915
+ last_figure = getattr(fig, "_last_figure_export_path", None)
916
+ if last_session:
917
+ col3.append("os: overwrite session")
918
+ if last_style:
919
+ col3.append("ops: overwrite style")
920
+ col3.append("opsg: overwrite style+geom")
921
+ if last_figure:
922
+ col3.append("oe: overwrite figure")
901
923
  w1 = max(len("(Styles)"), *(len(s) for s in col1), 12)
902
924
  w2 = max(len("(Operando)"), *(len(s) for s in col2), 14)
903
925
  w3 = max(len("(Options)"), *(len(s) for s in col3), 16)
@@ -1222,6 +1244,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1222
1244
  }
1223
1245
  # EC WASD state (only if ec_ax exists)
1224
1246
  if ec_ax is not None:
1247
+ # For EC, check if ylabel is currently visible (not hidden by user via d5)
1248
+ # EC uses the actual ylabel positioned on right, not a duplicate artist
1249
+ ec_ylabel_visible = bool(ec_ax.get_ylabel()) # Empty string = hidden
1225
1250
  ec_wasd = {
1226
1251
  'top': {'spine': _get_spine_visible(ec_ax, 'top'), 'ticks': ec_ax.xaxis._major_tick_kw.get('tick1On', True),
1227
1252
  'minor': bool(ec_ax.xaxis._minor_tick_kw.get('tick1On', False)),
@@ -1234,11 +1259,11 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1234
1259
  'left': {'spine': _get_spine_visible(ec_ax, 'left'), 'ticks': ec_ax.yaxis._major_tick_kw.get('tick1On', False),
1235
1260
  'minor': bool(ec_ax.yaxis._minor_tick_kw.get('tick1On', False)),
1236
1261
  'labels': ec_ax.yaxis._major_tick_kw.get('label1On', False),
1237
- 'title': bool(ec_ax.get_ylabel())},
1262
+ 'title': False}, # EC ylabel is on right, not left
1238
1263
  'right': {'spine': _get_spine_visible(ec_ax, 'right'), 'ticks': ec_ax.yaxis._major_tick_kw.get('tick2On', True),
1239
1264
  'minor': bool(ec_ax.yaxis._minor_tick_kw.get('tick2On', False)),
1240
1265
  'labels': ec_ax.yaxis._major_tick_kw.get('label2On', True),
1241
- 'title': bool(ec_ax.get_ylabel())},
1266
+ 'title': ec_ylabel_visible}, # True if ylabel is not empty
1242
1267
  }
1243
1268
  else:
1244
1269
  ec_wasd = None
@@ -1268,6 +1293,47 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1268
1293
  'x': getattr(ec_ax.xaxis, 'labelpad', None),
1269
1294
  'y': getattr(ec_ax.yaxis, 'labelpad', None),
1270
1295
  }
1296
+ # Spine and tick widths (l command) for undo
1297
+ op_spines_snap = {}
1298
+ for name in ('bottom', 'top', 'left', 'right'):
1299
+ sp = ax.spines.get(name)
1300
+ if sp:
1301
+ op_spines_snap[name] = float(sp.get_linewidth())
1302
+ op_ticks_snap = {
1303
+ 'x_major': _axis_tick_width(ax.xaxis, 'major'),
1304
+ 'x_minor': _axis_tick_width(ax.xaxis, 'minor'),
1305
+ 'y_major': _axis_tick_width(ax.yaxis, 'major'),
1306
+ 'y_minor': _axis_tick_width(ax.yaxis, 'minor'),
1307
+ }
1308
+ ec_spines_snap = None
1309
+ ec_ticks_snap = None
1310
+ ec_line_style = None
1311
+ if ec_ax is not None:
1312
+ ec_spines_snap = {}
1313
+ for name in ('bottom', 'top', 'left', 'right'):
1314
+ sp = ec_ax.spines.get(name)
1315
+ if sp:
1316
+ ec_spines_snap[name] = float(sp.get_linewidth())
1317
+ ec_ticks_snap = {
1318
+ 'x_major': _axis_tick_width(ec_ax.xaxis, 'major'),
1319
+ 'x_minor': _axis_tick_width(ec_ax.xaxis, 'minor'),
1320
+ 'y_major': _axis_tick_width(ec_ax.yaxis, 'major'),
1321
+ 'y_minor': _axis_tick_width(ec_ax.yaxis, 'minor'),
1322
+ }
1323
+ ln = getattr(ec_ax, '_ec_line', None)
1324
+ if ln is None and ec_ax.lines:
1325
+ try:
1326
+ ln = ec_ax.lines[0]
1327
+ except Exception:
1328
+ ln = None
1329
+ if ln is not None:
1330
+ try:
1331
+ ec_line_style = {
1332
+ 'color': ln.get_color(),
1333
+ 'linewidth': float(ln.get_linewidth() or 1.0),
1334
+ }
1335
+ except Exception:
1336
+ pass
1271
1337
  state_history.append({
1272
1338
  'note': note,
1273
1339
  'fig_size': (fig_w, fig_h),
@@ -1311,6 +1377,11 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1311
1377
  'right_x': float(getattr(ec_ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0) if ec_ax is not None else 0.0,
1312
1378
  'right_y': float(getattr(ec_ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0) if ec_ax is not None else 0.0,
1313
1379
  } if ec_ax is not None else None,
1380
+ 'op_spines': op_spines_snap,
1381
+ 'op_ticks': op_ticks_snap,
1382
+ 'ec_spines': ec_spines_snap,
1383
+ 'ec_ticks': ec_ticks_snap,
1384
+ 'ec_line_style': ec_line_style,
1314
1385
  })
1315
1386
  if len(state_history) > 40:
1316
1387
  state_history.pop(0)
@@ -1548,24 +1619,31 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1548
1619
  ec_ax.tick_params(axis='x', which='minor', **{tick_key: bool(st['minor'])})
1549
1620
  if 'labels' in st:
1550
1621
  ec_ax.tick_params(axis='x', which='major', **{label_key: bool(st['labels'])})
1551
- elif side == 'right': # d - only EC controls
1622
+ elif side == 'right': # d - only EC controls (EC y-axis is on right by default)
1552
1623
  if 'ticks' in st:
1553
1624
  ec_ax.tick_params(axis='y', which='major', left=False, right=bool(st['ticks']))
1554
1625
  if 'minor' in st:
1555
1626
  ec_ax.tick_params(axis='y', which='minor', left=False, right=bool(st['minor']))
1556
1627
  if 'labels' in st:
1557
1628
  ec_ax.tick_params(axis='y', which='major', labelleft=False, labelright=bool(st['labels']))
1629
+ print(f"[DEBUG UNDO] EC right restored: ticks={st.get('ticks')}, labels={st.get('labels')}")
1558
1630
  # Title restoration
1559
1631
  if side == 'top' and 'title' in st:
1560
1632
  setattr(ec_ax, '_top_xlabel_on', bool(st['title']))
1561
1633
  elif side == 'right' and 'title' in st:
1562
1634
  # EC right title is actual ylabel, not duplicate
1635
+ print(f"[DEBUG UNDO] EC right title state: {st['title']}, current ylabel: '{ec_ax.get_ylabel()}'")
1563
1636
  if bool(st['title']):
1564
- # Keep existing ylabel or restore from ec_labels
1565
- pass # ylabel already restored above
1637
+ # Ylabel should be visible - restore from _stored_ylabel if it's currently empty
1638
+ if not ec_ax.get_ylabel() and hasattr(ec_ax, '_stored_ylabel'):
1639
+ ec_ax.set_ylabel(ec_ax._stored_ylabel)
1640
+ print(f"[DEBUG UNDO] Restored EC ylabel from _stored_ylabel: '{ec_ax._stored_ylabel}'")
1566
1641
  else:
1567
1642
  # Hide ylabel
1643
+ if not hasattr(ec_ax, '_stored_ylabel'):
1644
+ ec_ax._stored_ylabel = ec_ax.get_ylabel()
1568
1645
  ec_ax.set_ylabel('')
1646
+ print(f"[DEBUG UNDO] Hid EC ylabel, stored: '{ec_ax._stored_ylabel}'")
1569
1647
  # Re-position titles using UI module functions
1570
1648
  try:
1571
1649
  # Build current tick state dict for UI functions
@@ -1597,11 +1675,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1597
1675
  ec_tick_state['b_ticks'] = st.get('ticks', True)
1598
1676
  ec_tick_state['b_labels'] = st.get('labels', True)
1599
1677
  elif side == 'left':
1600
- ec_tick_state['l_ticks'] = st.get('ticks', False)
1601
- ec_tick_state['l_labels'] = st.get('labels', False)
1678
+ ec_tick_state['l_ticks'] = st.get('ticks', False) # EC: left is off by default
1679
+ ec_tick_state['l_labels'] = st.get('labels', False) # EC: left labels off
1602
1680
  elif side == 'right':
1603
- ec_tick_state['r_ticks'] = st.get('ticks', True)
1604
- ec_tick_state['r_labels'] = st.get('labels', True)
1681
+ ec_tick_state['r_ticks'] = st.get('ticks', True) # EC: right ticks ON by default
1682
+ ec_tick_state['r_labels'] = st.get('labels', True) # EC: right labels ON by default
1683
+ print(f"[DEBUG UNDO] EC tick_state: r_ticks={ec_tick_state.get('r_ticks')}, r_labels={ec_tick_state.get('r_labels')}")
1605
1684
  # Position titles
1606
1685
  _ui_position_top_xlabel(ax, fig, op_tick_state)
1607
1686
  _ui_position_bottom_xlabel(ax, fig, op_tick_state)
@@ -1664,10 +1743,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1664
1743
  minor = tick_lengths.get('minor')
1665
1744
  if major is not None:
1666
1745
  ax.tick_params(axis='both', which='major', length=major)
1667
- ec_ax.tick_params(axis='both', which='major', length=major)
1746
+ if ec_ax is not None:
1747
+ ec_ax.tick_params(axis='both', which='major', length=major)
1668
1748
  if minor is not None:
1669
1749
  ax.tick_params(axis='both', which='minor', length=minor)
1670
- ec_ax.tick_params(axis='both', which='minor', length=minor)
1750
+ if ec_ax is not None:
1751
+ ec_ax.tick_params(axis='both', which='minor', length=minor)
1671
1752
  fig._tick_lengths = tick_lengths
1672
1753
  except Exception:
1673
1754
  pass
@@ -1675,10 +1756,68 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1675
1756
  try:
1676
1757
  tick_dir = snap.get('tick_direction', 'out')
1677
1758
  ax.tick_params(axis='both', which='both', direction=tick_dir)
1678
- ec_ax.tick_params(axis='both', which='both', direction=tick_dir)
1759
+ if ec_ax is not None:
1760
+ ec_ax.tick_params(axis='both', which='both', direction=tick_dir)
1679
1761
  fig._tick_direction = tick_dir
1680
1762
  except Exception:
1681
1763
  pass
1764
+ # Restore spine linewidths and tick widths (l command)
1765
+ try:
1766
+ op_sp = snap.get('op_spines', {})
1767
+ if op_sp:
1768
+ for name, lw in op_sp.items():
1769
+ sp = ax.spines.get(name)
1770
+ if sp is not None and lw is not None:
1771
+ sp.set_linewidth(float(lw))
1772
+ op_tw = snap.get('op_ticks', {})
1773
+ if op_tw:
1774
+ if op_tw.get('x_major') is not None:
1775
+ ax.tick_params(axis='x', which='major', width=op_tw['x_major'])
1776
+ if op_tw.get('x_minor') is not None:
1777
+ ax.tick_params(axis='x', which='minor', width=op_tw['x_minor'])
1778
+ if op_tw.get('y_major') is not None:
1779
+ ax.tick_params(axis='y', which='major', width=op_tw['y_major'])
1780
+ if op_tw.get('y_minor') is not None:
1781
+ ax.tick_params(axis='y', which='minor', width=op_tw['y_minor'])
1782
+ except Exception:
1783
+ pass
1784
+ try:
1785
+ if ec_ax is not None:
1786
+ ec_sp = snap.get('ec_spines', {})
1787
+ if ec_sp:
1788
+ for name, lw in ec_sp.items():
1789
+ sp = ec_ax.spines.get(name)
1790
+ if sp is not None and lw is not None:
1791
+ sp.set_linewidth(float(lw))
1792
+ ec_tw = snap.get('ec_ticks', {})
1793
+ if ec_tw:
1794
+ if ec_tw.get('x_major') is not None:
1795
+ ec_ax.tick_params(axis='x', which='major', width=ec_tw['x_major'])
1796
+ if ec_tw.get('x_minor') is not None:
1797
+ ec_ax.tick_params(axis='x', which='minor', width=ec_tw['x_minor'])
1798
+ if ec_tw.get('y_major') is not None:
1799
+ ec_ax.tick_params(axis='y', which='major', width=ec_tw['y_major'])
1800
+ if ec_tw.get('y_minor') is not None:
1801
+ ec_ax.tick_params(axis='y', which='minor', width=ec_tw['y_minor'])
1802
+ except Exception:
1803
+ pass
1804
+ # Restore EC line style (el command)
1805
+ try:
1806
+ ec_line_style = snap.get('ec_line_style')
1807
+ if ec_line_style and ec_ax is not None:
1808
+ ln = getattr(ec_ax, '_ec_line', None)
1809
+ if ln is None and ec_ax.lines:
1810
+ try:
1811
+ ln = ec_ax.lines[0]
1812
+ except Exception:
1813
+ ln = None
1814
+ if ln is not None:
1815
+ if ec_line_style.get('color') is not None:
1816
+ ln.set_color(ec_line_style['color'])
1817
+ if ec_line_style.get('linewidth') is not None:
1818
+ ln.set_linewidth(float(ec_line_style['linewidth']))
1819
+ except Exception:
1820
+ pass
1682
1821
  # Restore visibility states
1683
1822
  try:
1684
1823
  cb_vis = snap.get('cb_visible')
@@ -2954,6 +3093,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2954
3093
  try: axis._stored_ylabel = axis.get_ylabel()
2955
3094
  except Exception: axis._stored_ylabel = ''
2956
3095
  axis.set_ylabel("")
3096
+ # Set flag for right title state (used by save/export)
3097
+ axis._right_ylabel_on = bool(wasd_state['right']['title'])
2957
3098
  # Left ylabel is disabled for EC (hide any duplicate artist)
2958
3099
  # Note: EC uses the actual ylabel which is already on the right side
2959
3100
  else:
@@ -2977,7 +3118,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2977
3118
  if 'left' in changed_sides:
2978
3119
  _ui_position_left_ylabel(axis, fig, current_tick_state)
2979
3120
  if 'right' in changed_sides:
2980
- _ui_position_right_ylabel(axis, fig, current_tick_state)
3121
+ # EC axes use actual ylabel on right, not duplicate artist
3122
+ # Skip _ui_position_right_ylabel for EC to avoid creating unwanted duplicate
3123
+ if not is_ec:
3124
+ _ui_position_right_ylabel(axis, fig, current_tick_state)
2981
3125
 
2982
3126
  print(_colorize_inline_commands("WASD toggles: direction (w/a/s/d) x action (1..5)"))
2983
3127
  print(_colorize_inline_commands(" 1=spine 2=ticks 3=minor ticks 4=tick labels 5=axis title"))
@@ -4269,7 +4413,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4269
4413
  print_menu()
4270
4414
  elif cmd == 'i':
4271
4415
  # Load a .bps/.bpsg/.bpcfg style and apply
4272
- # Applies style properties from commands: oc, ow, ew, h, el, t, l, f, g, r
4416
+ # Applies: oc, ow, ew, h, el, t, l, f, g, r, v; .bpsg also applies ox, oy, oz, or, et, ex, ey, er (axes_geometry + ec y_mode)
4273
4417
  try:
4274
4418
  path = choose_style_file(file_paths, purpose="style import")
4275
4419
  if not path:
batplot/session.py CHANGED
@@ -564,6 +564,7 @@ def dump_session(
564
564
  'tick_state': dict(tick_state),
565
565
  'tick_widths': tick_widths,
566
566
  'tick_lengths': tick_lengths,
567
+ 'tick_direction': getattr(fig, '_tick_direction', 'out'),
567
568
  'font': {
568
569
  'size': plt.rcParams.get('font.size'),
569
570
  'chain': list(plt.rcParams.get('font.sans-serif', [])),
@@ -709,20 +710,34 @@ def dump_operando_session(
709
710
  def _capture_wasd_state(axis):
710
711
  ts = getattr(axis, '_saved_tick_state', {})
711
712
  wasd = {}
713
+ # Check if ylabel is positioned on right (typical for EC axis)
714
+ ylabel_on_right = False
715
+ try:
716
+ ylabel_on_right = (axis.yaxis.get_label_position() == 'right')
717
+ except Exception:
718
+ pass
719
+
712
720
  for side in ('top', 'bottom', 'left', 'right'):
713
721
  sp = axis.spines.get(side)
714
722
  prefix = {'top': 't', 'bottom': 'b', 'left': 'l', 'right': 'r'}[side]
715
- # For 'left' side ylabel: check if it's currently visible (has text)
716
- # If hidden but has stored text, the title state should be False (hidden)
723
+ # Title state logic
717
724
  if side == 'left':
718
- ylabel_text = axis.get_ylabel()
719
- title_state = bool(ylabel_text) # True only if currently visible with text
725
+ # If ylabel is positioned on right (EC axis), left has no title
726
+ if ylabel_on_right:
727
+ title_state = False
728
+ else:
729
+ ylabel_text = axis.get_ylabel()
730
+ title_state = bool(ylabel_text) # True only if currently visible with text
720
731
  elif side == 'bottom':
721
732
  title_state = bool(axis.get_xlabel())
722
733
  elif side == 'top':
723
734
  title_state = bool(getattr(axis, '_top_xlabel_on', False))
724
735
  elif side == 'right':
725
- title_state = bool(getattr(axis, '_right_ylabel_on', False))
736
+ # If ylabel is positioned on right (EC axis), check if ylabel is visible (not empty)
737
+ if ylabel_on_right:
738
+ title_state = bool(axis.get_ylabel()) # Empty string = hidden by user
739
+ else:
740
+ title_state = bool(getattr(axis, '_right_ylabel_on', False))
726
741
  else:
727
742
  title_state = False
728
743
 
@@ -949,6 +964,11 @@ def load_operando_session(filename: str):
949
964
  # Use standard DPI of 100 instead of saved DPI to avoid display-dependent issues
950
965
  # (Retina displays, Windows scaling, etc. can cause saved DPI to differ)
951
966
  fig = plt.figure(figsize=tuple(sess['figure']['size']), dpi=100)
967
+ # Seed last-session path so 'os' overwrite command is available immediately
968
+ try:
969
+ fig._last_session_save_path = os.path.abspath(filename)
970
+ except Exception:
971
+ pass
952
972
  # Disable automatic layout adjustments to preserve saved geometry
953
973
  try:
954
974
  fig.set_layout_engine('none')
@@ -1235,11 +1255,33 @@ def load_operando_session(filename: str):
1235
1255
  bottom=bool(ec_wasd.get('bottom', {}).get('ticks', True)),
1236
1256
  labeltop=bool(ec_wasd.get('top', {}).get('labels', False)),
1237
1257
  labelbottom=bool(ec_wasd.get('bottom', {}).get('labels', True)))
1258
+ # For EC: ticks and labels are on RIGHT by default, not left!
1259
+ # CRITICAL: EC y-axis defaults are: left=False, right=True (both ticks and labels)
1260
+ # Old sessions may have saved wrong values, so we need to sanitize them
1261
+
1262
+ # EC left side should ALWAYS be False (EC uses right side for y-axis)
1263
+ left_ticks = False
1264
+ left_labels = False
1265
+
1266
+ # EC right side should be True when ylabel is visible
1267
+ right_title = ec_wasd.get('right', {}).get('title', True)
1268
+
1269
+ # If right title is ON, ticks/labels should also be ON
1270
+ if right_title:
1271
+ right_ticks = True
1272
+ right_labels = True
1273
+ else:
1274
+ # Title is hidden - respect the saved tick/label state or use False
1275
+ right_ticks_val = ec_wasd.get('right', {}).get('ticks')
1276
+ right_labels_val = ec_wasd.get('right', {}).get('labels')
1277
+ right_ticks = bool(right_ticks_val) if right_ticks_val is not None else False
1278
+ right_labels = bool(right_labels_val) if right_labels_val is not None else False
1279
+
1238
1280
  ec_ax.tick_params(axis='y',
1239
- left=bool(ec_wasd.get('left', {}).get('ticks', True)),
1240
- right=bool(ec_wasd.get('right', {}).get('ticks', False)),
1241
- labelleft=bool(ec_wasd.get('left', {}).get('labels', True)),
1242
- labelright=bool(ec_wasd.get('right', {}).get('labels', False)))
1281
+ left=left_ticks,
1282
+ right=right_ticks,
1283
+ labelleft=left_labels,
1284
+ labelright=right_labels)
1243
1285
  # Apply minor ticks
1244
1286
  if ec_wasd.get('top', {}).get('minor') or ec_wasd.get('bottom', {}).get('minor'):
1245
1287
  ec_ax.xaxis.set_minor_locator(AutoMinorLocator())
@@ -1818,6 +1860,11 @@ def load_ec_session(filename: str):
1818
1860
  # Use standard DPI of 100 instead of saved DPI to avoid display-dependent issues
1819
1861
  # (Retina displays, Windows scaling, etc. can cause saved DPI to differ)
1820
1862
  fig = plt.figure(figsize=tuple(sess['figure']['size']), dpi=100)
1863
+ # Seed last-session path so 'os' overwrite command is available immediately
1864
+ try:
1865
+ fig._last_session_save_path = os.path.abspath(filename)
1866
+ except Exception:
1867
+ pass
1821
1868
  # Preserve saved geometry by disabling auto layout
1822
1869
  try:
1823
1870
  fig.set_layout_engine('none')
@@ -2717,6 +2764,11 @@ def load_cpc_session(filename: str):
2717
2764
  # Use standard DPI of 100 instead of saved DPI to avoid display-dependent issues
2718
2765
  # (Retina displays, Windows scaling, etc. can cause saved DPI to differ)
2719
2766
  fig = plt.figure(figsize=tuple(sess['figure']['size']), dpi=100)
2767
+ # Seed last-session path so 'os' overwrite command is available immediately
2768
+ try:
2769
+ fig._last_session_save_path = os.path.abspath(filename)
2770
+ except Exception:
2771
+ pass
2720
2772
  # Disable auto layout
2721
2773
  try:
2722
2774
  fig.set_layout_engine('none')