batplot 1.7.23__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 +938 -325
- batplot/electrochem_interactive.py +153 -15
- batplot/interactive.py +157 -31
- batplot/operando_ec_interactive.py +163 -12
- batplot/session.py +205 -17
- batplot/style.py +109 -47
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/METADATA +23 -2
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/RECORD +14 -14
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/WHEEL +0 -0
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.23.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:
|
|
@@ -79,6 +100,46 @@ def _colorize_menu(text):
|
|
|
79
100
|
return f"\033[96m{cmd}\033[0m: {desc}" # Cyan for command, default for description
|
|
80
101
|
|
|
81
102
|
|
|
103
|
+
def _color_of(artist):
|
|
104
|
+
"""Return a representative color for a Line2D/PathCollection."""
|
|
105
|
+
try:
|
|
106
|
+
if artist is None:
|
|
107
|
+
return None
|
|
108
|
+
if hasattr(artist, 'get_color'):
|
|
109
|
+
c = artist.get_color()
|
|
110
|
+
if isinstance(c, (list, tuple)) and c and not isinstance(c, str):
|
|
111
|
+
return c[0]
|
|
112
|
+
return c
|
|
113
|
+
if hasattr(artist, 'get_facecolors'):
|
|
114
|
+
arr = artist.get_facecolors()
|
|
115
|
+
if arr is not None and len(arr):
|
|
116
|
+
from matplotlib.colors import to_hex
|
|
117
|
+
return to_hex(arr[0])
|
|
118
|
+
except Exception:
|
|
119
|
+
return None
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
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."""
|
|
125
|
+
try:
|
|
126
|
+
title = getattr(fig, '_cpc_legend_title', None)
|
|
127
|
+
if isinstance(title, str) and title:
|
|
128
|
+
return title
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
try:
|
|
132
|
+
for ax in getattr(fig, 'axes', []):
|
|
133
|
+
leg = ax.get_legend()
|
|
134
|
+
if leg is not None:
|
|
135
|
+
t = leg.get_title().get_text()
|
|
136
|
+
if t:
|
|
137
|
+
return t
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
return default
|
|
141
|
+
|
|
142
|
+
|
|
82
143
|
def _colorize_prompt(text):
|
|
83
144
|
"""Colorize commands within input prompts. Handles formats like (s=size, f=family, q=return) or (y/n)."""
|
|
84
145
|
import re
|
|
@@ -175,7 +236,7 @@ def _print_menu():
|
|
|
175
236
|
" v: show/hide files",
|
|
176
237
|
]
|
|
177
238
|
col2 = [
|
|
178
|
-
"r: rename
|
|
239
|
+
"r: rename",
|
|
179
240
|
"x: x range",
|
|
180
241
|
"y: y ranges",
|
|
181
242
|
]
|
|
@@ -222,9 +283,22 @@ def _print_file_list(file_data, current_idx):
|
|
|
222
283
|
print()
|
|
223
284
|
|
|
224
285
|
|
|
225
|
-
def _rebuild_legend(ax, ax2, file_data):
|
|
226
|
-
"""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
|
+
"""
|
|
227
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
|
+
|
|
228
302
|
h1, l1 = ax.get_legend_handles_labels()
|
|
229
303
|
h2, l2 = ax2.get_legend_handles_labels()
|
|
230
304
|
# Filter to only visible items
|
|
@@ -233,8 +307,22 @@ def _rebuild_legend(ax, ax2, file_data):
|
|
|
233
307
|
if h.get_visible():
|
|
234
308
|
h_all.append(h)
|
|
235
309
|
l_all.append(l)
|
|
310
|
+
|
|
236
311
|
if h_all:
|
|
237
|
-
|
|
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)
|
|
238
326
|
else:
|
|
239
327
|
leg = ax.get_legend()
|
|
240
328
|
if leg:
|
|
@@ -298,24 +386,35 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
298
386
|
return None
|
|
299
387
|
return None
|
|
300
388
|
|
|
301
|
-
|
|
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)
|
|
302
396
|
tick_vis = {
|
|
303
|
-
'bx': True,
|
|
304
|
-
'
|
|
305
|
-
'ly': True,
|
|
306
|
-
'ry': True,
|
|
307
|
-
'mbx': False,
|
|
308
|
-
'mtx': False,
|
|
309
|
-
'mly': False,
|
|
310
|
-
'mry': False,
|
|
397
|
+
'bx': True, 'tx': False, 'ly': True, 'ry': True,
|
|
398
|
+
'mbx': False, 'mtx': False, 'mly': False, 'mry': False,
|
|
311
399
|
}
|
|
312
400
|
try:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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())
|
|
319
418
|
except Exception:
|
|
320
419
|
pass
|
|
321
420
|
|
|
@@ -333,36 +432,38 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
333
432
|
except Exception:
|
|
334
433
|
return False
|
|
335
434
|
|
|
336
|
-
wasd_state =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
'
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
'
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
'
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
'
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
}
|
|
366
467
|
|
|
367
468
|
# Capture legend state
|
|
368
469
|
legend_visible = False
|
|
@@ -398,7 +499,8 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
398
499
|
'font': {'family': fam0, 'size': fsize},
|
|
399
500
|
'legend': {
|
|
400
501
|
'visible': legend_visible,
|
|
401
|
-
'position_inches': legend_xy_in # [x, y] offset from canvas center in inches
|
|
502
|
+
'position_inches': legend_xy_in, # [x, y] offset from canvas center in inches
|
|
503
|
+
'title': _get_legend_title(fig),
|
|
402
504
|
},
|
|
403
505
|
'ticks': {
|
|
404
506
|
'widths': {
|
|
@@ -474,8 +576,29 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
474
576
|
'discharge_color': _color_of(f.get('sc_discharge')),
|
|
475
577
|
'efficiency_color': _color_of(f.get('sc_eff')),
|
|
476
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
|
|
477
592
|
multi_files.append(file_info)
|
|
478
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
|
|
479
602
|
|
|
480
603
|
return cfg
|
|
481
604
|
|
|
@@ -671,6 +794,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
671
794
|
if leg_cfg:
|
|
672
795
|
leg_visible = leg_cfg.get('visible', True)
|
|
673
796
|
leg_xy_in = leg_cfg.get('position_inches')
|
|
797
|
+
if 'title' in leg_cfg:
|
|
798
|
+
fig._cpc_legend_title = leg_cfg.get('title') or _get_legend_title(fig)
|
|
674
799
|
if leg_xy_in is not None:
|
|
675
800
|
fig._cpc_legend_xy_in = _sanitize_legend_offset(tuple(leg_xy_in))
|
|
676
801
|
leg = ax.get_legend()
|
|
@@ -685,6 +810,11 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
685
810
|
tk = cfg.get('ticks', {})
|
|
686
811
|
# Try wasd_state first (version 2), fall back to visibility dict (version 1)
|
|
687
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
|
|
688
818
|
if wasd:
|
|
689
819
|
# Use WASD state (20 parameters)
|
|
690
820
|
bx = bool(wasd.get('bottom', {}).get('labels', True))
|
|
@@ -711,6 +841,12 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
711
841
|
ax.tick_params(axis='x', bottom=bx, labelbottom=bx, top=tx, labeltop=tx)
|
|
712
842
|
ax.tick_params(axis='y', left=ly, labelleft=ly)
|
|
713
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
|
|
714
850
|
# Minor ticks
|
|
715
851
|
from matplotlib.ticker import AutoMinorLocator, NullFormatter, NullLocator, NullLocator
|
|
716
852
|
if mbx or mtx:
|
|
@@ -906,6 +1042,58 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
906
1042
|
_ui_position_right_ylabel(ax2, fig, tick_state)
|
|
907
1043
|
except Exception:
|
|
908
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
|
|
909
1097
|
try:
|
|
910
1098
|
fig.canvas.draw_idle()
|
|
911
1099
|
except Exception:
|
|
@@ -1062,6 +1250,19 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1062
1250
|
'mly': False, # minor left y-axis ticks - hidden by default
|
|
1063
1251
|
'mry': False, # minor right y-axis ticks - hidden by default
|
|
1064
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
|
|
1065
1266
|
|
|
1066
1267
|
# --- Undo stack using style snapshots ---
|
|
1067
1268
|
state_history = [] # list of cfg dicts
|
|
@@ -1138,6 +1339,8 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1138
1339
|
try:
|
|
1139
1340
|
# Apply shared visibility to primary ax; then adjust twin for right side
|
|
1140
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)
|
|
1141
1344
|
# Right axis tick params follow r_* keys
|
|
1142
1345
|
ax2.tick_params(axis='y',
|
|
1143
1346
|
right=tick_state.get('r_ticks', tick_state.get('ry', False)),
|
|
@@ -1196,8 +1399,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1196
1399
|
"""Reapply legend position using stored inches offset relative to canvas center."""
|
|
1197
1400
|
try:
|
|
1198
1401
|
xy_in = _sanitize_legend_offset(getattr(fig, '_cpc_legend_xy_in', None))
|
|
1199
|
-
|
|
1200
|
-
if xy_in is None or leg is None:
|
|
1402
|
+
if xy_in is None:
|
|
1201
1403
|
return
|
|
1202
1404
|
# Compute figure-fraction anchor from inches
|
|
1203
1405
|
fw, fh = fig.get_size_inches()
|
|
@@ -1205,11 +1407,19 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1205
1407
|
return
|
|
1206
1408
|
fx = 0.5 + float(xy_in[0]) / float(fw)
|
|
1207
1409
|
fy = 0.5 + float(xy_in[1]) / float(fh)
|
|
1208
|
-
# Use current handles/labels
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1410
|
+
# Use current visible handles/labels
|
|
1411
|
+
H, L = _visible_handles_labels(ax, ax2)
|
|
1412
|
+
if H:
|
|
1413
|
+
_legend_no_frame(
|
|
1414
|
+
ax,
|
|
1415
|
+
H,
|
|
1416
|
+
L,
|
|
1417
|
+
loc='center',
|
|
1418
|
+
bbox_to_anchor=(fx, fy),
|
|
1419
|
+
bbox_transform=fig.transFigure,
|
|
1420
|
+
borderaxespad=1.0,
|
|
1421
|
+
title=_get_legend_title(fig),
|
|
1422
|
+
)
|
|
1213
1423
|
except Exception:
|
|
1214
1424
|
pass
|
|
1215
1425
|
|
|
@@ -1227,9 +1437,6 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1227
1437
|
pass
|
|
1228
1438
|
|
|
1229
1439
|
_print_menu()
|
|
1230
|
-
if is_multi_file:
|
|
1231
|
-
print(f"\n[Multi-file mode: {len(file_data)} files loaded]")
|
|
1232
|
-
_print_file_list(file_data, current_file_idx)
|
|
1233
1440
|
|
|
1234
1441
|
while True:
|
|
1235
1442
|
try:
|
|
@@ -1680,12 +1887,28 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1680
1887
|
else:
|
|
1681
1888
|
print(f" {i}: {fname}")
|
|
1682
1889
|
|
|
1683
|
-
|
|
1890
|
+
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
1891
|
+
if last_figure_path:
|
|
1892
|
+
fname = input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
1893
|
+
else:
|
|
1894
|
+
fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
1684
1895
|
if not fname or fname.lower() == 'q':
|
|
1685
1896
|
_print_menu(); continue
|
|
1686
1897
|
|
|
1898
|
+
# Check for 'o' option
|
|
1899
|
+
if fname.lower() == 'o':
|
|
1900
|
+
if not last_figure_path:
|
|
1901
|
+
print("No previous export found.")
|
|
1902
|
+
_print_menu(); continue
|
|
1903
|
+
if not os.path.exists(last_figure_path):
|
|
1904
|
+
print(f"Previous export file not found: {last_figure_path}")
|
|
1905
|
+
_print_menu(); continue
|
|
1906
|
+
yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
1907
|
+
if yn != 'y':
|
|
1908
|
+
_print_menu(); continue
|
|
1909
|
+
target = last_figure_path
|
|
1687
1910
|
# Check if user selected a number
|
|
1688
|
-
|
|
1911
|
+
elif fname.isdigit() and files:
|
|
1689
1912
|
idx = int(fname)
|
|
1690
1913
|
if 1 <= idx <= len(files):
|
|
1691
1914
|
name = files[idx-1]
|
|
@@ -1732,8 +1955,52 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1732
1955
|
pass
|
|
1733
1956
|
|
|
1734
1957
|
# Export the figure
|
|
1735
|
-
|
|
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')
|
|
1736
2002
|
print(f"Exported figure to {target}")
|
|
2003
|
+
fig._last_figure_export_path = target
|
|
1737
2004
|
|
|
1738
2005
|
# Restore original labels
|
|
1739
2006
|
if is_multi_file and original_labels:
|
|
@@ -1750,6 +2017,58 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1750
2017
|
# Save CPC session (.pkl) with all data and styles
|
|
1751
2018
|
try:
|
|
1752
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
|
|
1753
2072
|
folder = choose_save_path(file_paths, purpose="CPC session save")
|
|
1754
2073
|
if not folder:
|
|
1755
2074
|
_print_menu(); continue
|
|
@@ -1767,10 +2086,28 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1767
2086
|
print(f" {i}: {f} ({timestamp})")
|
|
1768
2087
|
else:
|
|
1769
2088
|
print(f" {i}: {f}")
|
|
1770
|
-
|
|
2089
|
+
last_session_path = getattr(fig, '_last_session_save_path', None)
|
|
2090
|
+
if last_session_path:
|
|
2091
|
+
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
2092
|
+
else:
|
|
2093
|
+
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
1771
2094
|
choice = input(prompt).strip()
|
|
1772
2095
|
if not choice or choice.lower() == 'q':
|
|
1773
2096
|
_print_menu(); continue
|
|
2097
|
+
if choice.lower() == 'o':
|
|
2098
|
+
# Overwrite last saved session
|
|
2099
|
+
if not last_session_path:
|
|
2100
|
+
print("No previous save found.")
|
|
2101
|
+
_print_menu(); continue
|
|
2102
|
+
if not os.path.exists(last_session_path):
|
|
2103
|
+
print(f"Previous save file not found: {last_session_path}")
|
|
2104
|
+
_print_menu(); continue
|
|
2105
|
+
yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2106
|
+
if yn != 'y':
|
|
2107
|
+
_print_menu(); continue
|
|
2108
|
+
dump_cpc_session(last_session_path, fig=fig, ax=ax, ax2=ax2, sc_charge=sc_charge, sc_discharge=sc_discharge, sc_eff=sc_eff, file_data=file_data, skip_confirm=True)
|
|
2109
|
+
print(f"Overwritten session to {last_session_path}")
|
|
2110
|
+
_print_menu(); continue
|
|
1774
2111
|
if choice.isdigit() and files:
|
|
1775
2112
|
idx = int(choice)
|
|
1776
2113
|
if 1 <= idx <= len(files):
|
|
@@ -1779,10 +2116,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1779
2116
|
if yn != 'y':
|
|
1780
2117
|
_print_menu(); continue
|
|
1781
2118
|
target = os.path.join(folder, name)
|
|
2119
|
+
dump_cpc_session(target, fig=fig, ax=ax, ax2=ax2, sc_charge=sc_charge, sc_discharge=sc_discharge, sc_eff=sc_eff, file_data=file_data, skip_confirm=True)
|
|
2120
|
+
fig._last_session_save_path = target
|
|
2121
|
+
_print_menu(); continue
|
|
1782
2122
|
else:
|
|
1783
2123
|
print("Invalid number.")
|
|
1784
2124
|
_print_menu(); continue
|
|
1785
|
-
|
|
2125
|
+
if choice.lower() != 'o':
|
|
1786
2126
|
name = choice
|
|
1787
2127
|
root, ext = os.path.splitext(name)
|
|
1788
2128
|
if ext == '':
|
|
@@ -1792,7 +2132,8 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1792
2132
|
yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
1793
2133
|
if yn != 'y':
|
|
1794
2134
|
_print_menu(); continue
|
|
1795
|
-
|
|
2135
|
+
dump_cpc_session(target, fig=fig, ax=ax, ax2=ax2, sc_charge=sc_charge, sc_discharge=sc_discharge, sc_eff=sc_eff, file_data=file_data, skip_confirm=True)
|
|
2136
|
+
fig._last_session_save_path = target
|
|
1796
2137
|
except Exception as e:
|
|
1797
2138
|
print(f"Save failed: {e}")
|
|
1798
2139
|
_print_menu(); continue
|
|
@@ -1916,11 +2257,44 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1916
2257
|
else:
|
|
1917
2258
|
print(f" {_i}: {fname}")
|
|
1918
2259
|
|
|
1919
|
-
|
|
2260
|
+
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
2261
|
+
if last_style_path:
|
|
2262
|
+
sub = input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
2263
|
+
else:
|
|
2264
|
+
sub = input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
1920
2265
|
if sub == 'q':
|
|
1921
2266
|
break
|
|
1922
2267
|
if sub == 'r' or sub == '':
|
|
1923
2268
|
continue
|
|
2269
|
+
if sub == 'o':
|
|
2270
|
+
# Overwrite last exported style file
|
|
2271
|
+
if not last_style_path:
|
|
2272
|
+
print("No previous export found.")
|
|
2273
|
+
continue
|
|
2274
|
+
if not os.path.exists(last_style_path):
|
|
2275
|
+
print(f"Previous export file not found: {last_style_path}")
|
|
2276
|
+
continue
|
|
2277
|
+
yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2278
|
+
if yn != 'y':
|
|
2279
|
+
continue
|
|
2280
|
+
# Rebuild config based on current state
|
|
2281
|
+
snap = _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data)
|
|
2282
|
+
# Determine if last export was style-only or style+geometry
|
|
2283
|
+
try:
|
|
2284
|
+
with open(last_style_path, 'r', encoding='utf-8') as f:
|
|
2285
|
+
old_cfg = json.load(f)
|
|
2286
|
+
if old_cfg.get('kind') == 'cpc_style_geom':
|
|
2287
|
+
snap['kind'] = 'cpc_style_geom'
|
|
2288
|
+
snap['geometry'] = _get_geometry_snapshot(ax, ax2)
|
|
2289
|
+
else:
|
|
2290
|
+
snap['kind'] = 'cpc_style'
|
|
2291
|
+
except Exception:
|
|
2292
|
+
snap['kind'] = 'cpc_style'
|
|
2293
|
+
with open(last_style_path, 'w', encoding='utf-8') as f:
|
|
2294
|
+
json.dump(snap, f, indent=2)
|
|
2295
|
+
print(f"Overwritten style to {last_style_path}")
|
|
2296
|
+
style_menu_active = False
|
|
2297
|
+
break
|
|
1924
2298
|
if sub == 'e':
|
|
1925
2299
|
# Ask for ps or psg
|
|
1926
2300
|
print("Export options:")
|
|
@@ -1978,10 +2352,42 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1978
2352
|
print(f" {i}: {fname} ({timestamp})")
|
|
1979
2353
|
else:
|
|
1980
2354
|
print(f" {i}: {fname}")
|
|
1981
|
-
|
|
2355
|
+
if last_style_path:
|
|
2356
|
+
choice = input("Enter new filename, number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
2357
|
+
else:
|
|
2358
|
+
choice = input("Enter new filename or number to overwrite (q=cancel): ").strip()
|
|
1982
2359
|
if not choice or choice.lower() == 'q':
|
|
1983
2360
|
print("Style export canceled.")
|
|
1984
2361
|
continue
|
|
2362
|
+
if choice.lower() == 'o':
|
|
2363
|
+
# Overwrite last exported style file
|
|
2364
|
+
if not last_style_path:
|
|
2365
|
+
print("No previous export found.")
|
|
2366
|
+
continue
|
|
2367
|
+
if not os.path.exists(last_style_path):
|
|
2368
|
+
print(f"Previous export file not found: {last_style_path}")
|
|
2369
|
+
continue
|
|
2370
|
+
yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2371
|
+
if yn != 'y':
|
|
2372
|
+
continue
|
|
2373
|
+
# Rebuild config based on current state
|
|
2374
|
+
snap = _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data)
|
|
2375
|
+
# Determine if last export was style-only or style+geometry
|
|
2376
|
+
try:
|
|
2377
|
+
with open(last_style_path, 'r', encoding='utf-8') as f:
|
|
2378
|
+
old_cfg = json.load(f)
|
|
2379
|
+
if old_cfg.get('kind') == 'cpc_style_geom':
|
|
2380
|
+
snap['kind'] = 'cpc_style_geom'
|
|
2381
|
+
snap['geometry'] = _get_geometry_snapshot(ax, ax2)
|
|
2382
|
+
else:
|
|
2383
|
+
snap['kind'] = 'cpc_style'
|
|
2384
|
+
except Exception:
|
|
2385
|
+
snap['kind'] = 'cpc_style'
|
|
2386
|
+
with open(last_style_path, 'w', encoding='utf-8') as f:
|
|
2387
|
+
json.dump(snap, f, indent=2)
|
|
2388
|
+
print(f"Overwritten style to {last_style_path}")
|
|
2389
|
+
style_menu_active = False
|
|
2390
|
+
break
|
|
1985
2391
|
target = None
|
|
1986
2392
|
if choice.isdigit() and files:
|
|
1987
2393
|
idx = int(choice)
|
|
@@ -2011,6 +2417,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2011
2417
|
with open(target, 'w', encoding='utf-8') as f:
|
|
2012
2418
|
json.dump(snap, f, indent=2)
|
|
2013
2419
|
print(f"Exported CPC style to {target}")
|
|
2420
|
+
fig._last_style_export_path = target
|
|
2014
2421
|
style_menu_active = False # Exit style submenu and return to main menu
|
|
2015
2422
|
break
|
|
2016
2423
|
else:
|
|
@@ -2065,11 +2472,28 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2065
2472
|
# Toggle efficiency visibility on the right axis
|
|
2066
2473
|
try:
|
|
2067
2474
|
push_state("toggle-eff")
|
|
2068
|
-
vis = bool(sc_eff.get_visible()) if hasattr(sc_eff, 'get_visible') else True
|
|
2069
|
-
new_vis = not vis
|
|
2070
2475
|
|
|
2071
|
-
#
|
|
2072
|
-
|
|
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)
|
|
2073
2497
|
|
|
2074
2498
|
# 2. Hide/show right y-axis title
|
|
2075
2499
|
try:
|
|
@@ -2085,68 +2509,42 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2085
2509
|
except Exception:
|
|
2086
2510
|
pass
|
|
2087
2511
|
|
|
2088
|
-
#
|
|
2089
|
-
try:
|
|
2090
|
-
h1, l1 = ax.get_legend_handles_labels()
|
|
2091
|
-
except Exception:
|
|
2092
|
-
h1, l1 = [], []
|
|
2093
|
-
try:
|
|
2094
|
-
h2, l2 = ax2.get_legend_handles_labels()
|
|
2095
|
-
except Exception:
|
|
2096
|
-
h2, l2 = [], []
|
|
2097
|
-
|
|
2098
|
-
# Filter out efficiency entry if hidden
|
|
2099
|
-
eff_label = None
|
|
2512
|
+
# Persist WASD state so save/load and styles honor the toggle
|
|
2100
2513
|
try:
|
|
2101
|
-
|
|
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)
|
|
2102
2543
|
except Exception:
|
|
2103
2544
|
pass
|
|
2104
2545
|
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
def _keep(pair):
|
|
2109
|
-
h, l = pair
|
|
2110
|
-
# Drop invisible handles
|
|
2111
|
-
try:
|
|
2112
|
-
if hasattr(h, 'get_visible') and not h.get_visible():
|
|
2113
|
-
return False
|
|
2114
|
-
except Exception:
|
|
2115
|
-
pass
|
|
2116
|
-
# Drop the efficiency label when hidden
|
|
2117
|
-
if eff_label and (l == eff_label) and not new_vis:
|
|
2118
|
-
return False
|
|
2119
|
-
return True
|
|
2120
|
-
|
|
2121
|
-
vis_pairs1 = [p for p in pairs1 if _keep(p)]
|
|
2122
|
-
vis_pairs2 = [p for p in pairs2 if _keep(p)]
|
|
2123
|
-
H = [h for h, _ in vis_pairs1 + vis_pairs2]
|
|
2124
|
-
L = [l for _, l in vis_pairs1 + vis_pairs2]
|
|
2125
|
-
|
|
2126
|
-
if H:
|
|
2127
|
-
try:
|
|
2128
|
-
# Honor stored inch-based anchor if present; else fallback to 'best'
|
|
2129
|
-
xy_in = getattr(fig, '_cpc_legend_xy_in', None)
|
|
2130
|
-
if xy_in is not None:
|
|
2131
|
-
try:
|
|
2132
|
-
fw, fh = fig.get_size_inches()
|
|
2133
|
-
fx = 0.5 + float(xy_in[0]) / float(fw)
|
|
2134
|
-
fy = 0.5 + float(xy_in[1]) / float(fh)
|
|
2135
|
-
_legend_no_frame(ax, H, L, loc='center', bbox_to_anchor=(fx, fy), bbox_transform=fig.transFigure, borderaxespad=1.0)
|
|
2136
|
-
except Exception:
|
|
2137
|
-
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2138
|
-
else:
|
|
2139
|
-
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2140
|
-
except Exception:
|
|
2141
|
-
pass
|
|
2142
|
-
else:
|
|
2143
|
-
# No visible series: hide legend if present
|
|
2144
|
-
try:
|
|
2145
|
-
leg = ax.get_legend()
|
|
2146
|
-
if leg is not None:
|
|
2147
|
-
leg.set_visible(False)
|
|
2148
|
-
except Exception:
|
|
2149
|
-
pass
|
|
2546
|
+
# 4. Rebuild legend to remove/add efficiency entries (preserve position)
|
|
2547
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
2150
2548
|
|
|
2151
2549
|
fig.canvas.draw_idle()
|
|
2152
2550
|
except Exception:
|
|
@@ -2202,15 +2600,14 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2202
2600
|
leg.set_visible(False)
|
|
2203
2601
|
else:
|
|
2204
2602
|
# Ensure a legend exists at the stored position
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
if h1 or h2:
|
|
2603
|
+
H, L = _visible_handles_labels(ax, ax2)
|
|
2604
|
+
if H:
|
|
2208
2605
|
offset = _sanitize_legend_offset(getattr(fig, '_cpc_legend_xy_in', None))
|
|
2209
2606
|
if offset is not None:
|
|
2210
2607
|
fig._cpc_legend_xy_in = offset
|
|
2211
2608
|
_apply_legend_position()
|
|
2212
2609
|
else:
|
|
2213
|
-
_legend_no_frame(ax,
|
|
2610
|
+
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2214
2611
|
fig.canvas.draw_idle()
|
|
2215
2612
|
except Exception:
|
|
2216
2613
|
pass
|
|
@@ -3142,15 +3539,231 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3142
3539
|
# Rename axis titles
|
|
3143
3540
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
3144
3541
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
3145
|
-
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3542
|
+
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3146
3543
|
while True:
|
|
3147
|
-
print("Rename
|
|
3544
|
+
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3148
3545
|
sub = input("Rename> ").strip().lower()
|
|
3149
3546
|
if not sub:
|
|
3150
3547
|
continue
|
|
3151
3548
|
if sub == 'q':
|
|
3152
3549
|
break
|
|
3153
|
-
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':
|
|
3154
3767
|
current = ax.get_xlabel()
|
|
3155
3768
|
print(f"Current x-axis title: '{current}'")
|
|
3156
3769
|
new_title = input("Enter new x-axis title (q=cancel): ")
|
|
@@ -3310,202 +3923,202 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3310
3923
|
if ycmd == 'q':
|
|
3311
3924
|
break
|
|
3312
3925
|
if ycmd == 'ly':
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
ax.autoscale_view(scalex=False, scaley=True)
|
|
3335
|
-
# Reapply legend position after axis change to prevent movement
|
|
3336
|
-
try:
|
|
3337
|
-
leg = ax.get_legend()
|
|
3338
|
-
if leg is not None and leg.get_visible():
|
|
3339
|
-
_apply_legend_position()
|
|
3340
|
-
except Exception:
|
|
3341
|
-
pass
|
|
3342
|
-
fig.canvas.draw_idle()
|
|
3343
|
-
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
3344
|
-
if rng.lower() == 'w':
|
|
3345
|
-
continue
|
|
3346
|
-
if rng.lower() == 's':
|
|
3347
|
-
# Lower only: change lower limit, fix upper - stay in loop
|
|
3348
|
-
while True:
|
|
3349
|
-
current_ylim = ax.get_ylim()
|
|
3350
|
-
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3351
|
-
val = input(f"Enter new lower left Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3352
|
-
if not val or val.lower() == 'q':
|
|
3353
|
-
break
|
|
3354
|
-
try:
|
|
3355
|
-
new_lower = float(val)
|
|
3356
|
-
except (ValueError, KeyboardInterrupt):
|
|
3357
|
-
print("Invalid value, ignored.")
|
|
3358
|
-
continue
|
|
3359
|
-
push_state("y-left-range")
|
|
3360
|
-
ax.set_ylim(new_lower, current_ylim[1])
|
|
3361
|
-
ax.relim()
|
|
3362
|
-
ax.autoscale_view(scalex=False, scaley=True)
|
|
3363
|
-
# Reapply legend position after axis change to prevent movement
|
|
3364
|
-
try:
|
|
3365
|
-
leg = ax.get_legend()
|
|
3366
|
-
if leg is not None and leg.get_visible():
|
|
3367
|
-
_apply_legend_position()
|
|
3368
|
-
except Exception:
|
|
3369
|
-
pass
|
|
3370
|
-
fig.canvas.draw_idle()
|
|
3371
|
-
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
3372
|
-
continue
|
|
3373
|
-
if rng.lower() == 'a':
|
|
3374
|
-
# Auto: restore original range from scatter plots
|
|
3375
|
-
push_state("y-left-range-auto")
|
|
3376
|
-
try:
|
|
3377
|
-
all_y = []
|
|
3378
|
-
for sc in [sc_charge, sc_discharge]:
|
|
3379
|
-
if sc is not None and hasattr(sc, 'get_offsets'):
|
|
3380
|
-
offsets = sc.get_offsets()
|
|
3381
|
-
if offsets.size > 0:
|
|
3382
|
-
all_y.extend([offsets[:, 1].min(), offsets[:, 1].max()])
|
|
3383
|
-
if all_y:
|
|
3384
|
-
orig_min = min(all_y)
|
|
3385
|
-
orig_max = max(all_y)
|
|
3386
|
-
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)
|
|
3387
3947
|
ax.relim()
|
|
3388
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
|
|
3389
3956
|
fig.canvas.draw_idle()
|
|
3390
|
-
print(f"Left Y range
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
# Upper only: change upper limit, fix lower - stay in loop
|
|
3423
|
-
while True:
|
|
3424
|
-
current_ylim = ax2.get_ylim()
|
|
3425
|
-
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3426
|
-
val = input(f"Enter new upper right Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
3427
|
-
if not val or val.lower() == 'q':
|
|
3428
|
-
break
|
|
3429
|
-
try:
|
|
3430
|
-
new_upper = float(val)
|
|
3431
|
-
except (ValueError, KeyboardInterrupt):
|
|
3432
|
-
print("Invalid value, ignored.")
|
|
3433
|
-
continue
|
|
3434
|
-
push_state("y-right-range")
|
|
3435
|
-
ax2.set_ylim(current_ylim[0], new_upper)
|
|
3436
|
-
ax2.relim()
|
|
3437
|
-
ax2.autoscale_view(scalex=False, scaley=True)
|
|
3438
|
-
# Reapply legend position after axis change to prevent movement
|
|
3439
|
-
try:
|
|
3440
|
-
leg = ax.get_legend()
|
|
3441
|
-
if leg is not None and leg.get_visible():
|
|
3442
|
-
_apply_legend_position()
|
|
3443
|
-
except Exception:
|
|
3444
|
-
pass
|
|
3445
|
-
fig.canvas.draw_idle()
|
|
3446
|
-
print(f"Right Y range updated: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
3447
|
-
if rng.lower() == 'w':
|
|
3448
|
-
continue
|
|
3449
|
-
if rng.lower() == 's':
|
|
3450
|
-
# Lower only: change lower limit, fix upper - stay in loop
|
|
3451
|
-
while True:
|
|
3452
|
-
current_ylim = ax2.get_ylim()
|
|
3453
|
-
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3454
|
-
val = input(f"Enter new lower right Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3455
|
-
if not val or val.lower() == 'q':
|
|
3456
|
-
break
|
|
3457
|
-
try:
|
|
3458
|
-
new_lower = float(val)
|
|
3459
|
-
except (ValueError, KeyboardInterrupt):
|
|
3460
|
-
print("Invalid value, ignored.")
|
|
3461
|
-
continue
|
|
3462
|
-
push_state("y-right-range")
|
|
3463
|
-
ax2.set_ylim(new_lower, current_ylim[1])
|
|
3464
|
-
ax2.relim()
|
|
3465
|
-
ax2.autoscale_view(scalex=False, scaley=True)
|
|
3466
|
-
# 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")
|
|
3467
3989
|
try:
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
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))
|
|
3473
4018
|
fig.canvas.draw_idle()
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
push_state("y-right-range-auto")
|
|
4019
|
+
except Exception:
|
|
4020
|
+
print("Invalid numbers.")
|
|
4021
|
+
elif ycmd == 'ry':
|
|
4022
|
+
while True:
|
|
3479
4023
|
try:
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
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.")
|
|
3490
4105
|
else:
|
|
3491
4106
|
print("No original data available.")
|
|
3492
|
-
|
|
3493
|
-
print("
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
except Exception:
|
|
3508
|
-
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.")
|
|
3509
4122
|
_print_menu(); continue
|
|
3510
4123
|
else:
|
|
3511
4124
|
print("Unknown key.")
|