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/__init__.py +1 -1
- batplot/batplot.py +125 -53
- batplot/cpc_interactive.py +286 -140
- batplot/electrochem_interactive.py +57 -44
- batplot/interactive.py +216 -2
- batplot/modes.py +12 -11
- batplot/operando_ec_interactive.py +158 -14
- batplot/session.py +61 -9
- batplot/style.py +109 -19
- {batplot-1.8.5.dist-info → batplot-1.8.6.dist-info}/METADATA +1 -1
- {batplot-1.8.5.dist-info → batplot-1.8.6.dist-info}/RECORD +15 -15
- {batplot-1.8.5.dist-info → batplot-1.8.6.dist-info}/WHEEL +0 -0
- {batplot-1.8.5.dist-info → batplot-1.8.6.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.5.dist-info → batplot-1.8.6.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.8.5.dist-info → batplot-1.8.6.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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
|
-
|
|
660
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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':
|
|
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':
|
|
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
|
-
#
|
|
1565
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
716
|
-
# If hidden but has stored text, the title state should be False (hidden)
|
|
723
|
+
# Title state logic
|
|
717
724
|
if side == 'left':
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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=
|
|
1240
|
-
right=
|
|
1241
|
-
labelleft=
|
|
1242
|
-
labelright=
|
|
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')
|