batplot 1.7.24__py3-none-any.whl → 1.7.25__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 +2 -2
- batplot/cpc_interactive.py +775 -319
- batplot/electrochem_interactive.py +1 -1
- batplot/interactive.py +1 -1
- batplot/operando_ec_interactive.py +1 -1
- batplot/session.py +121 -17
- {batplot-1.7.24.dist-info → batplot-1.7.25.dist-info}/METADATA +1 -1
- {batplot-1.7.24.dist-info → batplot-1.7.25.dist-info}/RECORD +13 -13
- {batplot-1.7.24.dist-info → batplot-1.7.25.dist-info}/WHEEL +0 -0
- {batplot-1.7.24.dist-info → batplot-1.7.25.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.24.dist-info → batplot-1.7.25.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.24.dist-info → batplot-1.7.25.dist-info}/top_level.txt +0 -0
batplot/cpc_interactive.py
CHANGED
|
@@ -69,6 +69,27 @@ def _legend_no_frame(ax, *args, **kwargs):
|
|
|
69
69
|
pass
|
|
70
70
|
return leg
|
|
71
71
|
|
|
72
|
+
|
|
73
|
+
def _visible_handles_labels(ax, ax2):
|
|
74
|
+
"""Return handles/labels for visible artists only."""
|
|
75
|
+
try:
|
|
76
|
+
h1, l1 = ax.get_legend_handles_labels()
|
|
77
|
+
except Exception:
|
|
78
|
+
h1, l1 = [], []
|
|
79
|
+
try:
|
|
80
|
+
h2, l2 = ax2.get_legend_handles_labels()
|
|
81
|
+
except Exception:
|
|
82
|
+
h2, l2 = [], []
|
|
83
|
+
H, L = [], []
|
|
84
|
+
for h, l in list(zip(h1, l1)) + list(zip(h2, l2)):
|
|
85
|
+
try:
|
|
86
|
+
if hasattr(h, 'get_visible') and not h.get_visible():
|
|
87
|
+
continue
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
H.append(h); L.append(l)
|
|
91
|
+
return H, L
|
|
92
|
+
|
|
72
93
|
def _colorize_menu(text):
|
|
73
94
|
"""Colorize menu items: command in cyan, colon in white, description in default."""
|
|
74
95
|
if ':' not in text:
|
|
@@ -99,8 +120,8 @@ def _color_of(artist):
|
|
|
99
120
|
return None
|
|
100
121
|
|
|
101
122
|
|
|
102
|
-
def _get_legend_title(fig, default: str =
|
|
103
|
-
"""Fetch stored legend title, falling back to current legend text or
|
|
123
|
+
def _get_legend_title(fig, default: Optional[str] = None) -> Optional[str]:
|
|
124
|
+
"""Fetch stored legend title, falling back to current legend text or None."""
|
|
104
125
|
try:
|
|
105
126
|
title = getattr(fig, '_cpc_legend_title', None)
|
|
106
127
|
if isinstance(title, str) and title:
|
|
@@ -215,7 +236,7 @@ def _print_menu():
|
|
|
215
236
|
" v: show/hide files",
|
|
216
237
|
]
|
|
217
238
|
col2 = [
|
|
218
|
-
"r: rename
|
|
239
|
+
"r: rename",
|
|
219
240
|
"x: x range",
|
|
220
241
|
"y: y ranges",
|
|
221
242
|
]
|
|
@@ -262,9 +283,22 @@ def _print_file_list(file_data, current_idx):
|
|
|
262
283
|
print()
|
|
263
284
|
|
|
264
285
|
|
|
265
|
-
def _rebuild_legend(ax, ax2, file_data):
|
|
266
|
-
"""Rebuild legend from all visible files.
|
|
286
|
+
def _rebuild_legend(ax, ax2, file_data, preserve_position=True):
|
|
287
|
+
"""Rebuild legend from all visible files.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
preserve_position: If True, preserve legend position after rebuilding.
|
|
291
|
+
"""
|
|
267
292
|
try:
|
|
293
|
+
fig = ax.figure
|
|
294
|
+
# Get stored position before rebuilding
|
|
295
|
+
xy_in = None
|
|
296
|
+
if preserve_position:
|
|
297
|
+
try:
|
|
298
|
+
xy_in = getattr(fig, '_cpc_legend_xy_in', None)
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
268
302
|
h1, l1 = ax.get_legend_handles_labels()
|
|
269
303
|
h2, l2 = ax2.get_legend_handles_labels()
|
|
270
304
|
# Filter to only visible items
|
|
@@ -273,8 +307,22 @@ def _rebuild_legend(ax, ax2, file_data):
|
|
|
273
307
|
if h.get_visible():
|
|
274
308
|
h_all.append(h)
|
|
275
309
|
l_all.append(l)
|
|
310
|
+
|
|
276
311
|
if h_all:
|
|
277
|
-
|
|
312
|
+
# Get legend title (None if not set, to avoid showing "Legend")
|
|
313
|
+
leg_title = _get_legend_title(fig, default=None)
|
|
314
|
+
|
|
315
|
+
if xy_in is not None and preserve_position:
|
|
316
|
+
# Use stored position
|
|
317
|
+
try:
|
|
318
|
+
fw, fh = fig.get_size_inches()
|
|
319
|
+
fx = 0.5 + float(xy_in[0]) / float(fw)
|
|
320
|
+
fy = 0.5 + float(xy_in[1]) / float(fh)
|
|
321
|
+
_legend_no_frame(ax, h_all, l_all, loc='center', bbox_to_anchor=(fx, fy), bbox_transform=fig.transFigure, borderaxespad=1.0, title=leg_title)
|
|
322
|
+
except Exception:
|
|
323
|
+
_legend_no_frame(ax, h_all, l_all, loc='best', borderaxespad=1.0, title=leg_title)
|
|
324
|
+
else:
|
|
325
|
+
_legend_no_frame(ax, h_all, l_all, loc='best', borderaxespad=1.0, title=leg_title)
|
|
278
326
|
else:
|
|
279
327
|
leg = ax.get_legend()
|
|
280
328
|
if leg:
|
|
@@ -338,24 +386,35 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
338
386
|
return None
|
|
339
387
|
return None
|
|
340
388
|
|
|
341
|
-
|
|
389
|
+
def _label_visible(lbl):
|
|
390
|
+
try:
|
|
391
|
+
return bool(lbl.get_visible()) and bool(lbl.get_text())
|
|
392
|
+
except Exception:
|
|
393
|
+
return bool(lbl.get_text()) if hasattr(lbl, 'get_text') else False
|
|
394
|
+
|
|
395
|
+
# Current tick visibility (prefer persisted WASD state when available)
|
|
342
396
|
tick_vis = {
|
|
343
|
-
'bx': True,
|
|
344
|
-
'
|
|
345
|
-
'ly': True,
|
|
346
|
-
'ry': True,
|
|
347
|
-
'mbx': False,
|
|
348
|
-
'mtx': False,
|
|
349
|
-
'mly': False,
|
|
350
|
-
'mry': False,
|
|
397
|
+
'bx': True, 'tx': False, 'ly': True, 'ry': True,
|
|
398
|
+
'mbx': False, 'mtx': False, 'mly': False, 'mry': False,
|
|
351
399
|
}
|
|
352
400
|
try:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
401
|
+
wasd_from_fig = getattr(fig, '_cpc_wasd_state', None)
|
|
402
|
+
if isinstance(wasd_from_fig, dict) and wasd_from_fig:
|
|
403
|
+
# Use stored state (authoritative)
|
|
404
|
+
tick_vis['bx'] = bool(wasd_from_fig.get('bottom', {}).get('labels', True))
|
|
405
|
+
tick_vis['tx'] = bool(wasd_from_fig.get('top', {}).get('labels', False))
|
|
406
|
+
tick_vis['ly'] = bool(wasd_from_fig.get('left', {}).get('labels', True))
|
|
407
|
+
tick_vis['ry'] = bool(wasd_from_fig.get('right', {}).get('labels', True))
|
|
408
|
+
tick_vis['mbx'] = bool(wasd_from_fig.get('bottom', {}).get('minor', False))
|
|
409
|
+
tick_vis['mtx'] = bool(wasd_from_fig.get('top', {}).get('minor', False))
|
|
410
|
+
tick_vis['mly'] = bool(wasd_from_fig.get('left', {}).get('minor', False))
|
|
411
|
+
tick_vis['mry'] = bool(wasd_from_fig.get('right', {}).get('minor', False))
|
|
412
|
+
else:
|
|
413
|
+
# Infer from current axes state
|
|
414
|
+
tick_vis['bx'] = any(lbl.get_visible() for lbl in ax.get_xticklabels())
|
|
415
|
+
tick_vis['tx'] = False # CPC doesn't duplicate top labels by default
|
|
416
|
+
tick_vis['ly'] = any(lbl.get_visible() for lbl in ax.get_yticklabels())
|
|
417
|
+
tick_vis['ry'] = any(lbl.get_visible() for lbl in ax2.get_yticklabels())
|
|
359
418
|
except Exception:
|
|
360
419
|
pass
|
|
361
420
|
|
|
@@ -373,36 +432,38 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
373
432
|
except Exception:
|
|
374
433
|
return False
|
|
375
434
|
|
|
376
|
-
wasd_state =
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
'
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
'
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
'
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
'
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
435
|
+
wasd_state = getattr(fig, '_cpc_wasd_state', None)
|
|
436
|
+
if not isinstance(wasd_state, dict) or not wasd_state:
|
|
437
|
+
wasd_state = {
|
|
438
|
+
'bottom': {
|
|
439
|
+
'spine': _get_spine_visible(ax, 'bottom'),
|
|
440
|
+
'ticks': bool(tick_vis.get('bx', True)),
|
|
441
|
+
'minor': bool(tick_vis.get('mbx', False)),
|
|
442
|
+
'labels': bool(tick_vis.get('bx', True)), # bottom x labels
|
|
443
|
+
'title': bool(ax.get_xlabel()) # bottom x title
|
|
444
|
+
},
|
|
445
|
+
'top': {
|
|
446
|
+
'spine': _get_spine_visible(ax, 'top'),
|
|
447
|
+
'ticks': bool(tick_vis.get('tx', False)),
|
|
448
|
+
'minor': bool(tick_vis.get('mtx', False)),
|
|
449
|
+
'labels': bool(tick_vis.get('tx', False)),
|
|
450
|
+
'title': bool(getattr(ax, '_top_xlabel_text', None) and getattr(ax._top_xlabel_text, 'get_visible', lambda: False)())
|
|
451
|
+
},
|
|
452
|
+
'left': {
|
|
453
|
+
'spine': _get_spine_visible(ax, 'left'),
|
|
454
|
+
'ticks': bool(tick_vis.get('ly', True)),
|
|
455
|
+
'minor': bool(tick_vis.get('mly', False)),
|
|
456
|
+
'labels': bool(tick_vis.get('ly', True)), # left y labels (capacity)
|
|
457
|
+
'title': _label_visible(ax.yaxis.label) # left y title
|
|
458
|
+
},
|
|
459
|
+
'right': {
|
|
460
|
+
'spine': _get_spine_visible(ax2, 'right'),
|
|
461
|
+
'ticks': bool(tick_vis.get('ry', True)),
|
|
462
|
+
'minor': bool(tick_vis.get('mry', False)),
|
|
463
|
+
'labels': bool(tick_vis.get('ry', True)), # right y labels (efficiency)
|
|
464
|
+
'title': _label_visible(ax2.yaxis.label) # right y title respects visibility
|
|
465
|
+
},
|
|
466
|
+
}
|
|
406
467
|
|
|
407
468
|
# Capture legend state
|
|
408
469
|
legend_visible = False
|
|
@@ -515,8 +576,29 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
515
576
|
'discharge_color': _color_of(f.get('sc_discharge')),
|
|
516
577
|
'efficiency_color': _color_of(f.get('sc_eff')),
|
|
517
578
|
}
|
|
579
|
+
# Save legend labels
|
|
580
|
+
try:
|
|
581
|
+
sc_chg = f.get('sc_charge')
|
|
582
|
+
sc_dchg = f.get('sc_discharge')
|
|
583
|
+
sc_eff = f.get('sc_eff')
|
|
584
|
+
if sc_chg and hasattr(sc_chg, 'get_label'):
|
|
585
|
+
file_info['charge_label'] = sc_chg.get_label() or ''
|
|
586
|
+
if sc_dchg and hasattr(sc_dchg, 'get_label'):
|
|
587
|
+
file_info['discharge_label'] = sc_dchg.get_label() or ''
|
|
588
|
+
if sc_eff and hasattr(sc_eff, 'get_label'):
|
|
589
|
+
file_info['efficiency_label'] = sc_eff.get_label() or ''
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
518
592
|
multi_files.append(file_info)
|
|
519
593
|
cfg['multi_files'] = multi_files
|
|
594
|
+
else:
|
|
595
|
+
# Single file mode: save legend labels
|
|
596
|
+
try:
|
|
597
|
+
cfg['series']['charge']['label'] = sc_charge.get_label() if hasattr(sc_charge, 'get_label') else ''
|
|
598
|
+
cfg['series']['discharge']['label'] = sc_discharge.get_label() if hasattr(sc_discharge, 'get_label') else ''
|
|
599
|
+
cfg['series']['efficiency']['label'] = sc_eff.get_label() if hasattr(sc_eff, 'get_label') else ''
|
|
600
|
+
except Exception:
|
|
601
|
+
pass
|
|
520
602
|
|
|
521
603
|
return cfg
|
|
522
604
|
|
|
@@ -728,6 +810,11 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
728
810
|
tk = cfg.get('ticks', {})
|
|
729
811
|
# Try wasd_state first (version 2), fall back to visibility dict (version 1)
|
|
730
812
|
wasd = cfg.get('wasd_state', {})
|
|
813
|
+
if isinstance(wasd, dict) and wasd:
|
|
814
|
+
try:
|
|
815
|
+
setattr(fig, '_cpc_wasd_state', wasd)
|
|
816
|
+
except Exception:
|
|
817
|
+
pass
|
|
731
818
|
if wasd:
|
|
732
819
|
# Use WASD state (20 parameters)
|
|
733
820
|
bx = bool(wasd.get('bottom', {}).get('labels', True))
|
|
@@ -754,6 +841,12 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
754
841
|
ax.tick_params(axis='x', bottom=bx, labelbottom=bx, top=tx, labeltop=tx)
|
|
755
842
|
ax.tick_params(axis='y', left=ly, labelleft=ly)
|
|
756
843
|
ax2.tick_params(axis='y', right=ry, labelright=ry)
|
|
844
|
+
try:
|
|
845
|
+
ax.xaxis.label.set_visible(bool(wasd.get('bottom', {}).get('title', True)) if wasd else bx)
|
|
846
|
+
ax.yaxis.label.set_visible(bool(wasd.get('left', {}).get('title', True)) if wasd else ly)
|
|
847
|
+
ax2.yaxis.label.set_visible(bool(wasd.get('right', {}).get('title', True)) if wasd else ry)
|
|
848
|
+
except Exception:
|
|
849
|
+
pass
|
|
757
850
|
# Minor ticks
|
|
758
851
|
from matplotlib.ticker import AutoMinorLocator, NullFormatter, NullLocator, NullLocator
|
|
759
852
|
if mbx or mtx:
|
|
@@ -949,6 +1042,58 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
949
1042
|
_ui_position_right_ylabel(ax2, fig, tick_state)
|
|
950
1043
|
except Exception:
|
|
951
1044
|
pass
|
|
1045
|
+
# Restore legend labels
|
|
1046
|
+
try:
|
|
1047
|
+
if is_multi_file and file_data:
|
|
1048
|
+
multi_files = cfg.get('multi_files', [])
|
|
1049
|
+
if multi_files and len(multi_files) == len(file_data):
|
|
1050
|
+
for i, f_info in enumerate(multi_files):
|
|
1051
|
+
if i < len(file_data):
|
|
1052
|
+
f = file_data[i]
|
|
1053
|
+
# Restore legend labels
|
|
1054
|
+
if 'charge_label' in f_info and f.get('sc_charge'):
|
|
1055
|
+
try:
|
|
1056
|
+
f['sc_charge'].set_label(f_info['charge_label'])
|
|
1057
|
+
except Exception:
|
|
1058
|
+
pass
|
|
1059
|
+
if 'discharge_label' in f_info and f.get('sc_discharge'):
|
|
1060
|
+
try:
|
|
1061
|
+
f['sc_discharge'].set_label(f_info['discharge_label'])
|
|
1062
|
+
except Exception:
|
|
1063
|
+
pass
|
|
1064
|
+
if 'efficiency_label' in f_info and f.get('sc_eff'):
|
|
1065
|
+
try:
|
|
1066
|
+
f['sc_eff'].set_label(f_info['efficiency_label'])
|
|
1067
|
+
except Exception:
|
|
1068
|
+
pass
|
|
1069
|
+
# Update filename if present
|
|
1070
|
+
if 'filename' in f_info:
|
|
1071
|
+
f['filename'] = f_info['filename']
|
|
1072
|
+
else:
|
|
1073
|
+
# Single file mode: restore legend labels
|
|
1074
|
+
s = cfg.get('series', {})
|
|
1075
|
+
ch = s.get('charge', {})
|
|
1076
|
+
dh = s.get('discharge', {})
|
|
1077
|
+
ef = s.get('efficiency', {})
|
|
1078
|
+
if 'label' in ch and hasattr(sc_charge, 'set_label'):
|
|
1079
|
+
try:
|
|
1080
|
+
sc_charge.set_label(ch['label'])
|
|
1081
|
+
except Exception:
|
|
1082
|
+
pass
|
|
1083
|
+
if 'label' in dh and hasattr(sc_discharge, 'set_label'):
|
|
1084
|
+
try:
|
|
1085
|
+
sc_discharge.set_label(dh['label'])
|
|
1086
|
+
except Exception:
|
|
1087
|
+
pass
|
|
1088
|
+
if 'label' in ef and hasattr(sc_eff, 'set_label'):
|
|
1089
|
+
try:
|
|
1090
|
+
sc_eff.set_label(ef['label'])
|
|
1091
|
+
except Exception:
|
|
1092
|
+
pass
|
|
1093
|
+
# Rebuild legend after restoring labels
|
|
1094
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
1095
|
+
except Exception:
|
|
1096
|
+
pass
|
|
952
1097
|
try:
|
|
953
1098
|
fig.canvas.draw_idle()
|
|
954
1099
|
except Exception:
|
|
@@ -1105,6 +1250,19 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1105
1250
|
'mly': False, # minor left y-axis ticks - hidden by default
|
|
1106
1251
|
'mry': False, # minor right y-axis ticks - hidden by default
|
|
1107
1252
|
}
|
|
1253
|
+
try:
|
|
1254
|
+
saved_wasd = getattr(fig, '_cpc_wasd_state', None)
|
|
1255
|
+
if isinstance(saved_wasd, dict) and saved_wasd:
|
|
1256
|
+
tick_state['bx'] = bool(saved_wasd.get('bottom', {}).get('labels', tick_state['bx']))
|
|
1257
|
+
tick_state['tx'] = bool(saved_wasd.get('top', {}).get('labels', tick_state['tx']))
|
|
1258
|
+
tick_state['ly'] = bool(saved_wasd.get('left', {}).get('labels', tick_state['ly']))
|
|
1259
|
+
tick_state['ry'] = bool(saved_wasd.get('right', {}).get('labels', tick_state['ry']))
|
|
1260
|
+
tick_state['mbx'] = bool(saved_wasd.get('bottom', {}).get('minor', tick_state['mbx']))
|
|
1261
|
+
tick_state['mtx'] = bool(saved_wasd.get('top', {}).get('minor', tick_state['mtx']))
|
|
1262
|
+
tick_state['mly'] = bool(saved_wasd.get('left', {}).get('minor', tick_state['mly']))
|
|
1263
|
+
tick_state['mry'] = bool(saved_wasd.get('right', {}).get('minor', tick_state['mry']))
|
|
1264
|
+
except Exception:
|
|
1265
|
+
pass
|
|
1108
1266
|
|
|
1109
1267
|
# --- Undo stack using style snapshots ---
|
|
1110
1268
|
state_history = [] # list of cfg dicts
|
|
@@ -1181,6 +1339,8 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1181
1339
|
try:
|
|
1182
1340
|
# Apply shared visibility to primary ax; then adjust twin for right side
|
|
1183
1341
|
_ui_update_tick_visibility(ax, tick_state)
|
|
1342
|
+
# Ensure left axis ticks/labels don't appear on right axis
|
|
1343
|
+
ax.tick_params(axis='y', right=False, labelright=False)
|
|
1184
1344
|
# Right axis tick params follow r_* keys
|
|
1185
1345
|
ax2.tick_params(axis='y',
|
|
1186
1346
|
right=tick_state.get('r_ticks', tick_state.get('ry', False)),
|
|
@@ -1239,7 +1399,6 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1239
1399
|
"""Reapply legend position using stored inches offset relative to canvas center."""
|
|
1240
1400
|
try:
|
|
1241
1401
|
xy_in = _sanitize_legend_offset(getattr(fig, '_cpc_legend_xy_in', None))
|
|
1242
|
-
leg = ax.get_legend()
|
|
1243
1402
|
if xy_in is None:
|
|
1244
1403
|
return
|
|
1245
1404
|
# Compute figure-fraction anchor from inches
|
|
@@ -1248,14 +1407,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1248
1407
|
return
|
|
1249
1408
|
fx = 0.5 + float(xy_in[0]) / float(fw)
|
|
1250
1409
|
fy = 0.5 + float(xy_in[1]) / float(fh)
|
|
1251
|
-
# Use current handles/labels
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if h1 or h2:
|
|
1410
|
+
# Use current visible handles/labels
|
|
1411
|
+
H, L = _visible_handles_labels(ax, ax2)
|
|
1412
|
+
if H:
|
|
1255
1413
|
_legend_no_frame(
|
|
1256
1414
|
ax,
|
|
1257
|
-
|
|
1258
|
-
|
|
1415
|
+
H,
|
|
1416
|
+
L,
|
|
1259
1417
|
loc='center',
|
|
1260
1418
|
bbox_to_anchor=(fx, fy),
|
|
1261
1419
|
bbox_transform=fig.transFigure,
|
|
@@ -1279,9 +1437,6 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1279
1437
|
pass
|
|
1280
1438
|
|
|
1281
1439
|
_print_menu()
|
|
1282
|
-
if is_multi_file:
|
|
1283
|
-
print(f"\n[Multi-file mode: {len(file_data)} files loaded]")
|
|
1284
|
-
_print_file_list(file_data, current_file_idx)
|
|
1285
1440
|
|
|
1286
1441
|
while True:
|
|
1287
1442
|
try:
|
|
@@ -1800,7 +1955,50 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1800
1955
|
pass
|
|
1801
1956
|
|
|
1802
1957
|
# Export the figure
|
|
1803
|
-
|
|
1958
|
+
_, _ext = os.path.splitext(target)
|
|
1959
|
+
if _ext.lower() == '.svg':
|
|
1960
|
+
# Temporarily force transparent patches so SVG background stays transparent
|
|
1961
|
+
try:
|
|
1962
|
+
_fig_fc = fig.get_facecolor()
|
|
1963
|
+
except Exception:
|
|
1964
|
+
_fig_fc = None
|
|
1965
|
+
try:
|
|
1966
|
+
_ax_fc = ax.get_facecolor()
|
|
1967
|
+
except Exception:
|
|
1968
|
+
_ax_fc = None
|
|
1969
|
+
try:
|
|
1970
|
+
_ax2_fc = ax2.get_facecolor()
|
|
1971
|
+
except Exception:
|
|
1972
|
+
_ax2_fc = None
|
|
1973
|
+
try:
|
|
1974
|
+
if getattr(fig, 'patch', None) is not None:
|
|
1975
|
+
fig.patch.set_alpha(0.0); fig.patch.set_facecolor('none')
|
|
1976
|
+
if getattr(ax, 'patch', None) is not None:
|
|
1977
|
+
ax.patch.set_alpha(0.0); ax.patch.set_facecolor('none')
|
|
1978
|
+
if getattr(ax2, 'patch', None) is not None:
|
|
1979
|
+
ax2.patch.set_alpha(0.0); ax2.patch.set_facecolor('none')
|
|
1980
|
+
except Exception:
|
|
1981
|
+
pass
|
|
1982
|
+
try:
|
|
1983
|
+
fig.savefig(target, bbox_inches='tight', transparent=True, facecolor='none', edgecolor='none')
|
|
1984
|
+
finally:
|
|
1985
|
+
try:
|
|
1986
|
+
if _fig_fc is not None and getattr(fig, 'patch', None) is not None:
|
|
1987
|
+
fig.patch.set_alpha(1.0); fig.patch.set_facecolor(_fig_fc)
|
|
1988
|
+
except Exception:
|
|
1989
|
+
pass
|
|
1990
|
+
try:
|
|
1991
|
+
if _ax_fc is not None and getattr(ax, 'patch', None) is not None:
|
|
1992
|
+
ax.patch.set_alpha(1.0); ax.patch.set_facecolor(_ax_fc)
|
|
1993
|
+
except Exception:
|
|
1994
|
+
pass
|
|
1995
|
+
try:
|
|
1996
|
+
if _ax2_fc is not None and getattr(ax2, 'patch', None) is not None:
|
|
1997
|
+
ax2.patch.set_alpha(1.0); ax2.patch.set_facecolor(_ax2_fc)
|
|
1998
|
+
except Exception:
|
|
1999
|
+
pass
|
|
2000
|
+
else:
|
|
2001
|
+
fig.savefig(target, bbox_inches='tight')
|
|
1804
2002
|
print(f"Exported figure to {target}")
|
|
1805
2003
|
fig._last_figure_export_path = target
|
|
1806
2004
|
|
|
@@ -1819,6 +2017,58 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1819
2017
|
# Save CPC session (.pkl) with all data and styles
|
|
1820
2018
|
try:
|
|
1821
2019
|
from .session import dump_cpc_session
|
|
2020
|
+
# Sync current tick/title visibility (including minors) into stored WASD state before save
|
|
2021
|
+
try:
|
|
2022
|
+
wasd = getattr(fig, '_cpc_wasd_state', {})
|
|
2023
|
+
if not isinstance(wasd, dict):
|
|
2024
|
+
wasd = {}
|
|
2025
|
+
# bottom
|
|
2026
|
+
w = wasd.setdefault('bottom', {})
|
|
2027
|
+
w['ticks'] = bool(tick_state.get('b_ticks', tick_state.get('bx', True)))
|
|
2028
|
+
w['labels'] = bool(tick_state.get('b_labels', tick_state.get('bx', True)))
|
|
2029
|
+
w['minor'] = bool(tick_state.get('mbx', False))
|
|
2030
|
+
w['title'] = bool(ax.xaxis.label.get_visible())
|
|
2031
|
+
try:
|
|
2032
|
+
sp = ax.spines.get('bottom')
|
|
2033
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', True)
|
|
2034
|
+
except Exception:
|
|
2035
|
+
pass
|
|
2036
|
+
# top
|
|
2037
|
+
w = wasd.setdefault('top', {})
|
|
2038
|
+
w['ticks'] = bool(tick_state.get('t_ticks', tick_state.get('tx', False)))
|
|
2039
|
+
w['labels'] = bool(tick_state.get('t_labels', tick_state.get('tx', False)))
|
|
2040
|
+
w['minor'] = bool(tick_state.get('mtx', False))
|
|
2041
|
+
w['title'] = bool(getattr(ax, '_top_xlabel_on', False))
|
|
2042
|
+
try:
|
|
2043
|
+
sp = ax.spines.get('top')
|
|
2044
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', False)
|
|
2045
|
+
except Exception:
|
|
2046
|
+
pass
|
|
2047
|
+
# left
|
|
2048
|
+
w = wasd.setdefault('left', {})
|
|
2049
|
+
w['ticks'] = bool(tick_state.get('l_ticks', tick_state.get('ly', True)))
|
|
2050
|
+
w['labels'] = bool(tick_state.get('l_labels', tick_state.get('ly', True)))
|
|
2051
|
+
w['minor'] = bool(tick_state.get('mly', False))
|
|
2052
|
+
w['title'] = bool(ax.yaxis.label.get_visible())
|
|
2053
|
+
try:
|
|
2054
|
+
sp = ax.spines.get('left')
|
|
2055
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', True)
|
|
2056
|
+
except Exception:
|
|
2057
|
+
pass
|
|
2058
|
+
# right
|
|
2059
|
+
w = wasd.setdefault('right', {})
|
|
2060
|
+
w['ticks'] = bool(tick_state.get('r_ticks', tick_state.get('ry', True)))
|
|
2061
|
+
w['labels'] = bool(tick_state.get('r_labels', tick_state.get('ry', True)))
|
|
2062
|
+
w['minor'] = bool(tick_state.get('mry', False))
|
|
2063
|
+
w['title'] = bool(ax2.yaxis.label.get_visible() if ax2 is not None else False)
|
|
2064
|
+
try:
|
|
2065
|
+
sp = ax2.spines.get('right') if ax2 is not None else None
|
|
2066
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', True)
|
|
2067
|
+
except Exception:
|
|
2068
|
+
pass
|
|
2069
|
+
setattr(fig, '_cpc_wasd_state', wasd)
|
|
2070
|
+
except Exception:
|
|
2071
|
+
pass
|
|
1822
2072
|
folder = choose_save_path(file_paths, purpose="CPC session save")
|
|
1823
2073
|
if not folder:
|
|
1824
2074
|
_print_menu(); continue
|
|
@@ -2222,11 +2472,28 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2222
2472
|
# Toggle efficiency visibility on the right axis
|
|
2223
2473
|
try:
|
|
2224
2474
|
push_state("toggle-eff")
|
|
2225
|
-
vis = bool(sc_eff.get_visible()) if hasattr(sc_eff, 'get_visible') else True
|
|
2226
|
-
new_vis = not vis
|
|
2227
2475
|
|
|
2228
|
-
#
|
|
2229
|
-
|
|
2476
|
+
# Determine current visibility state (check if any efficiency is visible)
|
|
2477
|
+
if is_multi_file:
|
|
2478
|
+
# In multi-file mode, check if any efficiency is visible
|
|
2479
|
+
any_eff_visible = any(f.get('sc_eff', {}).get_visible() if hasattr(f.get('sc_eff'), 'get_visible') else True for f in file_data if f.get('sc_eff'))
|
|
2480
|
+
new_vis = not any_eff_visible
|
|
2481
|
+
else:
|
|
2482
|
+
# Single file mode
|
|
2483
|
+
vis = bool(sc_eff.get_visible()) if hasattr(sc_eff, 'get_visible') else True
|
|
2484
|
+
new_vis = not vis
|
|
2485
|
+
|
|
2486
|
+
# 1. Hide/show efficiency points (all files in multi-file mode)
|
|
2487
|
+
if is_multi_file:
|
|
2488
|
+
for f in file_data:
|
|
2489
|
+
eff_sc = f.get('sc_eff')
|
|
2490
|
+
if eff_sc is not None:
|
|
2491
|
+
try:
|
|
2492
|
+
eff_sc.set_visible(new_vis)
|
|
2493
|
+
except Exception:
|
|
2494
|
+
pass
|
|
2495
|
+
else:
|
|
2496
|
+
sc_eff.set_visible(new_vis)
|
|
2230
2497
|
|
|
2231
2498
|
# 2. Hide/show right y-axis title
|
|
2232
2499
|
try:
|
|
@@ -2242,68 +2509,42 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2242
2509
|
except Exception:
|
|
2243
2510
|
pass
|
|
2244
2511
|
|
|
2245
|
-
#
|
|
2512
|
+
# Persist WASD state so save/load and styles honor the toggle
|
|
2246
2513
|
try:
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2514
|
+
wasd = getattr(fig, '_cpc_wasd_state', None)
|
|
2515
|
+
if not isinstance(wasd, dict):
|
|
2516
|
+
wasd = {
|
|
2517
|
+
'top': {'spine': bool(ax.spines.get('top').get_visible()) if ax.spines.get('top') else False,
|
|
2518
|
+
'ticks': bool(tick_state.get('t_ticks', tick_state.get('tx', False))),
|
|
2519
|
+
'minor': bool(tick_state.get('mtx', False)),
|
|
2520
|
+
'labels': bool(tick_state.get('t_labels', tick_state.get('tx', False))),
|
|
2521
|
+
'title': bool(getattr(ax, '_top_xlabel_on', False))},
|
|
2522
|
+
'bottom': {'spine': bool(ax.spines.get('bottom').get_visible()) if ax.spines.get('bottom') else True,
|
|
2523
|
+
'ticks': bool(tick_state.get('b_ticks', tick_state.get('bx', True))),
|
|
2524
|
+
'minor': bool(tick_state.get('mbx', False)),
|
|
2525
|
+
'labels': bool(tick_state.get('b_labels', tick_state.get('bx', True))),
|
|
2526
|
+
'title': bool(ax.xaxis.label.get_visible()) and bool(ax.get_xlabel())},
|
|
2527
|
+
'left': {'spine': bool(ax.spines.get('left').get_visible()) if ax.spines.get('left') else True,
|
|
2528
|
+
'ticks': bool(tick_state.get('l_ticks', tick_state.get('ly', True))),
|
|
2529
|
+
'minor': bool(tick_state.get('mly', False)),
|
|
2530
|
+
'labels': bool(tick_state.get('l_labels', tick_state.get('ly', True))),
|
|
2531
|
+
'title': bool(ax.yaxis.label.get_visible()) and bool(ax.get_ylabel())},
|
|
2532
|
+
'right': {'spine': bool(ax2.spines.get('right').get_visible()) if ax2.spines.get('right') else True,
|
|
2533
|
+
'ticks': bool(tick_state.get('r_ticks', tick_state.get('ry', True))),
|
|
2534
|
+
'minor': bool(tick_state.get('mry', False)),
|
|
2535
|
+
'labels': bool(tick_state.get('r_labels', tick_state.get('ry', True))),
|
|
2536
|
+
'title': bool(ax2.yaxis.label.get_visible()) and bool(ax2.get_ylabel())},
|
|
2537
|
+
}
|
|
2538
|
+
wasd.setdefault('right', {})
|
|
2539
|
+
wasd['right']['ticks'] = bool(new_vis)
|
|
2540
|
+
wasd['right']['labels'] = bool(new_vis)
|
|
2541
|
+
wasd['right']['title'] = bool(new_vis)
|
|
2542
|
+
setattr(fig, '_cpc_wasd_state', wasd)
|
|
2259
2543
|
except Exception:
|
|
2260
2544
|
pass
|
|
2261
2545
|
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
def _keep(pair):
|
|
2266
|
-
h, l = pair
|
|
2267
|
-
# Drop invisible handles
|
|
2268
|
-
try:
|
|
2269
|
-
if hasattr(h, 'get_visible') and not h.get_visible():
|
|
2270
|
-
return False
|
|
2271
|
-
except Exception:
|
|
2272
|
-
pass
|
|
2273
|
-
# Drop the efficiency label when hidden
|
|
2274
|
-
if eff_label and (l == eff_label) and not new_vis:
|
|
2275
|
-
return False
|
|
2276
|
-
return True
|
|
2277
|
-
|
|
2278
|
-
vis_pairs1 = [p for p in pairs1 if _keep(p)]
|
|
2279
|
-
vis_pairs2 = [p for p in pairs2 if _keep(p)]
|
|
2280
|
-
H = [h for h, _ in vis_pairs1 + vis_pairs2]
|
|
2281
|
-
L = [l for _, l in vis_pairs1 + vis_pairs2]
|
|
2282
|
-
|
|
2283
|
-
if H:
|
|
2284
|
-
try:
|
|
2285
|
-
# Honor stored inch-based anchor if present; else fallback to 'best'
|
|
2286
|
-
xy_in = getattr(fig, '_cpc_legend_xy_in', None)
|
|
2287
|
-
if xy_in is not None:
|
|
2288
|
-
try:
|
|
2289
|
-
fw, fh = fig.get_size_inches()
|
|
2290
|
-
fx = 0.5 + float(xy_in[0]) / float(fw)
|
|
2291
|
-
fy = 0.5 + float(xy_in[1]) / float(fh)
|
|
2292
|
-
_legend_no_frame(ax, H, L, loc='center', bbox_to_anchor=(fx, fy), bbox_transform=fig.transFigure, borderaxespad=1.0)
|
|
2293
|
-
except Exception:
|
|
2294
|
-
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2295
|
-
else:
|
|
2296
|
-
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2297
|
-
except Exception:
|
|
2298
|
-
pass
|
|
2299
|
-
else:
|
|
2300
|
-
# No visible series: hide legend if present
|
|
2301
|
-
try:
|
|
2302
|
-
leg = ax.get_legend()
|
|
2303
|
-
if leg is not None:
|
|
2304
|
-
leg.set_visible(False)
|
|
2305
|
-
except Exception:
|
|
2306
|
-
pass
|
|
2546
|
+
# 4. Rebuild legend to remove/add efficiency entries (preserve position)
|
|
2547
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
2307
2548
|
|
|
2308
2549
|
fig.canvas.draw_idle()
|
|
2309
2550
|
except Exception:
|
|
@@ -2359,15 +2600,14 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2359
2600
|
leg.set_visible(False)
|
|
2360
2601
|
else:
|
|
2361
2602
|
# Ensure a legend exists at the stored position
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
if h1 or h2:
|
|
2603
|
+
H, L = _visible_handles_labels(ax, ax2)
|
|
2604
|
+
if H:
|
|
2365
2605
|
offset = _sanitize_legend_offset(getattr(fig, '_cpc_legend_xy_in', None))
|
|
2366
2606
|
if offset is not None:
|
|
2367
2607
|
fig._cpc_legend_xy_in = offset
|
|
2368
2608
|
_apply_legend_position()
|
|
2369
2609
|
else:
|
|
2370
|
-
_legend_no_frame(ax,
|
|
2610
|
+
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2371
2611
|
fig.canvas.draw_idle()
|
|
2372
2612
|
except Exception:
|
|
2373
2613
|
pass
|
|
@@ -3299,15 +3539,231 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3299
3539
|
# Rename axis titles
|
|
3300
3540
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
3301
3541
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
3302
|
-
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3542
|
+
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3303
3543
|
while True:
|
|
3304
|
-
print("Rename
|
|
3544
|
+
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3305
3545
|
sub = input("Rename> ").strip().lower()
|
|
3306
3546
|
if not sub:
|
|
3307
3547
|
continue
|
|
3308
3548
|
if sub == 'q':
|
|
3309
3549
|
break
|
|
3310
|
-
if sub == '
|
|
3550
|
+
if sub == 'l':
|
|
3551
|
+
# Rename legend labels (file name in legend)
|
|
3552
|
+
if not is_multi_file:
|
|
3553
|
+
# Single file mode: rename the default file
|
|
3554
|
+
current_file = file_data[0]
|
|
3555
|
+
sc_chg = current_file['sc_charge']
|
|
3556
|
+
sc_dchg = current_file['sc_discharge']
|
|
3557
|
+
sc_eff = current_file['sc_eff']
|
|
3558
|
+
|
|
3559
|
+
# Get current labels
|
|
3560
|
+
chg_label = sc_chg.get_label() or ''
|
|
3561
|
+
dchg_label = sc_dchg.get_label() or ''
|
|
3562
|
+
eff_label = sc_eff.get_label() or ''
|
|
3563
|
+
|
|
3564
|
+
# Extract base filename (everything before " charge", " discharge", or " efficiency")
|
|
3565
|
+
# Also handle patterns like "filename (Chg)", "filename (Dchg)", "filename (Eff)"
|
|
3566
|
+
base_name = current_file.get('filename', 'Data')
|
|
3567
|
+
|
|
3568
|
+
# Try to extract from labels
|
|
3569
|
+
import re
|
|
3570
|
+
for label in [chg_label, dchg_label, eff_label]:
|
|
3571
|
+
if label:
|
|
3572
|
+
# First try to extract from bracket pattern: "filename (Chg)" -> "filename"
|
|
3573
|
+
bracket_match = re.search(r'^(.+?)\s*\([^)]+\)\s*$', label)
|
|
3574
|
+
if bracket_match:
|
|
3575
|
+
potential_base = bracket_match.group(1).strip()
|
|
3576
|
+
if potential_base:
|
|
3577
|
+
base_name = potential_base
|
|
3578
|
+
break
|
|
3579
|
+
else:
|
|
3580
|
+
# Try to extract from text suffix patterns
|
|
3581
|
+
for suffix in [' charge', ' discharge', ' efficiency']:
|
|
3582
|
+
if label.endswith(suffix):
|
|
3583
|
+
potential_base = label[:-len(suffix)].strip()
|
|
3584
|
+
if potential_base:
|
|
3585
|
+
base_name = potential_base
|
|
3586
|
+
break
|
|
3587
|
+
if base_name != current_file.get('filename', 'Data'):
|
|
3588
|
+
break
|
|
3589
|
+
|
|
3590
|
+
print(f"Current file name in legend: '{base_name}'")
|
|
3591
|
+
new_name = input("Enter new file name (q=cancel): ").strip()
|
|
3592
|
+
if new_name and new_name.lower() != 'q':
|
|
3593
|
+
try:
|
|
3594
|
+
push_state("rename-legend")
|
|
3595
|
+
|
|
3596
|
+
# Extract bracket content from original labels if present
|
|
3597
|
+
import re
|
|
3598
|
+
chg_bracket = ''
|
|
3599
|
+
dchg_bracket = ''
|
|
3600
|
+
eff_bracket = ''
|
|
3601
|
+
|
|
3602
|
+
# Check for bracket patterns in original labels
|
|
3603
|
+
chg_match = re.search(r'\(([^)]+)\)', chg_label)
|
|
3604
|
+
if chg_match:
|
|
3605
|
+
chg_bracket = chg_match.group(1)
|
|
3606
|
+
dchg_match = re.search(r'\(([^)]+)\)', dchg_label)
|
|
3607
|
+
if dchg_match:
|
|
3608
|
+
dchg_bracket = dchg_match.group(1)
|
|
3609
|
+
# Fix capitalization: Dchg -> DChg
|
|
3610
|
+
if dchg_bracket.lower() == 'dchg':
|
|
3611
|
+
dchg_bracket = 'DChg'
|
|
3612
|
+
eff_match = re.search(r'\(([^)]+)\)', eff_label)
|
|
3613
|
+
if eff_match:
|
|
3614
|
+
eff_bracket = eff_match.group(1)
|
|
3615
|
+
|
|
3616
|
+
# If no brackets found, extract from label suffix or use defaults
|
|
3617
|
+
if not chg_bracket:
|
|
3618
|
+
# Try to extract from " charge" suffix
|
|
3619
|
+
if chg_label.endswith(' charge'):
|
|
3620
|
+
chg_bracket = 'Chg'
|
|
3621
|
+
else:
|
|
3622
|
+
chg_bracket = 'Chg'
|
|
3623
|
+
if not dchg_bracket:
|
|
3624
|
+
# Try to extract from " discharge" suffix
|
|
3625
|
+
if dchg_label.endswith(' discharge'):
|
|
3626
|
+
dchg_bracket = 'DChg'
|
|
3627
|
+
else:
|
|
3628
|
+
dchg_bracket = 'DChg'
|
|
3629
|
+
if not eff_bracket:
|
|
3630
|
+
# Try to extract from " efficiency" suffix
|
|
3631
|
+
if eff_label.endswith(' efficiency'):
|
|
3632
|
+
eff_bracket = 'Eff'
|
|
3633
|
+
else:
|
|
3634
|
+
eff_bracket = 'Eff'
|
|
3635
|
+
|
|
3636
|
+
# Build new labels with brackets preserved
|
|
3637
|
+
new_chg_label = f"{new_name} ({chg_bracket})"
|
|
3638
|
+
new_dchg_label = f"{new_name} ({dchg_bracket})"
|
|
3639
|
+
new_eff_label = f"{new_name} ({eff_bracket})"
|
|
3640
|
+
|
|
3641
|
+
# Update labels
|
|
3642
|
+
sc_chg.set_label(new_chg_label)
|
|
3643
|
+
sc_dchg.set_label(new_dchg_label)
|
|
3644
|
+
sc_eff.set_label(new_eff_label)
|
|
3645
|
+
|
|
3646
|
+
# Update filename in file_data
|
|
3647
|
+
current_file['filename'] = new_name
|
|
3648
|
+
|
|
3649
|
+
# Rebuild legend (preserve position)
|
|
3650
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
3651
|
+
fig.canvas.draw_idle()
|
|
3652
|
+
print(f"Legend labels updated: '{new_chg_label}', '{new_dchg_label}', '{new_eff_label}'")
|
|
3653
|
+
except Exception as e:
|
|
3654
|
+
print(f"Error: {e}")
|
|
3655
|
+
else:
|
|
3656
|
+
# Multi-file mode: show file list and let user select
|
|
3657
|
+
print("\nAvailable files:")
|
|
3658
|
+
_print_file_list(file_data, current_file_idx)
|
|
3659
|
+
file_choice = input("Enter file number to rename (q=cancel): ").strip()
|
|
3660
|
+
if file_choice and file_choice.lower() != 'q':
|
|
3661
|
+
try:
|
|
3662
|
+
file_idx = int(file_choice) - 1
|
|
3663
|
+
if 0 <= file_idx < len(file_data):
|
|
3664
|
+
current_file = file_data[file_idx]
|
|
3665
|
+
sc_chg = current_file['sc_charge']
|
|
3666
|
+
sc_dchg = current_file['sc_discharge']
|
|
3667
|
+
sc_eff = current_file['sc_eff']
|
|
3668
|
+
|
|
3669
|
+
# Get current labels
|
|
3670
|
+
chg_label = sc_chg.get_label() or ''
|
|
3671
|
+
dchg_label = sc_dchg.get_label() or ''
|
|
3672
|
+
eff_label = sc_eff.get_label() or ''
|
|
3673
|
+
|
|
3674
|
+
# Extract base filename
|
|
3675
|
+
base_name = current_file.get('filename', 'Data')
|
|
3676
|
+
import re
|
|
3677
|
+
for label in [chg_label, dchg_label, eff_label]:
|
|
3678
|
+
if label:
|
|
3679
|
+
# First try to extract from bracket pattern: "filename (Chg)" -> "filename"
|
|
3680
|
+
bracket_match = re.search(r'^(.+?)\s*\([^)]+\)\s*$', label)
|
|
3681
|
+
if bracket_match:
|
|
3682
|
+
potential_base = bracket_match.group(1).strip()
|
|
3683
|
+
if potential_base:
|
|
3684
|
+
base_name = potential_base
|
|
3685
|
+
break
|
|
3686
|
+
else:
|
|
3687
|
+
# Try to extract from text suffix patterns
|
|
3688
|
+
for suffix in [' charge', ' discharge', ' efficiency']:
|
|
3689
|
+
if label.endswith(suffix):
|
|
3690
|
+
potential_base = label[:-len(suffix)].strip()
|
|
3691
|
+
if potential_base:
|
|
3692
|
+
base_name = potential_base
|
|
3693
|
+
break
|
|
3694
|
+
if base_name != current_file.get('filename', 'Data'):
|
|
3695
|
+
break
|
|
3696
|
+
|
|
3697
|
+
print(f"Current file name in legend: '{base_name}'")
|
|
3698
|
+
new_name = input("Enter new file name (q=cancel): ").strip()
|
|
3699
|
+
if new_name and new_name.lower() != 'q':
|
|
3700
|
+
try:
|
|
3701
|
+
push_state("rename-legend")
|
|
3702
|
+
|
|
3703
|
+
# Extract bracket content from original labels if present
|
|
3704
|
+
import re
|
|
3705
|
+
chg_bracket = ''
|
|
3706
|
+
dchg_bracket = ''
|
|
3707
|
+
eff_bracket = ''
|
|
3708
|
+
|
|
3709
|
+
# Check for bracket patterns in original labels
|
|
3710
|
+
chg_match = re.search(r'\(([^)]+)\)', chg_label)
|
|
3711
|
+
if chg_match:
|
|
3712
|
+
chg_bracket = chg_match.group(1)
|
|
3713
|
+
dchg_match = re.search(r'\(([^)]+)\)', dchg_label)
|
|
3714
|
+
if dchg_match:
|
|
3715
|
+
dchg_bracket = dchg_match.group(1)
|
|
3716
|
+
# Fix capitalization: Dchg -> DChg
|
|
3717
|
+
if dchg_bracket.lower() == 'dchg':
|
|
3718
|
+
dchg_bracket = 'DChg'
|
|
3719
|
+
eff_match = re.search(r'\(([^)]+)\)', eff_label)
|
|
3720
|
+
if eff_match:
|
|
3721
|
+
eff_bracket = eff_match.group(1)
|
|
3722
|
+
|
|
3723
|
+
# If no brackets found, extract from label suffix or use defaults
|
|
3724
|
+
if not chg_bracket:
|
|
3725
|
+
# Try to extract from " charge" suffix
|
|
3726
|
+
if chg_label.endswith(' charge'):
|
|
3727
|
+
chg_bracket = 'Chg'
|
|
3728
|
+
else:
|
|
3729
|
+
chg_bracket = 'Chg'
|
|
3730
|
+
if not dchg_bracket:
|
|
3731
|
+
# Try to extract from " discharge" suffix
|
|
3732
|
+
if dchg_label.endswith(' discharge'):
|
|
3733
|
+
dchg_bracket = 'DChg'
|
|
3734
|
+
else:
|
|
3735
|
+
dchg_bracket = 'DChg'
|
|
3736
|
+
if not eff_bracket:
|
|
3737
|
+
# Try to extract from " efficiency" suffix
|
|
3738
|
+
if eff_label.endswith(' efficiency'):
|
|
3739
|
+
eff_bracket = 'Eff'
|
|
3740
|
+
else:
|
|
3741
|
+
eff_bracket = 'Eff'
|
|
3742
|
+
|
|
3743
|
+
# Build new labels with brackets preserved
|
|
3744
|
+
new_chg_label = f"{new_name} ({chg_bracket})"
|
|
3745
|
+
new_dchg_label = f"{new_name} ({dchg_bracket})"
|
|
3746
|
+
new_eff_label = f"{new_name} ({eff_bracket})"
|
|
3747
|
+
|
|
3748
|
+
# Update labels
|
|
3749
|
+
sc_chg.set_label(new_chg_label)
|
|
3750
|
+
sc_dchg.set_label(new_dchg_label)
|
|
3751
|
+
sc_eff.set_label(new_eff_label)
|
|
3752
|
+
|
|
3753
|
+
# Update filename in file_data
|
|
3754
|
+
current_file['filename'] = new_name
|
|
3755
|
+
|
|
3756
|
+
# Rebuild legend (preserve position)
|
|
3757
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
3758
|
+
fig.canvas.draw_idle()
|
|
3759
|
+
print(f"Legend labels updated: '{new_chg_label}', '{new_dchg_label}', '{new_eff_label}'")
|
|
3760
|
+
except Exception as e:
|
|
3761
|
+
print(f"Error: {e}")
|
|
3762
|
+
else:
|
|
3763
|
+
print("Invalid file number.")
|
|
3764
|
+
except (ValueError, KeyboardInterrupt):
|
|
3765
|
+
print("Invalid input.")
|
|
3766
|
+
elif sub == 'x':
|
|
3311
3767
|
current = ax.get_xlabel()
|
|
3312
3768
|
print(f"Current x-axis title: '{current}'")
|
|
3313
3769
|
new_title = input("Enter new x-axis title (q=cancel): ")
|
|
@@ -3467,202 +3923,202 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3467
3923
|
if ycmd == 'q':
|
|
3468
3924
|
break
|
|
3469
3925
|
if ycmd == 'ly':
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
ax.autoscale_view(scalex=False, scaley=True)
|
|
3492
|
-
# Reapply legend position after axis change to prevent movement
|
|
3493
|
-
try:
|
|
3494
|
-
leg = ax.get_legend()
|
|
3495
|
-
if leg is not None and leg.get_visible():
|
|
3496
|
-
_apply_legend_position()
|
|
3497
|
-
except Exception:
|
|
3498
|
-
pass
|
|
3499
|
-
fig.canvas.draw_idle()
|
|
3500
|
-
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
3501
|
-
if rng.lower() == 'w':
|
|
3502
|
-
continue
|
|
3503
|
-
if rng.lower() == 's':
|
|
3504
|
-
# Lower only: change lower limit, fix upper - stay in loop
|
|
3505
|
-
while True:
|
|
3506
|
-
current_ylim = ax.get_ylim()
|
|
3507
|
-
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3508
|
-
val = input(f"Enter new lower left Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3509
|
-
if not val or val.lower() == 'q':
|
|
3510
|
-
break
|
|
3511
|
-
try:
|
|
3512
|
-
new_lower = float(val)
|
|
3513
|
-
except (ValueError, KeyboardInterrupt):
|
|
3514
|
-
print("Invalid value, ignored.")
|
|
3515
|
-
continue
|
|
3516
|
-
push_state("y-left-range")
|
|
3517
|
-
ax.set_ylim(new_lower, current_ylim[1])
|
|
3518
|
-
ax.relim()
|
|
3519
|
-
ax.autoscale_view(scalex=False, scaley=True)
|
|
3520
|
-
# Reapply legend position after axis change to prevent movement
|
|
3521
|
-
try:
|
|
3522
|
-
leg = ax.get_legend()
|
|
3523
|
-
if leg is not None and leg.get_visible():
|
|
3524
|
-
_apply_legend_position()
|
|
3525
|
-
except Exception:
|
|
3526
|
-
pass
|
|
3527
|
-
fig.canvas.draw_idle()
|
|
3528
|
-
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
3529
|
-
continue
|
|
3530
|
-
if rng.lower() == 'a':
|
|
3531
|
-
# Auto: restore original range from scatter plots
|
|
3532
|
-
push_state("y-left-range-auto")
|
|
3533
|
-
try:
|
|
3534
|
-
all_y = []
|
|
3535
|
-
for sc in [sc_charge, sc_discharge]:
|
|
3536
|
-
if sc is not None and hasattr(sc, 'get_offsets'):
|
|
3537
|
-
offsets = sc.get_offsets()
|
|
3538
|
-
if offsets.size > 0:
|
|
3539
|
-
all_y.extend([offsets[:, 1].min(), offsets[:, 1].max()])
|
|
3540
|
-
if all_y:
|
|
3541
|
-
orig_min = min(all_y)
|
|
3542
|
-
orig_max = max(all_y)
|
|
3543
|
-
ax.set_ylim(orig_min, orig_max)
|
|
3926
|
+
while True:
|
|
3927
|
+
current_ylim = ax.get_ylim()
|
|
3928
|
+
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3929
|
+
rng = input("Enter left y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3930
|
+
if not rng or rng.lower() == 'q':
|
|
3931
|
+
break
|
|
3932
|
+
if rng.lower() == 'w':
|
|
3933
|
+
# Upper only: change upper limit, fix lower - stay in loop
|
|
3934
|
+
while True:
|
|
3935
|
+
current_ylim = ax.get_ylim()
|
|
3936
|
+
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3937
|
+
val = input(f"Enter new upper left Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
3938
|
+
if not val or val.lower() == 'q':
|
|
3939
|
+
break
|
|
3940
|
+
try:
|
|
3941
|
+
new_upper = float(val)
|
|
3942
|
+
except (ValueError, KeyboardInterrupt):
|
|
3943
|
+
print("Invalid value, ignored.")
|
|
3944
|
+
continue
|
|
3945
|
+
push_state("y-left-range")
|
|
3946
|
+
ax.set_ylim(current_ylim[0], new_upper)
|
|
3544
3947
|
ax.relim()
|
|
3545
3948
|
ax.autoscale_view(scalex=False, scaley=True)
|
|
3949
|
+
# Reapply legend position after axis change to prevent movement
|
|
3950
|
+
try:
|
|
3951
|
+
leg = ax.get_legend()
|
|
3952
|
+
if leg is not None and leg.get_visible():
|
|
3953
|
+
_apply_legend_position()
|
|
3954
|
+
except Exception:
|
|
3955
|
+
pass
|
|
3546
3956
|
fig.canvas.draw_idle()
|
|
3547
|
-
print(f"Left Y range
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
# Upper only: change upper limit, fix lower - stay in loop
|
|
3580
|
-
while True:
|
|
3581
|
-
current_ylim = ax2.get_ylim()
|
|
3582
|
-
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3583
|
-
val = input(f"Enter new upper right Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
3584
|
-
if not val or val.lower() == 'q':
|
|
3585
|
-
break
|
|
3586
|
-
try:
|
|
3587
|
-
new_upper = float(val)
|
|
3588
|
-
except (ValueError, KeyboardInterrupt):
|
|
3589
|
-
print("Invalid value, ignored.")
|
|
3590
|
-
continue
|
|
3591
|
-
push_state("y-right-range")
|
|
3592
|
-
ax2.set_ylim(current_ylim[0], new_upper)
|
|
3593
|
-
ax2.relim()
|
|
3594
|
-
ax2.autoscale_view(scalex=False, scaley=True)
|
|
3595
|
-
# Reapply legend position after axis change to prevent movement
|
|
3596
|
-
try:
|
|
3597
|
-
leg = ax.get_legend()
|
|
3598
|
-
if leg is not None and leg.get_visible():
|
|
3599
|
-
_apply_legend_position()
|
|
3600
|
-
except Exception:
|
|
3601
|
-
pass
|
|
3602
|
-
fig.canvas.draw_idle()
|
|
3603
|
-
print(f"Right Y range updated: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
3604
|
-
if rng.lower() == 'w':
|
|
3605
|
-
continue
|
|
3606
|
-
if rng.lower() == 's':
|
|
3607
|
-
# Lower only: change lower limit, fix upper - stay in loop
|
|
3608
|
-
while True:
|
|
3609
|
-
current_ylim = ax2.get_ylim()
|
|
3610
|
-
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3611
|
-
val = input(f"Enter new lower right Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3612
|
-
if not val or val.lower() == 'q':
|
|
3613
|
-
break
|
|
3614
|
-
try:
|
|
3615
|
-
new_lower = float(val)
|
|
3616
|
-
except (ValueError, KeyboardInterrupt):
|
|
3617
|
-
print("Invalid value, ignored.")
|
|
3618
|
-
continue
|
|
3619
|
-
push_state("y-right-range")
|
|
3620
|
-
ax2.set_ylim(new_lower, current_ylim[1])
|
|
3621
|
-
ax2.relim()
|
|
3622
|
-
ax2.autoscale_view(scalex=False, scaley=True)
|
|
3623
|
-
# Reapply legend position after axis change to prevent movement
|
|
3957
|
+
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
3958
|
+
continue
|
|
3959
|
+
if rng.lower() == 's':
|
|
3960
|
+
# Lower only: change lower limit, fix upper - stay in loop
|
|
3961
|
+
while True:
|
|
3962
|
+
current_ylim = ax.get_ylim()
|
|
3963
|
+
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3964
|
+
val = input(f"Enter new lower left Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3965
|
+
if not val or val.lower() == 'q':
|
|
3966
|
+
break
|
|
3967
|
+
try:
|
|
3968
|
+
new_lower = float(val)
|
|
3969
|
+
except (ValueError, KeyboardInterrupt):
|
|
3970
|
+
print("Invalid value, ignored.")
|
|
3971
|
+
continue
|
|
3972
|
+
push_state("y-left-range")
|
|
3973
|
+
ax.set_ylim(new_lower, current_ylim[1])
|
|
3974
|
+
ax.relim()
|
|
3975
|
+
ax.autoscale_view(scalex=False, scaley=True)
|
|
3976
|
+
# Reapply legend position after axis change to prevent movement
|
|
3977
|
+
try:
|
|
3978
|
+
leg = ax.get_legend()
|
|
3979
|
+
if leg is not None and leg.get_visible():
|
|
3980
|
+
_apply_legend_position()
|
|
3981
|
+
except Exception:
|
|
3982
|
+
pass
|
|
3983
|
+
fig.canvas.draw_idle()
|
|
3984
|
+
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
3985
|
+
continue
|
|
3986
|
+
if rng.lower() == 'a':
|
|
3987
|
+
# Auto: restore original range from scatter plots
|
|
3988
|
+
push_state("y-left-range-auto")
|
|
3624
3989
|
try:
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3990
|
+
all_y = []
|
|
3991
|
+
for sc in [sc_charge, sc_discharge]:
|
|
3992
|
+
if sc is not None and hasattr(sc, 'get_offsets'):
|
|
3993
|
+
offsets = sc.get_offsets()
|
|
3994
|
+
if offsets.size > 0:
|
|
3995
|
+
all_y.extend([offsets[:, 1].min(), offsets[:, 1].max()])
|
|
3996
|
+
if all_y:
|
|
3997
|
+
orig_min = min(all_y)
|
|
3998
|
+
orig_max = max(all_y)
|
|
3999
|
+
ax.set_ylim(orig_min, orig_max)
|
|
4000
|
+
ax.relim()
|
|
4001
|
+
ax.autoscale_view(scalex=False, scaley=True)
|
|
4002
|
+
fig.canvas.draw_idle()
|
|
4003
|
+
print(f"Left Y range restored to original: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
4004
|
+
else:
|
|
4005
|
+
print("No original data available.")
|
|
4006
|
+
except Exception as e:
|
|
4007
|
+
print(f"Error restoring original left Y range: {e}")
|
|
4008
|
+
continue
|
|
4009
|
+
parts = rng.replace(',', ' ').split()
|
|
4010
|
+
if len(parts) != 2:
|
|
4011
|
+
print("Need two numbers."); continue
|
|
4012
|
+
try:
|
|
4013
|
+
lo = float(parts[0]); hi = float(parts[1])
|
|
4014
|
+
if lo == hi:
|
|
4015
|
+
print("Min and max cannot be equal."); continue
|
|
4016
|
+
push_state("y-left-range")
|
|
4017
|
+
ax.set_ylim(min(lo, hi), max(lo, hi))
|
|
3630
4018
|
fig.canvas.draw_idle()
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
push_state("y-right-range-auto")
|
|
4019
|
+
except Exception:
|
|
4020
|
+
print("Invalid numbers.")
|
|
4021
|
+
elif ycmd == 'ry':
|
|
4022
|
+
while True:
|
|
3636
4023
|
try:
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
4024
|
+
eff_on = bool(sc_eff.get_visible())
|
|
4025
|
+
except Exception:
|
|
4026
|
+
eff_on = True
|
|
4027
|
+
if not eff_on:
|
|
4028
|
+
print("Right Y is not shown; enable efficiency with 'ry' first.")
|
|
4029
|
+
break
|
|
4030
|
+
current_ylim = ax2.get_ylim()
|
|
4031
|
+
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4032
|
+
rng = input("Enter right y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
4033
|
+
if not rng or rng.lower() == 'q':
|
|
4034
|
+
break
|
|
4035
|
+
if rng.lower() == 'w':
|
|
4036
|
+
# Upper only: change upper limit, fix lower - stay in loop
|
|
4037
|
+
while True:
|
|
4038
|
+
current_ylim = ax2.get_ylim()
|
|
4039
|
+
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4040
|
+
val = input(f"Enter new upper right Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
4041
|
+
if not val or val.lower() == 'q':
|
|
4042
|
+
break
|
|
4043
|
+
try:
|
|
4044
|
+
new_upper = float(val)
|
|
4045
|
+
except (ValueError, KeyboardInterrupt):
|
|
4046
|
+
print("Invalid value, ignored.")
|
|
4047
|
+
continue
|
|
4048
|
+
push_state("y-right-range")
|
|
4049
|
+
ax2.set_ylim(current_ylim[0], new_upper)
|
|
4050
|
+
ax2.relim()
|
|
4051
|
+
ax2.autoscale_view(scalex=False, scaley=True)
|
|
4052
|
+
# Reapply legend position after axis change to prevent movement
|
|
4053
|
+
try:
|
|
4054
|
+
leg = ax.get_legend()
|
|
4055
|
+
if leg is not None and leg.get_visible():
|
|
4056
|
+
_apply_legend_position()
|
|
4057
|
+
except Exception:
|
|
4058
|
+
pass
|
|
4059
|
+
fig.canvas.draw_idle()
|
|
4060
|
+
print(f"Right Y range updated: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
4061
|
+
continue
|
|
4062
|
+
if rng.lower() == 's':
|
|
4063
|
+
# Lower only: change lower limit, fix upper - stay in loop
|
|
4064
|
+
while True:
|
|
4065
|
+
current_ylim = ax2.get_ylim()
|
|
4066
|
+
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4067
|
+
val = input(f"Enter new lower right Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
4068
|
+
if not val or val.lower() == 'q':
|
|
4069
|
+
break
|
|
4070
|
+
try:
|
|
4071
|
+
new_lower = float(val)
|
|
4072
|
+
except (ValueError, KeyboardInterrupt):
|
|
4073
|
+
print("Invalid value, ignored.")
|
|
4074
|
+
continue
|
|
4075
|
+
push_state("y-right-range")
|
|
4076
|
+
ax2.set_ylim(new_lower, current_ylim[1])
|
|
4077
|
+
ax2.relim()
|
|
4078
|
+
ax2.autoscale_view(scalex=False, scaley=True)
|
|
4079
|
+
# Reapply legend position after axis change to prevent movement
|
|
4080
|
+
try:
|
|
4081
|
+
leg = ax.get_legend()
|
|
4082
|
+
if leg is not None and leg.get_visible():
|
|
4083
|
+
_apply_legend_position()
|
|
4084
|
+
except Exception:
|
|
4085
|
+
pass
|
|
4086
|
+
fig.canvas.draw_idle()
|
|
4087
|
+
print(f"Right Y range updated: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
4088
|
+
continue
|
|
4089
|
+
if rng.lower() == 'a':
|
|
4090
|
+
# Auto: restore original range from efficiency scatter plot
|
|
4091
|
+
push_state("y-right-range-auto")
|
|
4092
|
+
try:
|
|
4093
|
+
if sc_eff is not None and hasattr(sc_eff, 'get_offsets'):
|
|
4094
|
+
offsets = sc_eff.get_offsets()
|
|
4095
|
+
if offsets.size > 0:
|
|
4096
|
+
orig_min = float(offsets[:, 1].min())
|
|
4097
|
+
orig_max = float(offsets[:, 1].max())
|
|
4098
|
+
ax2.set_ylim(orig_min, orig_max)
|
|
4099
|
+
ax2.relim()
|
|
4100
|
+
ax2.autoscale_view(scalex=False, scaley=True)
|
|
4101
|
+
fig.canvas.draw_idle()
|
|
4102
|
+
print(f"Right Y range restored to original: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
4103
|
+
else:
|
|
4104
|
+
print("No original data available.")
|
|
3647
4105
|
else:
|
|
3648
4106
|
print("No original data available.")
|
|
3649
|
-
|
|
3650
|
-
print("
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
except Exception:
|
|
3665
|
-
print("Invalid numbers.")
|
|
4107
|
+
except Exception as e:
|
|
4108
|
+
print(f"Error restoring original right Y range: {e}")
|
|
4109
|
+
continue
|
|
4110
|
+
parts = rng.replace(',', ' ').split()
|
|
4111
|
+
if len(parts) != 2:
|
|
4112
|
+
print("Need two numbers."); continue
|
|
4113
|
+
try:
|
|
4114
|
+
lo = float(parts[0]); hi = float(parts[1])
|
|
4115
|
+
if lo == hi:
|
|
4116
|
+
print("Min and max cannot be equal."); continue
|
|
4117
|
+
push_state("y-right-range")
|
|
4118
|
+
ax2.set_ylim(min(lo, hi), max(lo, hi))
|
|
4119
|
+
fig.canvas.draw_idle()
|
|
4120
|
+
except Exception:
|
|
4121
|
+
print("Invalid numbers.")
|
|
3666
4122
|
_print_menu(); continue
|
|
3667
4123
|
else:
|
|
3668
4124
|
print("Unknown key.")
|