batplot 1.7.24__py3-none-any.whl → 1.7.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of batplot might be problematic. Click here for more details.
- batplot/__init__.py +1 -1
- batplot/batplot.py +64 -33
- batplot/color_utils.py +13 -6
- batplot/cpc_interactive.py +1274 -627
- batplot/electrochem_interactive.py +21 -3
- batplot/interactive.py +9 -5
- batplot/operando_ec_interactive.py +2 -2
- batplot/session.py +129 -17
- batplot/ui.py +13 -29
- {batplot-1.7.24.dist-info → batplot-1.7.26.dist-info}/METADATA +1 -1
- {batplot-1.7.24.dist-info → batplot-1.7.26.dist-info}/RECORD +15 -15
- {batplot-1.7.24.dist-info → batplot-1.7.26.dist-info}/WHEEL +0 -0
- {batplot-1.7.24.dist-info → batplot-1.7.26.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.24.dist-info → batplot-1.7.26.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.24.dist-info → batplot-1.7.26.dist-info}/top_level.txt +0 -0
batplot/cpc_interactive.py
CHANGED
|
@@ -36,11 +36,45 @@ from __future__ import annotations
|
|
|
36
36
|
from typing import Dict, Optional
|
|
37
37
|
import json
|
|
38
38
|
import os
|
|
39
|
+
import sys
|
|
40
|
+
import contextlib
|
|
41
|
+
from io import StringIO
|
|
39
42
|
|
|
40
43
|
import matplotlib.pyplot as plt
|
|
41
44
|
from matplotlib.ticker import AutoMinorLocator, NullFormatter, NullLocator
|
|
42
45
|
import random as _random
|
|
43
46
|
|
|
47
|
+
|
|
48
|
+
class _FilterIMKWarning:
|
|
49
|
+
"""Filter that suppresses macOS IMKCFRunLoopWakeUpReliable warnings while preserving other errors."""
|
|
50
|
+
def __init__(self, original_stderr):
|
|
51
|
+
self.original_stderr = original_stderr
|
|
52
|
+
|
|
53
|
+
def write(self, message):
|
|
54
|
+
# Filter out the harmless macOS IMK warning
|
|
55
|
+
if 'IMKCFRunLoopWakeUpReliable' not in message:
|
|
56
|
+
self.original_stderr.write(message)
|
|
57
|
+
|
|
58
|
+
def flush(self):
|
|
59
|
+
self.original_stderr.flush()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _safe_input(prompt: str = "") -> str:
|
|
63
|
+
"""Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
|
|
64
|
+
|
|
65
|
+
This is a harmless macOS system message that appears when using input() in terminals.
|
|
66
|
+
"""
|
|
67
|
+
# Filter stderr to hide macOS IMK warnings while preserving other errors
|
|
68
|
+
original_stderr = sys.stderr
|
|
69
|
+
sys.stderr = _FilterIMKWarning(original_stderr)
|
|
70
|
+
try:
|
|
71
|
+
result = input(prompt)
|
|
72
|
+
return result
|
|
73
|
+
except (KeyboardInterrupt, EOFError):
|
|
74
|
+
raise
|
|
75
|
+
finally:
|
|
76
|
+
sys.stderr = original_stderr
|
|
77
|
+
|
|
44
78
|
from .ui import (
|
|
45
79
|
resize_plot_frame, resize_canvas,
|
|
46
80
|
update_tick_visibility as _ui_update_tick_visibility,
|
|
@@ -57,10 +91,20 @@ from .utils import (
|
|
|
57
91
|
get_organized_path,
|
|
58
92
|
)
|
|
59
93
|
import time
|
|
60
|
-
from .color_utils import resolve_color_token
|
|
94
|
+
from .color_utils import resolve_color_token, color_block, palette_preview, manage_user_colors, get_user_color_list, ensure_colormap
|
|
61
95
|
|
|
62
96
|
|
|
63
97
|
def _legend_no_frame(ax, *args, **kwargs):
|
|
98
|
+
# Compact legend defaults and labelcolor matching marker/line color
|
|
99
|
+
kwargs.setdefault('frameon', False)
|
|
100
|
+
kwargs.setdefault('handlelength', 1.0)
|
|
101
|
+
kwargs.setdefault('handletextpad', 0.35)
|
|
102
|
+
kwargs.setdefault('labelspacing', 0.25)
|
|
103
|
+
kwargs.setdefault('borderaxespad', 0.5)
|
|
104
|
+
kwargs.setdefault('borderpad', 0.3)
|
|
105
|
+
kwargs.setdefault('columnspacing', 0.6)
|
|
106
|
+
# Let matplotlib color legend text from line/marker colors
|
|
107
|
+
kwargs.setdefault('labelcolor', 'linecolor')
|
|
64
108
|
leg = ax.legend(*args, **kwargs)
|
|
65
109
|
if leg is not None:
|
|
66
110
|
try:
|
|
@@ -69,6 +113,27 @@ def _legend_no_frame(ax, *args, **kwargs):
|
|
|
69
113
|
pass
|
|
70
114
|
return leg
|
|
71
115
|
|
|
116
|
+
|
|
117
|
+
def _visible_handles_labels(ax, ax2):
|
|
118
|
+
"""Return handles/labels for visible artists only."""
|
|
119
|
+
try:
|
|
120
|
+
h1, l1 = ax.get_legend_handles_labels()
|
|
121
|
+
except Exception:
|
|
122
|
+
h1, l1 = [], []
|
|
123
|
+
try:
|
|
124
|
+
h2, l2 = ax2.get_legend_handles_labels()
|
|
125
|
+
except Exception:
|
|
126
|
+
h2, l2 = [], []
|
|
127
|
+
H, L = [], []
|
|
128
|
+
for h, l in list(zip(h1, l1)) + list(zip(h2, l2)):
|
|
129
|
+
try:
|
|
130
|
+
if hasattr(h, 'get_visible') and not h.get_visible():
|
|
131
|
+
continue
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
H.append(h); L.append(l)
|
|
135
|
+
return H, L
|
|
136
|
+
|
|
72
137
|
def _colorize_menu(text):
|
|
73
138
|
"""Colorize menu items: command in cyan, colon in white, description in default."""
|
|
74
139
|
if ':' not in text:
|
|
@@ -99,8 +164,8 @@ def _color_of(artist):
|
|
|
99
164
|
return None
|
|
100
165
|
|
|
101
166
|
|
|
102
|
-
def _get_legend_title(fig, default: str =
|
|
103
|
-
"""Fetch stored legend title, falling back to current legend text or
|
|
167
|
+
def _get_legend_title(fig, default: Optional[str] = None) -> Optional[str]:
|
|
168
|
+
"""Fetch stored legend title, falling back to current legend text or None."""
|
|
104
169
|
try:
|
|
105
170
|
title = getattr(fig, '_cpc_legend_title', None)
|
|
106
171
|
if isinstance(title, str) and title:
|
|
@@ -215,7 +280,7 @@ def _print_menu():
|
|
|
215
280
|
" v: show/hide files",
|
|
216
281
|
]
|
|
217
282
|
col2 = [
|
|
218
|
-
"r: rename
|
|
283
|
+
"r: rename",
|
|
219
284
|
"x: x range",
|
|
220
285
|
"y: y ranges",
|
|
221
286
|
]
|
|
@@ -262,9 +327,22 @@ def _print_file_list(file_data, current_idx):
|
|
|
262
327
|
print()
|
|
263
328
|
|
|
264
329
|
|
|
265
|
-
def _rebuild_legend(ax, ax2, file_data):
|
|
266
|
-
"""Rebuild legend from all visible files.
|
|
330
|
+
def _rebuild_legend(ax, ax2, file_data, preserve_position=True):
|
|
331
|
+
"""Rebuild legend from all visible files.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
preserve_position: If True, preserve legend position after rebuilding.
|
|
335
|
+
"""
|
|
267
336
|
try:
|
|
337
|
+
fig = ax.figure
|
|
338
|
+
# Get stored position before rebuilding
|
|
339
|
+
xy_in = None
|
|
340
|
+
if preserve_position:
|
|
341
|
+
try:
|
|
342
|
+
xy_in = getattr(fig, '_cpc_legend_xy_in', None)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
268
346
|
h1, l1 = ax.get_legend_handles_labels()
|
|
269
347
|
h2, l2 = ax2.get_legend_handles_labels()
|
|
270
348
|
# Filter to only visible items
|
|
@@ -273,8 +351,22 @@ def _rebuild_legend(ax, ax2, file_data):
|
|
|
273
351
|
if h.get_visible():
|
|
274
352
|
h_all.append(h)
|
|
275
353
|
l_all.append(l)
|
|
354
|
+
|
|
276
355
|
if h_all:
|
|
277
|
-
|
|
356
|
+
# Get legend title (None if not set, to avoid showing "Legend")
|
|
357
|
+
leg_title = _get_legend_title(fig, default=None)
|
|
358
|
+
|
|
359
|
+
if xy_in is not None and preserve_position:
|
|
360
|
+
# Use stored position
|
|
361
|
+
try:
|
|
362
|
+
fw, fh = fig.get_size_inches()
|
|
363
|
+
fx = 0.5 + float(xy_in[0]) / float(fw)
|
|
364
|
+
fy = 0.5 + float(xy_in[1]) / float(fh)
|
|
365
|
+
_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)
|
|
366
|
+
except Exception:
|
|
367
|
+
_legend_no_frame(ax, h_all, l_all, loc='best', borderaxespad=1.0, title=leg_title)
|
|
368
|
+
else:
|
|
369
|
+
_legend_no_frame(ax, h_all, l_all, loc='best', borderaxespad=1.0, title=leg_title)
|
|
278
370
|
else:
|
|
279
371
|
leg = ax.get_legend()
|
|
280
372
|
if leg:
|
|
@@ -338,24 +430,35 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
338
430
|
return None
|
|
339
431
|
return None
|
|
340
432
|
|
|
341
|
-
|
|
433
|
+
def _label_visible(lbl):
|
|
434
|
+
try:
|
|
435
|
+
return bool(lbl.get_visible()) and bool(lbl.get_text())
|
|
436
|
+
except Exception:
|
|
437
|
+
return bool(lbl.get_text()) if hasattr(lbl, 'get_text') else False
|
|
438
|
+
|
|
439
|
+
# Current tick visibility (prefer persisted WASD state when available)
|
|
342
440
|
tick_vis = {
|
|
343
|
-
'bx': True,
|
|
344
|
-
'
|
|
345
|
-
'ly': True,
|
|
346
|
-
'ry': True,
|
|
347
|
-
'mbx': False,
|
|
348
|
-
'mtx': False,
|
|
349
|
-
'mly': False,
|
|
350
|
-
'mry': False,
|
|
441
|
+
'bx': True, 'tx': False, 'ly': True, 'ry': True,
|
|
442
|
+
'mbx': False, 'mtx': False, 'mly': False, 'mry': False,
|
|
351
443
|
}
|
|
352
444
|
try:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
445
|
+
wasd_from_fig = getattr(fig, '_cpc_wasd_state', None)
|
|
446
|
+
if isinstance(wasd_from_fig, dict) and wasd_from_fig:
|
|
447
|
+
# Use stored state (authoritative)
|
|
448
|
+
tick_vis['bx'] = bool(wasd_from_fig.get('bottom', {}).get('labels', True))
|
|
449
|
+
tick_vis['tx'] = bool(wasd_from_fig.get('top', {}).get('labels', False))
|
|
450
|
+
tick_vis['ly'] = bool(wasd_from_fig.get('left', {}).get('labels', True))
|
|
451
|
+
tick_vis['ry'] = bool(wasd_from_fig.get('right', {}).get('labels', True))
|
|
452
|
+
tick_vis['mbx'] = bool(wasd_from_fig.get('bottom', {}).get('minor', False))
|
|
453
|
+
tick_vis['mtx'] = bool(wasd_from_fig.get('top', {}).get('minor', False))
|
|
454
|
+
tick_vis['mly'] = bool(wasd_from_fig.get('left', {}).get('minor', False))
|
|
455
|
+
tick_vis['mry'] = bool(wasd_from_fig.get('right', {}).get('minor', False))
|
|
456
|
+
else:
|
|
457
|
+
# Infer from current axes state
|
|
458
|
+
tick_vis['bx'] = any(lbl.get_visible() for lbl in ax.get_xticklabels())
|
|
459
|
+
tick_vis['tx'] = False # CPC doesn't duplicate top labels by default
|
|
460
|
+
tick_vis['ly'] = any(lbl.get_visible() for lbl in ax.get_yticklabels())
|
|
461
|
+
tick_vis['ry'] = any(lbl.get_visible() for lbl in ax2.get_yticklabels())
|
|
359
462
|
except Exception:
|
|
360
463
|
pass
|
|
361
464
|
|
|
@@ -373,36 +476,38 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
373
476
|
except Exception:
|
|
374
477
|
return False
|
|
375
478
|
|
|
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
|
-
|
|
479
|
+
wasd_state = getattr(fig, '_cpc_wasd_state', None)
|
|
480
|
+
if not isinstance(wasd_state, dict) or not wasd_state:
|
|
481
|
+
wasd_state = {
|
|
482
|
+
'bottom': {
|
|
483
|
+
'spine': _get_spine_visible(ax, 'bottom'),
|
|
484
|
+
'ticks': bool(tick_vis.get('bx', True)),
|
|
485
|
+
'minor': bool(tick_vis.get('mbx', False)),
|
|
486
|
+
'labels': bool(tick_vis.get('bx', True)), # bottom x labels
|
|
487
|
+
'title': bool(ax.get_xlabel()) # bottom x title
|
|
488
|
+
},
|
|
489
|
+
'top': {
|
|
490
|
+
'spine': _get_spine_visible(ax, 'top'),
|
|
491
|
+
'ticks': bool(tick_vis.get('tx', False)),
|
|
492
|
+
'minor': bool(tick_vis.get('mtx', False)),
|
|
493
|
+
'labels': bool(tick_vis.get('tx', False)),
|
|
494
|
+
'title': bool(getattr(ax, '_top_xlabel_text', None) and getattr(ax._top_xlabel_text, 'get_visible', lambda: False)())
|
|
495
|
+
},
|
|
496
|
+
'left': {
|
|
497
|
+
'spine': _get_spine_visible(ax, 'left'),
|
|
498
|
+
'ticks': bool(tick_vis.get('ly', True)),
|
|
499
|
+
'minor': bool(tick_vis.get('mly', False)),
|
|
500
|
+
'labels': bool(tick_vis.get('ly', True)), # left y labels (capacity)
|
|
501
|
+
'title': _label_visible(ax.yaxis.label) # left y title
|
|
502
|
+
},
|
|
503
|
+
'right': {
|
|
504
|
+
'spine': _get_spine_visible(ax2, 'right'),
|
|
505
|
+
'ticks': bool(tick_vis.get('ry', True)),
|
|
506
|
+
'minor': bool(tick_vis.get('mry', False)),
|
|
507
|
+
'labels': bool(tick_vis.get('ry', True)), # right y labels (efficiency)
|
|
508
|
+
'title': _label_visible(ax2.yaxis.label) # right y title respects visibility
|
|
509
|
+
},
|
|
510
|
+
}
|
|
406
511
|
|
|
407
512
|
# Capture legend state
|
|
408
513
|
legend_visible = False
|
|
@@ -487,16 +592,19 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
487
592
|
'series': {
|
|
488
593
|
'charge': {
|
|
489
594
|
'color': _color_of(sc_charge),
|
|
595
|
+
'marker': getattr(sc_charge, 'get_marker', lambda: 'o')(),
|
|
490
596
|
'markersize': float(getattr(sc_charge, 'get_sizes', lambda: [32])()[0]) if hasattr(sc_charge, 'get_sizes') else 32.0,
|
|
491
597
|
'alpha': float(sc_charge.get_alpha()) if sc_charge.get_alpha() is not None else 1.0,
|
|
492
598
|
},
|
|
493
599
|
'discharge': {
|
|
494
600
|
'color': _color_of(sc_discharge),
|
|
601
|
+
'marker': getattr(sc_discharge, 'get_marker', lambda: 's')(),
|
|
495
602
|
'markersize': float(getattr(sc_discharge, 'get_sizes', lambda: [32])()[0]) if hasattr(sc_discharge, 'get_sizes') else 32.0,
|
|
496
603
|
'alpha': float(sc_discharge.get_alpha()) if sc_discharge.get_alpha() is not None else 1.0,
|
|
497
604
|
},
|
|
498
605
|
'efficiency': {
|
|
499
606
|
'color': (sc_eff.get_facecolors()[0].tolist() if hasattr(sc_eff, 'get_facecolors') and len(sc_eff.get_facecolors()) else '#2ca02c'),
|
|
607
|
+
'marker': getattr(sc_eff, 'get_marker', lambda: '^')(),
|
|
500
608
|
'markersize': float(getattr(sc_eff, 'get_sizes', lambda: [40])()[0]) if hasattr(sc_eff, 'get_sizes') else 40.0,
|
|
501
609
|
'alpha': float(sc_eff.get_alpha()) if sc_eff.get_alpha() is not None else 1.0,
|
|
502
610
|
'visible': bool(getattr(sc_eff, 'get_visible', lambda: True)()),
|
|
@@ -508,15 +616,42 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
508
616
|
if file_data and isinstance(file_data, list) and len(file_data) > 0:
|
|
509
617
|
multi_files = []
|
|
510
618
|
for f in file_data:
|
|
619
|
+
sc_chg = f.get('sc_charge')
|
|
620
|
+
sc_dchg = f.get('sc_discharge')
|
|
621
|
+
sc_eff = f.get('sc_eff')
|
|
511
622
|
file_info = {
|
|
512
623
|
'filename': f.get('filename', 'unknown'),
|
|
513
624
|
'visible': f.get('visible', True),
|
|
514
|
-
'charge_color': _color_of(
|
|
515
|
-
'
|
|
516
|
-
'
|
|
625
|
+
'charge_color': _color_of(sc_chg),
|
|
626
|
+
'charge_marker': getattr(sc_chg, 'get_marker', lambda: 'o')() if sc_chg else 'o',
|
|
627
|
+
'discharge_color': _color_of(sc_dchg),
|
|
628
|
+
'discharge_marker': getattr(sc_dchg, 'get_marker', lambda: 's')() if sc_dchg else 's',
|
|
629
|
+
'efficiency_color': _color_of(sc_eff),
|
|
630
|
+
'efficiency_marker': getattr(sc_eff, 'get_marker', lambda: '^')() if sc_eff else '^',
|
|
517
631
|
}
|
|
632
|
+
# Save legend labels
|
|
633
|
+
try:
|
|
634
|
+
sc_chg = f.get('sc_charge')
|
|
635
|
+
sc_dchg = f.get('sc_discharge')
|
|
636
|
+
sc_eff = f.get('sc_eff')
|
|
637
|
+
if sc_chg and hasattr(sc_chg, 'get_label'):
|
|
638
|
+
file_info['charge_label'] = sc_chg.get_label() or ''
|
|
639
|
+
if sc_dchg and hasattr(sc_dchg, 'get_label'):
|
|
640
|
+
file_info['discharge_label'] = sc_dchg.get_label() or ''
|
|
641
|
+
if sc_eff and hasattr(sc_eff, 'get_label'):
|
|
642
|
+
file_info['efficiency_label'] = sc_eff.get_label() or ''
|
|
643
|
+
except Exception:
|
|
644
|
+
pass
|
|
518
645
|
multi_files.append(file_info)
|
|
519
646
|
cfg['multi_files'] = multi_files
|
|
647
|
+
else:
|
|
648
|
+
# Single file mode: save legend labels
|
|
649
|
+
try:
|
|
650
|
+
cfg['series']['charge']['label'] = sc_charge.get_label() if hasattr(sc_charge, 'get_label') else ''
|
|
651
|
+
cfg['series']['discharge']['label'] = sc_discharge.get_label() if hasattr(sc_discharge, 'get_label') else ''
|
|
652
|
+
cfg['series']['efficiency']['label'] = sc_eff.get_label() if hasattr(sc_eff, 'get_label') else ''
|
|
653
|
+
except Exception:
|
|
654
|
+
pass
|
|
520
655
|
|
|
521
656
|
return cfg
|
|
522
657
|
|
|
@@ -644,6 +779,13 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
644
779
|
# Apply marker sizes and alpha globally to all files in multi-file mode
|
|
645
780
|
if is_multi_file:
|
|
646
781
|
for f in file_data:
|
|
782
|
+
# Marker types (global)
|
|
783
|
+
if ch.get('marker') is not None and hasattr(f['sc_charge'], 'set_marker'):
|
|
784
|
+
f['sc_charge'].set_marker(ch['marker'])
|
|
785
|
+
if dh.get('marker') is not None and hasattr(f['sc_discharge'], 'set_marker'):
|
|
786
|
+
f['sc_discharge'].set_marker(dh['marker'])
|
|
787
|
+
if ef.get('marker') is not None and hasattr(f['sc_eff'], 'set_marker'):
|
|
788
|
+
f['sc_eff'].set_marker(ef['marker'])
|
|
647
789
|
# Marker sizes (global)
|
|
648
790
|
if ch.get('markersize') is not None and hasattr(f['sc_charge'], 'set_sizes'):
|
|
649
791
|
f['sc_charge'].set_sizes([float(ch['markersize'])])
|
|
@@ -677,6 +819,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
677
819
|
if ch:
|
|
678
820
|
if ch.get('color') is not None:
|
|
679
821
|
sc_charge.set_color(ch['color'])
|
|
822
|
+
if ch.get('marker') is not None and hasattr(sc_charge, 'set_marker'):
|
|
823
|
+
sc_charge.set_marker(ch['marker'])
|
|
680
824
|
if ch.get('markersize') is not None and hasattr(sc_charge, 'set_sizes'):
|
|
681
825
|
sc_charge.set_sizes([float(ch['markersize'])])
|
|
682
826
|
if ch.get('alpha') is not None:
|
|
@@ -684,6 +828,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
684
828
|
if dh:
|
|
685
829
|
if dh.get('color') is not None:
|
|
686
830
|
sc_discharge.set_color(dh['color'])
|
|
831
|
+
if dh.get('marker') is not None and hasattr(sc_discharge, 'set_marker'):
|
|
832
|
+
sc_discharge.set_marker(dh['marker'])
|
|
687
833
|
if dh.get('markersize') is not None and hasattr(sc_discharge, 'set_sizes'):
|
|
688
834
|
sc_discharge.set_sizes([float(dh['markersize'])])
|
|
689
835
|
if dh.get('alpha') is not None:
|
|
@@ -694,6 +840,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
694
840
|
sc_eff.set_color(ef['color'])
|
|
695
841
|
except Exception:
|
|
696
842
|
pass
|
|
843
|
+
if ef.get('marker') is not None and hasattr(sc_eff, 'set_marker'):
|
|
844
|
+
sc_eff.set_marker(ef['marker'])
|
|
697
845
|
if ef.get('markersize') is not None and hasattr(sc_eff, 'set_sizes'):
|
|
698
846
|
sc_eff.set_sizes([float(ef['markersize'])])
|
|
699
847
|
if ef.get('alpha') is not None:
|
|
@@ -721,6 +869,27 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
721
869
|
leg.set_visible(leg_visible)
|
|
722
870
|
if leg_visible:
|
|
723
871
|
_apply_legend_position()
|
|
872
|
+
# Re-apply legend label colors to match handles after position/visibility changes
|
|
873
|
+
try:
|
|
874
|
+
leg = ax.get_legend()
|
|
875
|
+
if leg is not None:
|
|
876
|
+
handles = list(getattr(leg, "legendHandles", []))
|
|
877
|
+
for h, txt in zip(handles, leg.get_texts()):
|
|
878
|
+
col = _color_of(h)
|
|
879
|
+
if col is None and hasattr(h, 'get_edgecolor'):
|
|
880
|
+
col = h.get_edgecolor()
|
|
881
|
+
if isinstance(col, (list, tuple)) and len(col) and not isinstance(col, str):
|
|
882
|
+
col = col[0]
|
|
883
|
+
try:
|
|
884
|
+
import numpy as _np
|
|
885
|
+
if hasattr(col, "__len__") and not isinstance(col, str):
|
|
886
|
+
col = tuple(_np.array(col).ravel().tolist())
|
|
887
|
+
except Exception:
|
|
888
|
+
pass
|
|
889
|
+
if col is not None:
|
|
890
|
+
txt.set_color(col)
|
|
891
|
+
except Exception:
|
|
892
|
+
pass
|
|
724
893
|
except Exception:
|
|
725
894
|
pass
|
|
726
895
|
# Apply tick visibility/widths and spines
|
|
@@ -728,6 +897,11 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
728
897
|
tk = cfg.get('ticks', {})
|
|
729
898
|
# Try wasd_state first (version 2), fall back to visibility dict (version 1)
|
|
730
899
|
wasd = cfg.get('wasd_state', {})
|
|
900
|
+
if isinstance(wasd, dict) and wasd:
|
|
901
|
+
try:
|
|
902
|
+
setattr(fig, '_cpc_wasd_state', wasd)
|
|
903
|
+
except Exception:
|
|
904
|
+
pass
|
|
731
905
|
if wasd:
|
|
732
906
|
# Use WASD state (20 parameters)
|
|
733
907
|
bx = bool(wasd.get('bottom', {}).get('labels', True))
|
|
@@ -754,6 +928,12 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
754
928
|
ax.tick_params(axis='x', bottom=bx, labelbottom=bx, top=tx, labeltop=tx)
|
|
755
929
|
ax.tick_params(axis='y', left=ly, labelleft=ly)
|
|
756
930
|
ax2.tick_params(axis='y', right=ry, labelright=ry)
|
|
931
|
+
try:
|
|
932
|
+
ax.xaxis.label.set_visible(bool(wasd.get('bottom', {}).get('title', True)) if wasd else bx)
|
|
933
|
+
ax.yaxis.label.set_visible(bool(wasd.get('left', {}).get('title', True)) if wasd else ly)
|
|
934
|
+
ax2.yaxis.label.set_visible(bool(wasd.get('right', {}).get('title', True)) if wasd else ry)
|
|
935
|
+
except Exception:
|
|
936
|
+
pass
|
|
757
937
|
# Minor ticks
|
|
758
938
|
from matplotlib.ticker import AutoMinorLocator, NullFormatter, NullLocator, NullLocator
|
|
759
939
|
if mbx or mtx:
|
|
@@ -949,6 +1129,94 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
949
1129
|
_ui_position_right_ylabel(ax2, fig, tick_state)
|
|
950
1130
|
except Exception:
|
|
951
1131
|
pass
|
|
1132
|
+
# Restore legend labels
|
|
1133
|
+
try:
|
|
1134
|
+
if is_multi_file and file_data:
|
|
1135
|
+
multi_files = cfg.get('multi_files', [])
|
|
1136
|
+
if multi_files and len(multi_files) == len(file_data):
|
|
1137
|
+
for i, f_info in enumerate(multi_files):
|
|
1138
|
+
if i < len(file_data):
|
|
1139
|
+
f = file_data[i]
|
|
1140
|
+
# Restore colors FIRST (before labels)
|
|
1141
|
+
if 'charge_color' in f_info and f.get('sc_charge'):
|
|
1142
|
+
try:
|
|
1143
|
+
col = f_info['charge_color']
|
|
1144
|
+
f['sc_charge'].set_color(col)
|
|
1145
|
+
f['color'] = col
|
|
1146
|
+
# Force update of facecolors for scatter plots
|
|
1147
|
+
if hasattr(f['sc_charge'], 'set_facecolors'):
|
|
1148
|
+
from matplotlib.colors import to_rgba
|
|
1149
|
+
rgba = to_rgba(col)
|
|
1150
|
+
f['sc_charge'].set_facecolors(rgba)
|
|
1151
|
+
except Exception:
|
|
1152
|
+
pass
|
|
1153
|
+
if 'discharge_color' in f_info and f.get('sc_discharge'):
|
|
1154
|
+
try:
|
|
1155
|
+
col = f_info['discharge_color']
|
|
1156
|
+
f['sc_discharge'].set_color(col)
|
|
1157
|
+
# Force update of facecolors for scatter plots
|
|
1158
|
+
if hasattr(f['sc_discharge'], 'set_facecolors'):
|
|
1159
|
+
from matplotlib.colors import to_rgba
|
|
1160
|
+
rgba = to_rgba(col)
|
|
1161
|
+
f['sc_discharge'].set_facecolors(rgba)
|
|
1162
|
+
except Exception:
|
|
1163
|
+
pass
|
|
1164
|
+
if 'efficiency_color' in f_info and f.get('sc_eff'):
|
|
1165
|
+
try:
|
|
1166
|
+
col = f_info['efficiency_color']
|
|
1167
|
+
f['sc_eff'].set_color(col)
|
|
1168
|
+
f['eff_color'] = col
|
|
1169
|
+
# Force update of facecolors for scatter plots
|
|
1170
|
+
if hasattr(f['sc_eff'], 'set_facecolors'):
|
|
1171
|
+
from matplotlib.colors import to_rgba
|
|
1172
|
+
rgba = to_rgba(col)
|
|
1173
|
+
f['sc_eff'].set_facecolors(rgba)
|
|
1174
|
+
except Exception:
|
|
1175
|
+
pass
|
|
1176
|
+
# Restore legend labels
|
|
1177
|
+
if 'charge_label' in f_info and f.get('sc_charge'):
|
|
1178
|
+
try:
|
|
1179
|
+
f['sc_charge'].set_label(f_info['charge_label'])
|
|
1180
|
+
except Exception:
|
|
1181
|
+
pass
|
|
1182
|
+
if 'discharge_label' in f_info and f.get('sc_discharge'):
|
|
1183
|
+
try:
|
|
1184
|
+
f['sc_discharge'].set_label(f_info['discharge_label'])
|
|
1185
|
+
except Exception:
|
|
1186
|
+
pass
|
|
1187
|
+
if 'efficiency_label' in f_info and f.get('sc_eff'):
|
|
1188
|
+
try:
|
|
1189
|
+
f['sc_eff'].set_label(f_info['efficiency_label'])
|
|
1190
|
+
except Exception:
|
|
1191
|
+
pass
|
|
1192
|
+
# Update filename if present
|
|
1193
|
+
if 'filename' in f_info:
|
|
1194
|
+
f['filename'] = f_info['filename']
|
|
1195
|
+
else:
|
|
1196
|
+
# Single file mode: restore legend labels
|
|
1197
|
+
s = cfg.get('series', {})
|
|
1198
|
+
ch = s.get('charge', {})
|
|
1199
|
+
dh = s.get('discharge', {})
|
|
1200
|
+
ef = s.get('efficiency', {})
|
|
1201
|
+
if 'label' in ch and hasattr(sc_charge, 'set_label'):
|
|
1202
|
+
try:
|
|
1203
|
+
sc_charge.set_label(ch['label'])
|
|
1204
|
+
except Exception:
|
|
1205
|
+
pass
|
|
1206
|
+
if 'label' in dh and hasattr(sc_discharge, 'set_label'):
|
|
1207
|
+
try:
|
|
1208
|
+
sc_discharge.set_label(dh['label'])
|
|
1209
|
+
except Exception:
|
|
1210
|
+
pass
|
|
1211
|
+
if 'label' in ef and hasattr(sc_eff, 'set_label'):
|
|
1212
|
+
try:
|
|
1213
|
+
sc_eff.set_label(ef['label'])
|
|
1214
|
+
except Exception:
|
|
1215
|
+
pass
|
|
1216
|
+
# Rebuild legend after restoring labels
|
|
1217
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
1218
|
+
except Exception:
|
|
1219
|
+
pass
|
|
952
1220
|
try:
|
|
953
1221
|
fig.canvas.draw_idle()
|
|
954
1222
|
except Exception:
|
|
@@ -1061,8 +1329,22 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1061
1329
|
if file_data is None:
|
|
1062
1330
|
# Backward compatibility: create file_data structure from single file
|
|
1063
1331
|
# This allows the function to work with old code that passes individual artists
|
|
1332
|
+
# Try to get filename from label if available
|
|
1333
|
+
filename = 'Data'
|
|
1334
|
+
try:
|
|
1335
|
+
if hasattr(sc_charge, 'get_label') and sc_charge.get_label():
|
|
1336
|
+
label = sc_charge.get_label()
|
|
1337
|
+
# Extract filename from label like "filename (Chg)" or use label as-is
|
|
1338
|
+
if ' (Chg)' in label:
|
|
1339
|
+
filename = label.replace(' (Chg)', '')
|
|
1340
|
+
elif ' (Dch)' in label:
|
|
1341
|
+
filename = label.replace(' (Dch)', '')
|
|
1342
|
+
elif label and label != 'Charge capacity':
|
|
1343
|
+
filename = label
|
|
1344
|
+
except Exception:
|
|
1345
|
+
pass
|
|
1064
1346
|
file_data = [{
|
|
1065
|
-
'filename':
|
|
1347
|
+
'filename': filename,
|
|
1066
1348
|
'sc_charge': sc_charge, # Charge capacity scatter artist
|
|
1067
1349
|
'sc_discharge': sc_discharge, # Discharge capacity scatter artist
|
|
1068
1350
|
'sc_eff': sc_eff, # Efficiency scatter artist
|
|
@@ -1105,6 +1387,19 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1105
1387
|
'mly': False, # minor left y-axis ticks - hidden by default
|
|
1106
1388
|
'mry': False, # minor right y-axis ticks - hidden by default
|
|
1107
1389
|
}
|
|
1390
|
+
try:
|
|
1391
|
+
saved_wasd = getattr(fig, '_cpc_wasd_state', None)
|
|
1392
|
+
if isinstance(saved_wasd, dict) and saved_wasd:
|
|
1393
|
+
tick_state['bx'] = bool(saved_wasd.get('bottom', {}).get('labels', tick_state['bx']))
|
|
1394
|
+
tick_state['tx'] = bool(saved_wasd.get('top', {}).get('labels', tick_state['tx']))
|
|
1395
|
+
tick_state['ly'] = bool(saved_wasd.get('left', {}).get('labels', tick_state['ly']))
|
|
1396
|
+
tick_state['ry'] = bool(saved_wasd.get('right', {}).get('labels', tick_state['ry']))
|
|
1397
|
+
tick_state['mbx'] = bool(saved_wasd.get('bottom', {}).get('minor', tick_state['mbx']))
|
|
1398
|
+
tick_state['mtx'] = bool(saved_wasd.get('top', {}).get('minor', tick_state['mtx']))
|
|
1399
|
+
tick_state['mly'] = bool(saved_wasd.get('left', {}).get('minor', tick_state['mly']))
|
|
1400
|
+
tick_state['mry'] = bool(saved_wasd.get('right', {}).get('minor', tick_state['mry']))
|
|
1401
|
+
except Exception:
|
|
1402
|
+
pass
|
|
1108
1403
|
|
|
1109
1404
|
# --- Undo stack using style snapshots ---
|
|
1110
1405
|
state_history = [] # list of cfg dicts
|
|
@@ -1169,6 +1464,27 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1169
1464
|
if k in tick_state:
|
|
1170
1465
|
tick_state[k] = bool(v)
|
|
1171
1466
|
_update_ticks()
|
|
1467
|
+
# Re-apply legend text colors after state restore (undo)
|
|
1468
|
+
try:
|
|
1469
|
+
leg = ax.get_legend()
|
|
1470
|
+
if leg is not None:
|
|
1471
|
+
handles = list(getattr(leg, "legendHandles", []))
|
|
1472
|
+
for h, txt in zip(handles, leg.get_texts()):
|
|
1473
|
+
col = _color_of(h)
|
|
1474
|
+
if col is None and hasattr(h, 'get_edgecolor'):
|
|
1475
|
+
col = h.get_edgecolor()
|
|
1476
|
+
if isinstance(col, (list, tuple)) and len(col) and not isinstance(col, str):
|
|
1477
|
+
col = col[0]
|
|
1478
|
+
try:
|
|
1479
|
+
import numpy as _np
|
|
1480
|
+
if hasattr(col, "__len__") and not isinstance(col, str):
|
|
1481
|
+
col = tuple(_np.array(col).ravel().tolist())
|
|
1482
|
+
except Exception:
|
|
1483
|
+
pass
|
|
1484
|
+
if col is not None:
|
|
1485
|
+
txt.set_color(col)
|
|
1486
|
+
except Exception:
|
|
1487
|
+
pass
|
|
1172
1488
|
try:
|
|
1173
1489
|
fig.canvas.draw()
|
|
1174
1490
|
except Exception:
|
|
@@ -1181,6 +1497,8 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1181
1497
|
try:
|
|
1182
1498
|
# Apply shared visibility to primary ax; then adjust twin for right side
|
|
1183
1499
|
_ui_update_tick_visibility(ax, tick_state)
|
|
1500
|
+
# Ensure left axis ticks/labels don't appear on right axis
|
|
1501
|
+
ax.tick_params(axis='y', right=False, labelright=False)
|
|
1184
1502
|
# Right axis tick params follow r_* keys
|
|
1185
1503
|
ax2.tick_params(axis='y',
|
|
1186
1504
|
right=tick_state.get('r_ticks', tick_state.get('ry', False)),
|
|
@@ -1239,7 +1557,6 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1239
1557
|
"""Reapply legend position using stored inches offset relative to canvas center."""
|
|
1240
1558
|
try:
|
|
1241
1559
|
xy_in = _sanitize_legend_offset(getattr(fig, '_cpc_legend_xy_in', None))
|
|
1242
|
-
leg = ax.get_legend()
|
|
1243
1560
|
if xy_in is None:
|
|
1244
1561
|
return
|
|
1245
1562
|
# Compute figure-fraction anchor from inches
|
|
@@ -1248,14 +1565,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1248
1565
|
return
|
|
1249
1566
|
fx = 0.5 + float(xy_in[0]) / float(fw)
|
|
1250
1567
|
fy = 0.5 + float(xy_in[1]) / float(fh)
|
|
1251
|
-
# Use current handles/labels
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if h1 or h2:
|
|
1568
|
+
# Use current visible handles/labels
|
|
1569
|
+
H, L = _visible_handles_labels(ax, ax2)
|
|
1570
|
+
if H:
|
|
1255
1571
|
_legend_no_frame(
|
|
1256
1572
|
ax,
|
|
1257
|
-
|
|
1258
|
-
|
|
1573
|
+
H,
|
|
1574
|
+
L,
|
|
1259
1575
|
loc='center',
|
|
1260
1576
|
bbox_to_anchor=(fx, fy),
|
|
1261
1577
|
bbox_transform=fig.transFigure,
|
|
@@ -1279,16 +1595,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1279
1595
|
pass
|
|
1280
1596
|
|
|
1281
1597
|
_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
1598
|
|
|
1286
1599
|
while True:
|
|
1287
1600
|
try:
|
|
1288
1601
|
# Update current file's scatter artists for commands that need them
|
|
1289
1602
|
sc_charge, sc_discharge, sc_eff = _get_current_file_artists(file_data, current_file_idx)
|
|
1290
1603
|
|
|
1291
|
-
key =
|
|
1604
|
+
key = _safe_input("Press a key: ").strip().lower()
|
|
1292
1605
|
except (KeyboardInterrupt, EOFError):
|
|
1293
1606
|
print("\n\nExiting interactive menu...")
|
|
1294
1607
|
break
|
|
@@ -1300,7 +1613,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1300
1613
|
try:
|
|
1301
1614
|
if is_multi_file:
|
|
1302
1615
|
_print_file_list(file_data, current_file_idx)
|
|
1303
|
-
choice =
|
|
1616
|
+
choice = _safe_input(f"Toggle visibility for file (1-{len(file_data)}), 'a' for all, or q=cancel: ").strip()
|
|
1304
1617
|
if choice.lower() == 'q':
|
|
1305
1618
|
_print_menu()
|
|
1306
1619
|
_print_file_list(file_data, current_file_idx)
|
|
@@ -1350,7 +1663,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1350
1663
|
|
|
1351
1664
|
if key == 'q':
|
|
1352
1665
|
try:
|
|
1353
|
-
confirm =
|
|
1666
|
+
confirm = _safe_input(_colorize_prompt("Quit CPC interactive? Remember to save! Quit now? (y/n): ")).strip().lower()
|
|
1354
1667
|
except Exception:
|
|
1355
1668
|
confirm = 'y'
|
|
1356
1669
|
if confirm == 'y':
|
|
@@ -1361,283 +1674,282 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1361
1674
|
restore_state()
|
|
1362
1675
|
_print_menu(); continue
|
|
1363
1676
|
elif key == 'c':
|
|
1364
|
-
# Colors submenu: ly (left Y series) and ry (right Y efficiency)
|
|
1677
|
+
# Colors submenu: ly (left Y series) and ry (right Y efficiency), with user colors and palettes
|
|
1365
1678
|
try:
|
|
1679
|
+
# Use same palettes as EC interactive
|
|
1680
|
+
palette_opts = ['tab10', 'Set2', 'Dark2', 'viridis', 'plasma']
|
|
1681
|
+
def _palette_color(name, idx=0, total=1, default_val=0.4):
|
|
1682
|
+
import matplotlib.cm as cm
|
|
1683
|
+
import matplotlib.colors as mcolors
|
|
1684
|
+
import numpy as _np
|
|
1685
|
+
# Ensure colormap is registered before use
|
|
1686
|
+
if not ensure_colormap(name):
|
|
1687
|
+
# Fallback to viridis if colormap can't be registered
|
|
1688
|
+
name = 'viridis'
|
|
1689
|
+
ensure_colormap(name)
|
|
1690
|
+
try:
|
|
1691
|
+
cmap = cm.get_cmap(name)
|
|
1692
|
+
except Exception:
|
|
1693
|
+
# Fallback if get_cmap fails
|
|
1694
|
+
ensure_colormap('viridis')
|
|
1695
|
+
cmap = cm.get_cmap('viridis')
|
|
1696
|
+
|
|
1697
|
+
# Special handling for tab10 to match hardcoded colors exactly
|
|
1698
|
+
if name.lower() == 'tab10':
|
|
1699
|
+
default_tab10_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
|
|
1700
|
+
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
|
|
1701
|
+
return default_tab10_colors[idx % len(default_tab10_colors)]
|
|
1702
|
+
|
|
1703
|
+
# For discrete colormaps (Set2, Dark2), access colors directly
|
|
1704
|
+
if hasattr(cmap, 'colors') and cmap.colors is not None:
|
|
1705
|
+
# Discrete colormap: access colors directly by index
|
|
1706
|
+
colors = cmap.colors
|
|
1707
|
+
rgb = colors[idx % len(colors)]
|
|
1708
|
+
if isinstance(rgb, tuple) and len(rgb) >= 3:
|
|
1709
|
+
return mcolors.rgb2hex(rgb[:3])
|
|
1710
|
+
|
|
1711
|
+
# For continuous colormaps (viridis, plasma), sample evenly
|
|
1712
|
+
if total == 1:
|
|
1713
|
+
vals = [0.55]
|
|
1714
|
+
elif total == 2:
|
|
1715
|
+
vals = [0.15, 0.85]
|
|
1716
|
+
else:
|
|
1717
|
+
vals = _np.linspace(0.08, 0.88, total)
|
|
1718
|
+
rgb = cmap(vals[idx % len(vals)])
|
|
1719
|
+
return mcolors.rgb2hex(rgb[:3])
|
|
1720
|
+
def _resolve_color(spec, idx=0, total=1, default_cmap='tab10'):
|
|
1721
|
+
spec = spec.strip()
|
|
1722
|
+
if not spec:
|
|
1723
|
+
return None
|
|
1724
|
+
if spec.lower() == 'r':
|
|
1725
|
+
return _palette_color(default_cmap, idx, total, 0.4)
|
|
1726
|
+
# user colors: u# or plain number referencing saved list
|
|
1727
|
+
uc = None
|
|
1728
|
+
if spec.lower().startswith('u') and len(spec) > 1 and spec[1:].isdigit():
|
|
1729
|
+
uc = resolve_color_token(spec, fig)
|
|
1730
|
+
elif spec.isdigit():
|
|
1731
|
+
# number as palette index if within palette list
|
|
1732
|
+
n = int(spec)
|
|
1733
|
+
if 1 <= n <= len(palette_opts):
|
|
1734
|
+
palette_name = palette_opts[n-1]
|
|
1735
|
+
return _palette_color(palette_name, idx, total, 0.4)
|
|
1736
|
+
if uc:
|
|
1737
|
+
return uc
|
|
1738
|
+
# Check if spec is a palette name (case-insensitive)
|
|
1739
|
+
spec_lower = spec.lower()
|
|
1740
|
+
base = spec.rstrip('_r').rstrip('_R')
|
|
1741
|
+
base_lower = base.lower()
|
|
1742
|
+
# Check against palette_opts (case-insensitive)
|
|
1743
|
+
for pal in palette_opts:
|
|
1744
|
+
if spec_lower == pal.lower() or base_lower == pal.lower() or spec_lower == (pal + '_r').lower():
|
|
1745
|
+
return _palette_color(pal if not spec.endswith('_r') and not spec.endswith('_R') else spec, idx, total, 0.4)
|
|
1746
|
+
# Fall back to resolve_color_token for hex colors, named colors, etc.
|
|
1747
|
+
return resolve_color_token(spec, fig)
|
|
1748
|
+
|
|
1366
1749
|
while True:
|
|
1367
|
-
print("\nColors: ly=capacity curves, ry=efficiency triangles, q=back")
|
|
1368
|
-
sub =
|
|
1750
|
+
print("\nColors: ly=capacity curves, ry=efficiency triangles, u=user colors, q=back")
|
|
1751
|
+
sub = _safe_input("Colors> ").strip().lower()
|
|
1369
1752
|
if not sub:
|
|
1370
1753
|
continue
|
|
1371
1754
|
if sub == 'q':
|
|
1372
1755
|
break
|
|
1756
|
+
if sub == 'u':
|
|
1757
|
+
manage_user_colors(fig); continue
|
|
1373
1758
|
if sub == 'ly':
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
print("
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1759
|
+
push_state("colors-ly")
|
|
1760
|
+
print("\nCurrent capacity curves:")
|
|
1761
|
+
for i, f in enumerate(file_data, 1):
|
|
1762
|
+
cur = _color_of(f['sc_charge'])
|
|
1763
|
+
vis_mark = "●" if f.get('visible', True) else "○"
|
|
1764
|
+
print(f" {i}. {vis_mark} {f['filename']} {color_block(cur)} {cur}")
|
|
1765
|
+
uc = get_user_color_list(fig)
|
|
1766
|
+
if uc:
|
|
1767
|
+
print("\nSaved colors (refer as number or u#):")
|
|
1768
|
+
for i, c in enumerate(uc, 1):
|
|
1769
|
+
print(f" {i}: {color_block(c)} {c}")
|
|
1770
|
+
print("\nPalettes:")
|
|
1771
|
+
for idx, name in enumerate(palette_opts, 1):
|
|
1772
|
+
bar = palette_preview(name)
|
|
1773
|
+
print(f" {idx}. {name}")
|
|
1774
|
+
if bar:
|
|
1775
|
+
print(f" {bar}")
|
|
1776
|
+
color_input = _safe_input("Enter file+color pairs (e.g., 1:2 2:3 or 1 2 2 3) or palette/number for all, q=cancel: ").strip()
|
|
1777
|
+
if not color_input or color_input.lower() == 'q':
|
|
1778
|
+
continue
|
|
1779
|
+
tokens = color_input.split()
|
|
1780
|
+
if len(tokens) == 1:
|
|
1781
|
+
# Single token: apply palette to all files
|
|
1782
|
+
spec = tokens[0]
|
|
1783
|
+
for i, f in enumerate(file_data):
|
|
1784
|
+
charge_col = _resolve_color(spec, i, len(file_data), default_cmap='tab10')
|
|
1785
|
+
if not charge_col:
|
|
1395
1786
|
continue
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1787
|
+
discharge_col = _generate_similar_color(charge_col)
|
|
1788
|
+
try:
|
|
1789
|
+
f['sc_charge'].set_color(charge_col)
|
|
1790
|
+
f['sc_discharge'].set_color(discharge_col)
|
|
1791
|
+
f['color'] = charge_col
|
|
1792
|
+
# Force update of facecolors for scatter plots
|
|
1793
|
+
if hasattr(f['sc_charge'], 'set_facecolors'):
|
|
1794
|
+
from matplotlib.colors import to_rgba
|
|
1795
|
+
rgba = to_rgba(charge_col)
|
|
1796
|
+
f['sc_charge'].set_facecolors(rgba)
|
|
1797
|
+
if hasattr(f['sc_discharge'], 'set_facecolors'):
|
|
1798
|
+
from matplotlib.colors import to_rgba
|
|
1799
|
+
rgba = to_rgba(discharge_col)
|
|
1800
|
+
f['sc_discharge'].set_facecolors(rgba)
|
|
1801
|
+
except Exception as e:
|
|
1802
|
+
print(f"Error setting color: {e}")
|
|
1803
|
+
pass
|
|
1804
|
+
else:
|
|
1805
|
+
# Multiple tokens: parse file:color pairs
|
|
1806
|
+
def _apply_manual_entries(tokens):
|
|
1807
|
+
idx_color_pairs = []
|
|
1808
|
+
i = 0
|
|
1809
|
+
while i < len(tokens):
|
|
1810
|
+
tok = tokens[i]
|
|
1811
|
+
if ':' in tok:
|
|
1812
|
+
idx_str, color = tok.split(':', 1)
|
|
1415
1813
|
else:
|
|
1416
|
-
|
|
1814
|
+
if i + 1 >= len(tokens):
|
|
1815
|
+
print(f"Skip incomplete entry: {tok}")
|
|
1816
|
+
break
|
|
1817
|
+
idx_str = tok
|
|
1818
|
+
color = tokens[i + 1]
|
|
1819
|
+
i += 1
|
|
1820
|
+
idx_color_pairs.append((idx_str, color))
|
|
1821
|
+
i += 1
|
|
1822
|
+
for idx_str, color in idx_color_pairs:
|
|
1823
|
+
try:
|
|
1824
|
+
file_idx = int(idx_str) - 1
|
|
1825
|
+
except ValueError:
|
|
1826
|
+
print(f"Bad index: {idx_str}")
|
|
1827
|
+
continue
|
|
1828
|
+
if not (0 <= file_idx < len(file_data)):
|
|
1829
|
+
print(f"Index out of range: {idx_str}")
|
|
1830
|
+
continue
|
|
1831
|
+
resolved = resolve_color_token(color, fig)
|
|
1832
|
+
charge_col = resolved if resolved else color
|
|
1833
|
+
if not charge_col:
|
|
1834
|
+
continue
|
|
1417
1835
|
discharge_col = _generate_similar_color(charge_col)
|
|
1418
1836
|
try:
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1837
|
+
file_data[file_idx]['sc_charge'].set_color(charge_col)
|
|
1838
|
+
file_data[file_idx]['sc_discharge'].set_color(discharge_col)
|
|
1839
|
+
file_data[file_idx]['color'] = charge_col
|
|
1840
|
+
# Force update of facecolors for scatter plots
|
|
1841
|
+
if hasattr(file_data[file_idx]['sc_charge'], 'set_facecolors'):
|
|
1842
|
+
from matplotlib.colors import to_rgba
|
|
1843
|
+
rgba = to_rgba(charge_col)
|
|
1844
|
+
file_data[file_idx]['sc_charge'].set_facecolors(rgba)
|
|
1845
|
+
if hasattr(file_data[file_idx]['sc_discharge'], 'set_facecolors'):
|
|
1846
|
+
from matplotlib.colors import to_rgba
|
|
1847
|
+
rgba = to_rgba(discharge_col)
|
|
1848
|
+
file_data[file_idx]['sc_discharge'].set_facecolors(rgba)
|
|
1422
1849
|
except Exception:
|
|
1423
1850
|
pass
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
try:
|
|
1427
|
-
idx = int(choice) - 1
|
|
1428
|
-
if 0 <= idx < len(file_data):
|
|
1429
|
-
print("\nCharge color palettes (discharge will be auto-generated):")
|
|
1430
|
-
print(" 1. Reds: #d62728, #c62828, #b71c1c, #8b0000, #a30000")
|
|
1431
|
-
print(" 2. Oranges: #ff7f0e, #ff6f00, #ff5722, #f4511e, #e64a19")
|
|
1432
|
-
print(" 3. Pinks/Magentas: #e377c2, #d81b60, #c2185b, #ad1457, #880e4f")
|
|
1433
|
-
print(" 4. Purples: #9c27b0, #8e24aa, #7b1fa2, #6a1b9a, #4a148c")
|
|
1434
|
-
print(" 5. Deep oranges/reds: #d84315, #bf360c, #c2185b, #d32f2f, #c62828")
|
|
1435
|
-
spec = input("Enter color (name/hex), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1436
|
-
if not spec or spec.lower() == 'q':
|
|
1437
|
-
continue
|
|
1438
|
-
if spec.lower() == 'r':
|
|
1439
|
-
# Use Viridis colormap
|
|
1440
|
-
import matplotlib.cm as cm
|
|
1441
|
-
import matplotlib.colors as mcolors
|
|
1442
|
-
viridis = cm.get_cmap('viridis', 10)
|
|
1443
|
-
charge_col = mcolors.rgb2hex(viridis(_random.random())[:3])
|
|
1444
|
-
elif spec in ['1', '2', '3', '4', '5']:
|
|
1445
|
-
# Use selected palette
|
|
1446
|
-
charge_palettes = [
|
|
1447
|
-
['#d62728', '#c62828', '#b71c1c', '#8b0000', '#a30000'],
|
|
1448
|
-
['#ff7f0e', '#ff6f00', '#ff5722', '#f4511e', '#e64a19'],
|
|
1449
|
-
['#e377c2', '#d81b60', '#c2185b', '#ad1457', '#880e4f'],
|
|
1450
|
-
['#9c27b0', '#8e24aa', '#7b1fa2', '#6a1b9a', '#4a148c'],
|
|
1451
|
-
['#d84315', '#bf360c', '#c2185b', '#d32f2f', '#c62828']
|
|
1452
|
-
]
|
|
1453
|
-
palette = charge_palettes[int(spec) - 1]
|
|
1454
|
-
charge_col = palette[0] # Use first color from palette for single file
|
|
1455
|
-
else:
|
|
1456
|
-
charge_col = spec
|
|
1457
|
-
discharge_col = _generate_similar_color(charge_col)
|
|
1458
|
-
try:
|
|
1459
|
-
file_data[idx]['sc_charge'].set_color(charge_col)
|
|
1460
|
-
file_data[idx]['sc_discharge'].set_color(discharge_col)
|
|
1461
|
-
file_data[idx]['color'] = charge_col
|
|
1462
|
-
except Exception:
|
|
1463
|
-
pass
|
|
1464
|
-
else:
|
|
1465
|
-
print("Invalid file number.")
|
|
1466
|
-
except ValueError:
|
|
1467
|
-
print("Invalid input.")
|
|
1468
|
-
else:
|
|
1469
|
-
# Single file mode
|
|
1470
|
-
push_state("colors-ly")
|
|
1471
|
-
print("\nCharge color palettes (discharge will be auto-generated):")
|
|
1472
|
-
print(" 1. Reds: #d62728, #c62828, #b71c1c, #8b0000, #a30000")
|
|
1473
|
-
print(" 2. Oranges: #ff7f0e, #ff6f00, #ff5722, #f4511e, #e64a19")
|
|
1474
|
-
print(" 3. Pinks/Magentas: #e377c2, #d81b60, #c2185b, #ad1457, #880e4f")
|
|
1475
|
-
print(" 4. Purples: #9c27b0, #8e24aa, #7b1fa2, #6a1b9a, #4a148c")
|
|
1476
|
-
print(" 5. Deep oranges/reds: #d84315, #bf360c, #c2185b, #d32f2f, #c62828")
|
|
1477
|
-
spec = input("Enter color (name/hex), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1478
|
-
if not spec or spec.lower() == 'q':
|
|
1479
|
-
continue
|
|
1480
|
-
if spec.strip().lower() == 'r':
|
|
1481
|
-
# Use Viridis colormap
|
|
1482
|
-
import matplotlib.cm as cm
|
|
1483
|
-
import matplotlib.colors as mcolors
|
|
1484
|
-
viridis = cm.get_cmap('viridis', 10)
|
|
1485
|
-
charge_col = mcolors.rgb2hex(viridis(_random.random())[:3])
|
|
1486
|
-
elif spec in ['1', '2', '3', '4', '5']:
|
|
1487
|
-
# Use selected palette
|
|
1488
|
-
charge_palettes = [
|
|
1489
|
-
['#d62728', '#c62828', '#b71c1c', '#8b0000', '#a30000'],
|
|
1490
|
-
['#ff7f0e', '#ff6f00', '#ff5722', '#f4511e', '#e64a19'],
|
|
1491
|
-
['#e377c2', '#d81b60', '#c2185b', '#ad1457', '#880e4f'],
|
|
1492
|
-
['#9c27b0', '#8e24aa', '#7b1fa2', '#6a1b9a', '#4a148c'],
|
|
1493
|
-
['#d84315', '#bf360c', '#c2185b', '#d32f2f', '#c62828']
|
|
1494
|
-
]
|
|
1495
|
-
palette = charge_palettes[int(spec) - 1]
|
|
1496
|
-
charge_col = palette[0] # Use first color from palette
|
|
1497
|
-
else:
|
|
1498
|
-
charge_col = spec
|
|
1499
|
-
discharge_col = _generate_similar_color(charge_col)
|
|
1851
|
+
_apply_manual_entries(tokens)
|
|
1852
|
+
if not is_multi_file and getattr(fig, '_cpc_spine_auto', False):
|
|
1500
1853
|
try:
|
|
1501
|
-
sc_charge
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
if not is_multi_file and getattr(fig, '_cpc_spine_auto', False):
|
|
1505
|
-
_set_spine_color('left', charge_col)
|
|
1854
|
+
cur_col = _color_of(sc_charge)
|
|
1855
|
+
if cur_col:
|
|
1856
|
+
_set_spine_color('left', cur_col)
|
|
1506
1857
|
except Exception:
|
|
1507
1858
|
pass
|
|
1508
1859
|
try:
|
|
1509
|
-
_rebuild_legend(ax, ax2, file_data)
|
|
1510
|
-
fig.canvas.draw_idle()
|
|
1860
|
+
_rebuild_legend(ax, ax2, file_data); fig.canvas.draw_idle()
|
|
1511
1861
|
except Exception:
|
|
1512
1862
|
pass
|
|
1513
1863
|
elif sub == 'ry':
|
|
1514
1864
|
push_state("colors-ry")
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
print("
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1865
|
+
print("\nCurrent efficiency curves:")
|
|
1866
|
+
for i, f in enumerate(file_data, 1):
|
|
1867
|
+
cur = _color_of(f['sc_eff'])
|
|
1868
|
+
vis_mark = "●" if f.get('visible', True) else "○"
|
|
1869
|
+
print(f" {i}. {vis_mark} {f['filename']} {color_block(cur)} {cur}")
|
|
1870
|
+
uc = get_user_color_list(fig)
|
|
1871
|
+
if uc:
|
|
1872
|
+
print("\nSaved colors (refer as number or u#):")
|
|
1873
|
+
for i, c in enumerate(uc, 1):
|
|
1874
|
+
print(f" {i}: {color_block(c)} {c}")
|
|
1875
|
+
print("\nPalettes:")
|
|
1876
|
+
for idx, name in enumerate(palette_opts, 1):
|
|
1877
|
+
bar = palette_preview(name)
|
|
1878
|
+
print(f" {idx}. {name}")
|
|
1879
|
+
if bar:
|
|
1880
|
+
print(f" {bar}")
|
|
1881
|
+
color_input = _safe_input("Enter file+color pairs (e.g., 1:2 2:3 or 1 2 2 3) or palette/number for all, q=cancel: ").strip()
|
|
1882
|
+
if not color_input or color_input.lower() == 'q':
|
|
1883
|
+
continue
|
|
1884
|
+
tokens = color_input.split()
|
|
1885
|
+
if len(tokens) == 1:
|
|
1886
|
+
# Single token: apply palette to all files
|
|
1887
|
+
spec = tokens[0]
|
|
1888
|
+
for i, f in enumerate(file_data):
|
|
1889
|
+
col = _resolve_color(spec, i, len(file_data), default_cmap='viridis')
|
|
1890
|
+
if not col:
|
|
1534
1891
|
continue
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1892
|
+
try:
|
|
1893
|
+
f['sc_eff'].set_color(col)
|
|
1894
|
+
f['eff_color'] = col
|
|
1895
|
+
# Force update of facecolors for scatter plots
|
|
1896
|
+
if hasattr(f['sc_eff'], 'set_facecolors'):
|
|
1897
|
+
from matplotlib.colors import to_rgba
|
|
1898
|
+
rgba = to_rgba(col)
|
|
1899
|
+
f['sc_eff'].set_facecolors(rgba)
|
|
1900
|
+
except Exception:
|
|
1901
|
+
pass
|
|
1902
|
+
else:
|
|
1903
|
+
# Multiple tokens: parse file:color pairs
|
|
1904
|
+
def _apply_manual_entries_eff(tokens):
|
|
1905
|
+
idx_color_pairs = []
|
|
1906
|
+
i = 0
|
|
1907
|
+
while i < len(tokens):
|
|
1908
|
+
tok = tokens[i]
|
|
1909
|
+
if ':' in tok:
|
|
1910
|
+
idx_str, color = tok.split(':', 1)
|
|
1553
1911
|
else:
|
|
1554
|
-
|
|
1912
|
+
if i + 1 >= len(tokens):
|
|
1913
|
+
print(f"Skip incomplete entry: {tok}")
|
|
1914
|
+
break
|
|
1915
|
+
idx_str = tok
|
|
1916
|
+
color = tokens[i + 1]
|
|
1917
|
+
i += 1
|
|
1918
|
+
idx_color_pairs.append((idx_str, color))
|
|
1919
|
+
i += 1
|
|
1920
|
+
for idx_str, color in idx_color_pairs:
|
|
1555
1921
|
try:
|
|
1556
|
-
|
|
1557
|
-
|
|
1922
|
+
file_idx = int(idx_str) - 1
|
|
1923
|
+
except ValueError:
|
|
1924
|
+
print(f"Bad index: {idx_str}")
|
|
1925
|
+
continue
|
|
1926
|
+
if not (0 <= file_idx < len(file_data)):
|
|
1927
|
+
print(f"Index out of range: {idx_str}")
|
|
1928
|
+
continue
|
|
1929
|
+
resolved = resolve_color_token(color, fig)
|
|
1930
|
+
col = resolved if resolved else color
|
|
1931
|
+
if not col:
|
|
1932
|
+
continue
|
|
1933
|
+
try:
|
|
1934
|
+
file_data[file_idx]['sc_eff'].set_color(col)
|
|
1935
|
+
file_data[file_idx]['eff_color'] = col
|
|
1936
|
+
# Force update of facecolors for scatter plots
|
|
1937
|
+
if hasattr(file_data[file_idx]['sc_eff'], 'set_facecolors'):
|
|
1938
|
+
from matplotlib.colors import to_rgba
|
|
1939
|
+
rgba = to_rgba(col)
|
|
1940
|
+
file_data[file_idx]['sc_eff'].set_facecolors(rgba)
|
|
1558
1941
|
except Exception:
|
|
1559
1942
|
pass
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
idx = int(choice) - 1
|
|
1563
|
-
if 0 <= idx < len(file_data):
|
|
1564
|
-
print("\nEfficiency color palettes:")
|
|
1565
|
-
print(" 1. Blues: #1f77b4, #1976d2, #1565c0, #0d47a1, #01579b")
|
|
1566
|
-
print(" 2. Cyans/Teals: #17becf, #00acc1, #0097a7, #00838f, #006064")
|
|
1567
|
-
print(" 3. Purples/Indigos: #9467bd, #5e35b1, #512da8, #4527a0, #311b92")
|
|
1568
|
-
print(" 4. Deep blues: #2196f3, #1e88e5, #1976d2, #1565c0, #0d47a1")
|
|
1569
|
-
print(" 5. Dark cyans/purples: #0097a7, #00838f, #006064, #5e35b1, #4527a0")
|
|
1570
|
-
val = input("Enter color (hex/name), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1571
|
-
if not val or val.lower() == 'q':
|
|
1572
|
-
continue
|
|
1573
|
-
if val.lower() == 'r':
|
|
1574
|
-
# Use Plasma colormap
|
|
1575
|
-
import matplotlib.cm as cm
|
|
1576
|
-
import matplotlib.colors as mcolors
|
|
1577
|
-
plasma = cm.get_cmap('plasma', 10)
|
|
1578
|
-
col = mcolors.rgb2hex(plasma(_random.random())[:3])
|
|
1579
|
-
elif val in ['1', '2', '3', '4', '5']:
|
|
1580
|
-
# Use selected palette
|
|
1581
|
-
efficiency_palettes = [
|
|
1582
|
-
['#1f77b4', '#1976d2', '#1565c0', '#0d47a1', '#01579b'],
|
|
1583
|
-
['#17becf', '#00acc1', '#0097a7', '#00838f', '#006064'],
|
|
1584
|
-
['#9467bd', '#5e35b1', '#512da8', '#4527a0', '#311b92'],
|
|
1585
|
-
['#2196f3', '#1e88e5', '#1976d2', '#1565c0', '#0d47a1'],
|
|
1586
|
-
['#0097a7', '#00838f', '#006064', '#5e35b1', '#4527a0']
|
|
1587
|
-
]
|
|
1588
|
-
palette = efficiency_palettes[int(val) - 1]
|
|
1589
|
-
col = palette[0] # Use first color from palette for single file
|
|
1590
|
-
else:
|
|
1591
|
-
col = val
|
|
1592
|
-
try:
|
|
1593
|
-
file_data[idx]['sc_eff'].set_color(col)
|
|
1594
|
-
file_data[idx]['eff_color'] = col # Store efficiency color
|
|
1595
|
-
except Exception:
|
|
1596
|
-
pass
|
|
1597
|
-
else:
|
|
1598
|
-
print("Invalid file number.")
|
|
1599
|
-
except ValueError:
|
|
1600
|
-
print("Invalid input.")
|
|
1601
|
-
else:
|
|
1602
|
-
# Single file mode
|
|
1603
|
-
print("\nEfficiency color palettes:")
|
|
1604
|
-
print(" 1. Blues: #1f77b4, #1976d2, #1565c0, #0d47a1, #01579b")
|
|
1605
|
-
print(" 2. Cyans/Teals: #17becf, #00acc1, #0097a7, #00838f, #006064")
|
|
1606
|
-
print(" 3. Purples/Indigos: #9467bd, #5e35b1, #512da8, #4527a0, #311b92")
|
|
1607
|
-
print(" 4. Deep blues: #2196f3, #1e88e5, #1976d2, #1565c0, #0d47a1")
|
|
1608
|
-
print(" 5. Dark cyans/purples: #0097a7, #00838f, #006064, #5e35b1, #4527a0")
|
|
1609
|
-
val = input("Enter color (hex/name), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1610
|
-
if not val or val.lower() == 'q':
|
|
1611
|
-
continue
|
|
1612
|
-
if val.lower() == 'r':
|
|
1613
|
-
# Use Plasma colormap
|
|
1614
|
-
import matplotlib.cm as cm
|
|
1615
|
-
import matplotlib.colors as mcolors
|
|
1616
|
-
plasma = cm.get_cmap('plasma', 10)
|
|
1617
|
-
col = mcolors.rgb2hex(plasma(_random.random())[:3])
|
|
1618
|
-
elif val in ['1', '2', '3', '4', '5']:
|
|
1619
|
-
# Use selected palette
|
|
1620
|
-
efficiency_palettes = [
|
|
1621
|
-
['#1f77b4', '#1976d2', '#1565c0', '#0d47a1', '#01579b'],
|
|
1622
|
-
['#17becf', '#00acc1', '#0097a7', '#00838f', '#006064'],
|
|
1623
|
-
['#9467bd', '#5e35b1', '#512da8', '#4527a0', '#311b92'],
|
|
1624
|
-
['#2196f3', '#1e88e5', '#1976d2', '#1565c0', '#0d47a1'],
|
|
1625
|
-
['#0097a7', '#00838f', '#006064', '#5e35b1', '#4527a0']
|
|
1626
|
-
]
|
|
1627
|
-
palette = efficiency_palettes[int(val) - 1]
|
|
1628
|
-
col = palette[0] # Use first color from palette
|
|
1629
|
-
else:
|
|
1630
|
-
col = val
|
|
1943
|
+
_apply_manual_entries_eff(tokens)
|
|
1944
|
+
if not is_multi_file and getattr(fig, '_cpc_spine_auto', False):
|
|
1631
1945
|
try:
|
|
1632
|
-
sc_eff
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
_set_spine_color('right', col)
|
|
1946
|
+
cur_col = _color_of(sc_eff)
|
|
1947
|
+
if cur_col:
|
|
1948
|
+
_set_spine_color('right', cur_col)
|
|
1636
1949
|
except Exception:
|
|
1637
1950
|
pass
|
|
1638
1951
|
try:
|
|
1639
|
-
_rebuild_legend(ax, ax2, file_data)
|
|
1640
|
-
fig.canvas.draw_idle()
|
|
1952
|
+
_rebuild_legend(ax, ax2, file_data); fig.canvas.draw_idle()
|
|
1641
1953
|
except Exception:
|
|
1642
1954
|
pass
|
|
1643
1955
|
else:
|
|
@@ -1662,7 +1974,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1662
1974
|
auto_status = "ON" if auto_enabled else "OFF"
|
|
1663
1975
|
print(_colorize_inline_commands(f" a : auto (apply capacity curve color to left y-axis, efficiency to right y-axis) [{auto_status}]"))
|
|
1664
1976
|
print("q: back to main menu")
|
|
1665
|
-
line =
|
|
1977
|
+
line = _safe_input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
|
|
1666
1978
|
if not line or line.lower() == 'q':
|
|
1667
1979
|
break
|
|
1668
1980
|
# Handle auto toggle when only one file is loaded
|
|
@@ -1734,9 +2046,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1734
2046
|
|
|
1735
2047
|
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
1736
2048
|
if last_figure_path:
|
|
1737
|
-
fname =
|
|
2049
|
+
fname = _safe_input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
1738
2050
|
else:
|
|
1739
|
-
fname =
|
|
2051
|
+
fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
1740
2052
|
if not fname or fname.lower() == 'q':
|
|
1741
2053
|
_print_menu(); continue
|
|
1742
2054
|
|
|
@@ -1748,7 +2060,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1748
2060
|
if not os.path.exists(last_figure_path):
|
|
1749
2061
|
print(f"Previous export file not found: {last_figure_path}")
|
|
1750
2062
|
_print_menu(); continue
|
|
1751
|
-
yn =
|
|
2063
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
1752
2064
|
if yn != 'y':
|
|
1753
2065
|
_print_menu(); continue
|
|
1754
2066
|
target = last_figure_path
|
|
@@ -1757,7 +2069,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1757
2069
|
idx = int(fname)
|
|
1758
2070
|
if 1 <= idx <= len(files):
|
|
1759
2071
|
name = files[idx-1]
|
|
1760
|
-
yn =
|
|
2072
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
1761
2073
|
if yn != 'y':
|
|
1762
2074
|
_print_menu(); continue
|
|
1763
2075
|
target = file_list[idx-1][1] # Full path from list
|
|
@@ -1774,10 +2086,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1774
2086
|
else:
|
|
1775
2087
|
target = get_organized_path(fname, 'figure', base_path=base_path)
|
|
1776
2088
|
if os.path.exists(target):
|
|
1777
|
-
yn =
|
|
2089
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
1778
2090
|
if yn != 'y':
|
|
1779
2091
|
_print_menu(); continue
|
|
1780
2092
|
if target:
|
|
2093
|
+
# Save current legend position before export (savefig can change layout)
|
|
2094
|
+
saved_legend_pos = None
|
|
2095
|
+
try:
|
|
2096
|
+
saved_legend_pos = getattr(fig, '_cpc_legend_xy_in', None)
|
|
2097
|
+
except Exception:
|
|
2098
|
+
pass
|
|
2099
|
+
|
|
1781
2100
|
# Remove numbering from legend labels before export
|
|
1782
2101
|
original_labels = {}
|
|
1783
2102
|
if is_multi_file:
|
|
@@ -1800,18 +2119,88 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1800
2119
|
pass
|
|
1801
2120
|
|
|
1802
2121
|
# Export the figure
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
# Restore original labels
|
|
1808
|
-
if is_multi_file and original_labels:
|
|
2122
|
+
_, _ext = os.path.splitext(target)
|
|
2123
|
+
if _ext.lower() == '.svg':
|
|
2124
|
+
# Temporarily force transparent patches so SVG background stays transparent
|
|
1809
2125
|
try:
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2126
|
+
_fig_fc = fig.get_facecolor()
|
|
2127
|
+
except Exception:
|
|
2128
|
+
_fig_fc = None
|
|
2129
|
+
try:
|
|
2130
|
+
_ax_fc = ax.get_facecolor()
|
|
2131
|
+
except Exception:
|
|
2132
|
+
_ax_fc = None
|
|
2133
|
+
try:
|
|
2134
|
+
_ax2_fc = ax2.get_facecolor()
|
|
2135
|
+
except Exception:
|
|
2136
|
+
_ax2_fc = None
|
|
2137
|
+
try:
|
|
2138
|
+
if getattr(fig, 'patch', None) is not None:
|
|
2139
|
+
fig.patch.set_alpha(0.0); fig.patch.set_facecolor('none')
|
|
2140
|
+
if getattr(ax, 'patch', None) is not None:
|
|
2141
|
+
ax.patch.set_alpha(0.0); ax.patch.set_facecolor('none')
|
|
2142
|
+
if getattr(ax2, 'patch', None) is not None:
|
|
2143
|
+
ax2.patch.set_alpha(0.0); ax2.patch.set_facecolor('none')
|
|
1813
2144
|
except Exception:
|
|
1814
2145
|
pass
|
|
2146
|
+
try:
|
|
2147
|
+
fig.savefig(target, bbox_inches='tight', transparent=True, facecolor='none', edgecolor='none')
|
|
2148
|
+
finally:
|
|
2149
|
+
try:
|
|
2150
|
+
if _fig_fc is not None and getattr(fig, 'patch', None) is not None:
|
|
2151
|
+
fig.patch.set_alpha(1.0); fig.patch.set_facecolor(_fig_fc)
|
|
2152
|
+
except Exception:
|
|
2153
|
+
pass
|
|
2154
|
+
try:
|
|
2155
|
+
if _ax_fc is not None and getattr(ax, 'patch', None) is not None:
|
|
2156
|
+
ax.patch.set_alpha(1.0); ax.patch.set_facecolor(_ax_fc)
|
|
2157
|
+
except Exception:
|
|
2158
|
+
pass
|
|
2159
|
+
try:
|
|
2160
|
+
if _ax2_fc is not None and getattr(ax2, 'patch', None) is not None:
|
|
2161
|
+
ax2.patch.set_alpha(1.0); ax2.patch.set_facecolor(_ax2_fc)
|
|
2162
|
+
except Exception:
|
|
2163
|
+
pass
|
|
2164
|
+
print(f"Exported figure to {target}")
|
|
2165
|
+
fig._last_figure_export_path = target
|
|
2166
|
+
|
|
2167
|
+
# Restore original labels and legend position
|
|
2168
|
+
if is_multi_file and original_labels:
|
|
2169
|
+
try:
|
|
2170
|
+
for artist, label in original_labels.items():
|
|
2171
|
+
artist.set_label(label)
|
|
2172
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2173
|
+
except Exception:
|
|
2174
|
+
pass
|
|
2175
|
+
# Restore legend position after savefig (which may have changed layout)
|
|
2176
|
+
if saved_legend_pos is not None:
|
|
2177
|
+
try:
|
|
2178
|
+
fig._cpc_legend_xy_in = saved_legend_pos
|
|
2179
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2180
|
+
fig.canvas.draw_idle()
|
|
2181
|
+
except Exception:
|
|
2182
|
+
pass
|
|
2183
|
+
else:
|
|
2184
|
+
fig.savefig(target, bbox_inches='tight')
|
|
2185
|
+
print(f"Exported figure to {target}")
|
|
2186
|
+
fig._last_figure_export_path = target
|
|
2187
|
+
|
|
2188
|
+
# Restore original labels and legend position
|
|
2189
|
+
if is_multi_file and original_labels:
|
|
2190
|
+
try:
|
|
2191
|
+
for artist, label in original_labels.items():
|
|
2192
|
+
artist.set_label(label)
|
|
2193
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2194
|
+
except Exception:
|
|
2195
|
+
pass
|
|
2196
|
+
# Restore legend position after savefig (which may have changed layout)
|
|
2197
|
+
if saved_legend_pos is not None:
|
|
2198
|
+
try:
|
|
2199
|
+
fig._cpc_legend_xy_in = saved_legend_pos
|
|
2200
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2201
|
+
fig.canvas.draw_idle()
|
|
2202
|
+
except Exception:
|
|
2203
|
+
pass
|
|
1815
2204
|
except Exception as e:
|
|
1816
2205
|
print(f"Export failed: {e}")
|
|
1817
2206
|
_print_menu(); continue
|
|
@@ -1819,6 +2208,58 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1819
2208
|
# Save CPC session (.pkl) with all data and styles
|
|
1820
2209
|
try:
|
|
1821
2210
|
from .session import dump_cpc_session
|
|
2211
|
+
# Sync current tick/title visibility (including minors) into stored WASD state before save
|
|
2212
|
+
try:
|
|
2213
|
+
wasd = getattr(fig, '_cpc_wasd_state', {})
|
|
2214
|
+
if not isinstance(wasd, dict):
|
|
2215
|
+
wasd = {}
|
|
2216
|
+
# bottom
|
|
2217
|
+
w = wasd.setdefault('bottom', {})
|
|
2218
|
+
w['ticks'] = bool(tick_state.get('b_ticks', tick_state.get('bx', True)))
|
|
2219
|
+
w['labels'] = bool(tick_state.get('b_labels', tick_state.get('bx', True)))
|
|
2220
|
+
w['minor'] = bool(tick_state.get('mbx', False))
|
|
2221
|
+
w['title'] = bool(ax.xaxis.label.get_visible())
|
|
2222
|
+
try:
|
|
2223
|
+
sp = ax.spines.get('bottom')
|
|
2224
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', True)
|
|
2225
|
+
except Exception:
|
|
2226
|
+
pass
|
|
2227
|
+
# top
|
|
2228
|
+
w = wasd.setdefault('top', {})
|
|
2229
|
+
w['ticks'] = bool(tick_state.get('t_ticks', tick_state.get('tx', False)))
|
|
2230
|
+
w['labels'] = bool(tick_state.get('t_labels', tick_state.get('tx', False)))
|
|
2231
|
+
w['minor'] = bool(tick_state.get('mtx', False))
|
|
2232
|
+
w['title'] = bool(getattr(ax, '_top_xlabel_on', False))
|
|
2233
|
+
try:
|
|
2234
|
+
sp = ax.spines.get('top')
|
|
2235
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', False)
|
|
2236
|
+
except Exception:
|
|
2237
|
+
pass
|
|
2238
|
+
# left
|
|
2239
|
+
w = wasd.setdefault('left', {})
|
|
2240
|
+
w['ticks'] = bool(tick_state.get('l_ticks', tick_state.get('ly', True)))
|
|
2241
|
+
w['labels'] = bool(tick_state.get('l_labels', tick_state.get('ly', True)))
|
|
2242
|
+
w['minor'] = bool(tick_state.get('mly', False))
|
|
2243
|
+
w['title'] = bool(ax.yaxis.label.get_visible())
|
|
2244
|
+
try:
|
|
2245
|
+
sp = ax.spines.get('left')
|
|
2246
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', True)
|
|
2247
|
+
except Exception:
|
|
2248
|
+
pass
|
|
2249
|
+
# right
|
|
2250
|
+
w = wasd.setdefault('right', {})
|
|
2251
|
+
w['ticks'] = bool(tick_state.get('r_ticks', tick_state.get('ry', True)))
|
|
2252
|
+
w['labels'] = bool(tick_state.get('r_labels', tick_state.get('ry', True)))
|
|
2253
|
+
w['minor'] = bool(tick_state.get('mry', False))
|
|
2254
|
+
w['title'] = bool(ax2.yaxis.label.get_visible() if ax2 is not None else False)
|
|
2255
|
+
try:
|
|
2256
|
+
sp = ax2.spines.get('right') if ax2 is not None else None
|
|
2257
|
+
w['spine'] = bool(sp.get_visible()) if sp else w.get('spine', True)
|
|
2258
|
+
except Exception:
|
|
2259
|
+
pass
|
|
2260
|
+
setattr(fig, '_cpc_wasd_state', wasd)
|
|
2261
|
+
except Exception:
|
|
2262
|
+
pass
|
|
1822
2263
|
folder = choose_save_path(file_paths, purpose="CPC session save")
|
|
1823
2264
|
if not folder:
|
|
1824
2265
|
_print_menu(); continue
|
|
@@ -1841,7 +2282,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1841
2282
|
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
1842
2283
|
else:
|
|
1843
2284
|
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
1844
|
-
choice =
|
|
2285
|
+
choice = _safe_input(prompt).strip()
|
|
1845
2286
|
if not choice or choice.lower() == 'q':
|
|
1846
2287
|
_print_menu(); continue
|
|
1847
2288
|
if choice.lower() == 'o':
|
|
@@ -1852,7 +2293,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1852
2293
|
if not os.path.exists(last_session_path):
|
|
1853
2294
|
print(f"Previous save file not found: {last_session_path}")
|
|
1854
2295
|
_print_menu(); continue
|
|
1855
|
-
yn =
|
|
2296
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
1856
2297
|
if yn != 'y':
|
|
1857
2298
|
_print_menu(); continue
|
|
1858
2299
|
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)
|
|
@@ -1862,7 +2303,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1862
2303
|
idx = int(choice)
|
|
1863
2304
|
if 1 <= idx <= len(files):
|
|
1864
2305
|
name = files[idx-1]
|
|
1865
|
-
yn =
|
|
2306
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
1866
2307
|
if yn != 'y':
|
|
1867
2308
|
_print_menu(); continue
|
|
1868
2309
|
target = os.path.join(folder, name)
|
|
@@ -1879,7 +2320,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1879
2320
|
name = name + '.pkl'
|
|
1880
2321
|
target = name if os.path.isabs(name) else os.path.join(folder, name)
|
|
1881
2322
|
if os.path.exists(target):
|
|
1882
|
-
yn =
|
|
2323
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
1883
2324
|
if yn != 'y':
|
|
1884
2325
|
_print_menu(); continue
|
|
1885
2326
|
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)
|
|
@@ -2009,9 +2450,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2009
2450
|
|
|
2010
2451
|
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
2011
2452
|
if last_style_path:
|
|
2012
|
-
sub =
|
|
2453
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
2013
2454
|
else:
|
|
2014
|
-
sub =
|
|
2455
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
2015
2456
|
if sub == 'q':
|
|
2016
2457
|
break
|
|
2017
2458
|
if sub == 'r' or sub == '':
|
|
@@ -2024,7 +2465,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2024
2465
|
if not os.path.exists(last_style_path):
|
|
2025
2466
|
print(f"Previous export file not found: {last_style_path}")
|
|
2026
2467
|
continue
|
|
2027
|
-
yn =
|
|
2468
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2028
2469
|
if yn != 'y':
|
|
2029
2470
|
continue
|
|
2030
2471
|
# Rebuild config based on current state
|
|
@@ -2050,7 +2491,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2050
2491
|
print("Export options:")
|
|
2051
2492
|
print(" ps = style only (.bps)")
|
|
2052
2493
|
print(" psg = style + geometry (.bpsg)")
|
|
2053
|
-
exp_choice =
|
|
2494
|
+
exp_choice = _safe_input(_colorize_prompt("Export choice (ps/psg, q=cancel): ")).strip().lower()
|
|
2054
2495
|
if not exp_choice or exp_choice == 'q':
|
|
2055
2496
|
print("Style export canceled.")
|
|
2056
2497
|
continue
|
|
@@ -2103,9 +2544,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2103
2544
|
else:
|
|
2104
2545
|
print(f" {i}: {fname}")
|
|
2105
2546
|
if last_style_path:
|
|
2106
|
-
choice =
|
|
2547
|
+
choice = _safe_input("Enter new filename, number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
2107
2548
|
else:
|
|
2108
|
-
choice =
|
|
2549
|
+
choice = _safe_input("Enter new filename or number to overwrite (q=cancel): ").strip()
|
|
2109
2550
|
if not choice or choice.lower() == 'q':
|
|
2110
2551
|
print("Style export canceled.")
|
|
2111
2552
|
continue
|
|
@@ -2117,7 +2558,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2117
2558
|
if not os.path.exists(last_style_path):
|
|
2118
2559
|
print(f"Previous export file not found: {last_style_path}")
|
|
2119
2560
|
continue
|
|
2120
|
-
yn =
|
|
2561
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2121
2562
|
if yn != 'y':
|
|
2122
2563
|
continue
|
|
2123
2564
|
# Rebuild config based on current state
|
|
@@ -2143,7 +2584,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2143
2584
|
idx = int(choice)
|
|
2144
2585
|
if 1 <= idx <= len(files):
|
|
2145
2586
|
name = files[idx-1]
|
|
2146
|
-
yn =
|
|
2587
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
2147
2588
|
if yn == 'y':
|
|
2148
2589
|
target = file_list[idx-1][1] # Full path from list
|
|
2149
2590
|
else:
|
|
@@ -2160,7 +2601,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2160
2601
|
else:
|
|
2161
2602
|
target = get_organized_path(name, 'style', base_path=save_base)
|
|
2162
2603
|
if os.path.exists(target):
|
|
2163
|
-
yn =
|
|
2604
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2164
2605
|
if yn != 'y':
|
|
2165
2606
|
target = None
|
|
2166
2607
|
if target:
|
|
@@ -2222,11 +2663,28 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2222
2663
|
# Toggle efficiency visibility on the right axis
|
|
2223
2664
|
try:
|
|
2224
2665
|
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
2666
|
|
|
2228
|
-
#
|
|
2229
|
-
|
|
2667
|
+
# Determine current visibility state (check if any efficiency is visible)
|
|
2668
|
+
if is_multi_file:
|
|
2669
|
+
# In multi-file mode, check if any efficiency is visible
|
|
2670
|
+
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'))
|
|
2671
|
+
new_vis = not any_eff_visible
|
|
2672
|
+
else:
|
|
2673
|
+
# Single file mode
|
|
2674
|
+
vis = bool(sc_eff.get_visible()) if hasattr(sc_eff, 'get_visible') else True
|
|
2675
|
+
new_vis = not vis
|
|
2676
|
+
|
|
2677
|
+
# 1. Hide/show efficiency points (all files in multi-file mode)
|
|
2678
|
+
if is_multi_file:
|
|
2679
|
+
for f in file_data:
|
|
2680
|
+
eff_sc = f.get('sc_eff')
|
|
2681
|
+
if eff_sc is not None:
|
|
2682
|
+
try:
|
|
2683
|
+
eff_sc.set_visible(new_vis)
|
|
2684
|
+
except Exception:
|
|
2685
|
+
pass
|
|
2686
|
+
else:
|
|
2687
|
+
sc_eff.set_visible(new_vis)
|
|
2230
2688
|
|
|
2231
2689
|
# 2. Hide/show right y-axis title
|
|
2232
2690
|
try:
|
|
@@ -2242,68 +2700,42 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2242
2700
|
except Exception:
|
|
2243
2701
|
pass
|
|
2244
2702
|
|
|
2245
|
-
#
|
|
2246
|
-
try:
|
|
2247
|
-
h1, l1 = ax.get_legend_handles_labels()
|
|
2248
|
-
except Exception:
|
|
2249
|
-
h1, l1 = [], []
|
|
2703
|
+
# Persist WASD state so save/load and styles honor the toggle
|
|
2250
2704
|
try:
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2705
|
+
wasd = getattr(fig, '_cpc_wasd_state', None)
|
|
2706
|
+
if not isinstance(wasd, dict):
|
|
2707
|
+
wasd = {
|
|
2708
|
+
'top': {'spine': bool(ax.spines.get('top').get_visible()) if ax.spines.get('top') else False,
|
|
2709
|
+
'ticks': bool(tick_state.get('t_ticks', tick_state.get('tx', False))),
|
|
2710
|
+
'minor': bool(tick_state.get('mtx', False)),
|
|
2711
|
+
'labels': bool(tick_state.get('t_labels', tick_state.get('tx', False))),
|
|
2712
|
+
'title': bool(getattr(ax, '_top_xlabel_on', False))},
|
|
2713
|
+
'bottom': {'spine': bool(ax.spines.get('bottom').get_visible()) if ax.spines.get('bottom') else True,
|
|
2714
|
+
'ticks': bool(tick_state.get('b_ticks', tick_state.get('bx', True))),
|
|
2715
|
+
'minor': bool(tick_state.get('mbx', False)),
|
|
2716
|
+
'labels': bool(tick_state.get('b_labels', tick_state.get('bx', True))),
|
|
2717
|
+
'title': bool(ax.xaxis.label.get_visible()) and bool(ax.get_xlabel())},
|
|
2718
|
+
'left': {'spine': bool(ax.spines.get('left').get_visible()) if ax.spines.get('left') else True,
|
|
2719
|
+
'ticks': bool(tick_state.get('l_ticks', tick_state.get('ly', True))),
|
|
2720
|
+
'minor': bool(tick_state.get('mly', False)),
|
|
2721
|
+
'labels': bool(tick_state.get('l_labels', tick_state.get('ly', True))),
|
|
2722
|
+
'title': bool(ax.yaxis.label.get_visible()) and bool(ax.get_ylabel())},
|
|
2723
|
+
'right': {'spine': bool(ax2.spines.get('right').get_visible()) if ax2.spines.get('right') else True,
|
|
2724
|
+
'ticks': bool(tick_state.get('r_ticks', tick_state.get('ry', True))),
|
|
2725
|
+
'minor': bool(tick_state.get('mry', False)),
|
|
2726
|
+
'labels': bool(tick_state.get('r_labels', tick_state.get('ry', True))),
|
|
2727
|
+
'title': bool(ax2.yaxis.label.get_visible()) and bool(ax2.get_ylabel())},
|
|
2728
|
+
}
|
|
2729
|
+
wasd.setdefault('right', {})
|
|
2730
|
+
wasd['right']['ticks'] = bool(new_vis)
|
|
2731
|
+
wasd['right']['labels'] = bool(new_vis)
|
|
2732
|
+
wasd['right']['title'] = bool(new_vis)
|
|
2733
|
+
setattr(fig, '_cpc_wasd_state', wasd)
|
|
2259
2734
|
except Exception:
|
|
2260
2735
|
pass
|
|
2261
2736
|
|
|
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
|
|
2737
|
+
# 4. Rebuild legend to remove/add efficiency entries (preserve position)
|
|
2738
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
2307
2739
|
|
|
2308
2740
|
fig.canvas.draw_idle()
|
|
2309
2741
|
except Exception:
|
|
@@ -2346,7 +2778,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2346
2778
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2347
2779
|
print(f"Legend is {'ON' if vis else 'off'}; position (inches from center): x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2348
2780
|
while True:
|
|
2349
|
-
sub =
|
|
2781
|
+
sub = _safe_input("Legend: t=toggle, p=set position, q=back: ").strip().lower()
|
|
2350
2782
|
if not sub:
|
|
2351
2783
|
continue
|
|
2352
2784
|
if sub == 'q':
|
|
@@ -2359,15 +2791,14 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2359
2791
|
leg.set_visible(False)
|
|
2360
2792
|
else:
|
|
2361
2793
|
# Ensure a legend exists at the stored position
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
if h1 or h2:
|
|
2794
|
+
H, L = _visible_handles_labels(ax, ax2)
|
|
2795
|
+
if H:
|
|
2365
2796
|
offset = _sanitize_legend_offset(getattr(fig, '_cpc_legend_xy_in', None))
|
|
2366
2797
|
if offset is not None:
|
|
2367
2798
|
fig._cpc_legend_xy_in = offset
|
|
2368
2799
|
_apply_legend_position()
|
|
2369
2800
|
else:
|
|
2370
|
-
_legend_no_frame(ax,
|
|
2801
|
+
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2371
2802
|
fig.canvas.draw_idle()
|
|
2372
2803
|
except Exception:
|
|
2373
2804
|
pass
|
|
@@ -2377,7 +2808,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2377
2808
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2378
2809
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2379
2810
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2380
|
-
pos_cmd =
|
|
2811
|
+
pos_cmd = _safe_input("Position: (x y) or x=x only, y=y only, q=back: ").strip().lower()
|
|
2381
2812
|
if not pos_cmd or pos_cmd == 'q':
|
|
2382
2813
|
break
|
|
2383
2814
|
if pos_cmd == 'x':
|
|
@@ -2386,7 +2817,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2386
2817
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2387
2818
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2388
2819
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2389
|
-
val =
|
|
2820
|
+
val = _safe_input(f"Enter new x position (current y: {xy_in[1]:.2f}, q=back): ").strip()
|
|
2390
2821
|
if not val or val.lower() == 'q':
|
|
2391
2822
|
break
|
|
2392
2823
|
try:
|
|
@@ -2409,7 +2840,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2409
2840
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2410
2841
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2411
2842
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2412
|
-
val =
|
|
2843
|
+
val = _safe_input(f"Enter new y position (current x: {xy_in[0]:.2f}, q=back): ").strip()
|
|
2413
2844
|
if not val or val.lower() == 'q':
|
|
2414
2845
|
break
|
|
2415
2846
|
try:
|
|
@@ -2450,11 +2881,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2450
2881
|
pass
|
|
2451
2882
|
_print_menu(); continue
|
|
2452
2883
|
elif key == 'f':
|
|
2453
|
-
sub =
|
|
2884
|
+
sub = _safe_input("Font: f=family, s=size, q=back: ").strip().lower()
|
|
2454
2885
|
if sub == 'q' or not sub:
|
|
2455
2886
|
_print_menu(); continue
|
|
2456
2887
|
if sub == 'f':
|
|
2457
|
-
fam =
|
|
2888
|
+
fam = _safe_input("Enter font family (e.g., Arial, DejaVu Sans): ").strip()
|
|
2458
2889
|
if fam:
|
|
2459
2890
|
try:
|
|
2460
2891
|
push_state("font-family")
|
|
@@ -2510,7 +2941,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2510
2941
|
except Exception:
|
|
2511
2942
|
pass
|
|
2512
2943
|
elif sub == 's':
|
|
2513
|
-
val =
|
|
2944
|
+
val = _safe_input("Enter font size (number): ").strip()
|
|
2514
2945
|
try:
|
|
2515
2946
|
size = float(val)
|
|
2516
2947
|
push_state("font-size")
|
|
@@ -2604,13 +3035,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2604
3035
|
print(f" {_colorize_menu('f : change frame (axes spines) and tick widths')}")
|
|
2605
3036
|
print(f" {_colorize_menu('g : toggle grid lines')}")
|
|
2606
3037
|
print(f" {_colorize_menu('q : return')}")
|
|
2607
|
-
sub =
|
|
3038
|
+
sub = _safe_input(_colorize_prompt("Choose (f/g/q): ")).strip().lower()
|
|
2608
3039
|
if not sub:
|
|
2609
3040
|
continue
|
|
2610
3041
|
if sub == 'q':
|
|
2611
3042
|
break
|
|
2612
3043
|
if sub == 'f':
|
|
2613
|
-
fw_in =
|
|
3044
|
+
fw_in = _safe_input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
|
|
2614
3045
|
if not fw_in or fw_in.lower() == 'q':
|
|
2615
3046
|
print("Canceled.")
|
|
2616
3047
|
continue
|
|
@@ -2682,7 +3113,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2682
3113
|
except Exception:
|
|
2683
3114
|
e_ms = 40
|
|
2684
3115
|
print(f" charge ms={c_ms}, discharge ms={d_ms}, efficiency ms={e_ms}")
|
|
2685
|
-
spec =
|
|
3116
|
+
spec = _safe_input("Set marker size: 'c <ms>', 'd <ms>', 'e <ms>' (q=cancel): ").strip().lower()
|
|
2686
3117
|
if not spec or spec == 'q':
|
|
2687
3118
|
_print_menu(); continue
|
|
2688
3119
|
parts = spec.split()
|
|
@@ -2939,7 +3370,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2939
3370
|
print(_colorize_inline_commands("Type 'i' to invert tick direction, 'l' to change tick length, 'list' to show current state, 'q' to go back."))
|
|
2940
3371
|
print(_colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
|
|
2941
3372
|
while True:
|
|
2942
|
-
cmd =
|
|
3373
|
+
cmd = _safe_input(_colorize_prompt("t> ")).strip().lower()
|
|
2943
3374
|
if not cmd:
|
|
2944
3375
|
continue
|
|
2945
3376
|
if cmd == 'q':
|
|
@@ -2964,7 +3395,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2964
3395
|
# Get current major tick length from axes
|
|
2965
3396
|
current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
|
|
2966
3397
|
print(f"Current major tick length: {current_major}")
|
|
2967
|
-
new_length_str =
|
|
3398
|
+
new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
|
|
2968
3399
|
if not new_length_str:
|
|
2969
3400
|
continue
|
|
2970
3401
|
new_major = float(new_length_str)
|
|
@@ -3039,7 +3470,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3039
3470
|
current_y_px = _px_value('_top_xlabel_manual_offset_y_pts')
|
|
3040
3471
|
current_x_px = _px_value('_top_xlabel_manual_offset_x_pts')
|
|
3041
3472
|
print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
|
|
3042
|
-
sub =
|
|
3473
|
+
sub = _safe_input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
3043
3474
|
if not sub:
|
|
3044
3475
|
continue
|
|
3045
3476
|
if sub == 'q':
|
|
@@ -3076,7 +3507,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3076
3507
|
while True:
|
|
3077
3508
|
current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts')
|
|
3078
3509
|
print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
|
|
3079
|
-
sub =
|
|
3510
|
+
sub = _safe_input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
|
|
3080
3511
|
if not sub:
|
|
3081
3512
|
continue
|
|
3082
3513
|
if sub == 'q':
|
|
@@ -3106,7 +3537,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3106
3537
|
while True:
|
|
3107
3538
|
current_x_px = _px_value('_left_ylabel_manual_offset_x_pts')
|
|
3108
3539
|
print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
|
|
3109
|
-
sub =
|
|
3540
|
+
sub = _safe_input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
3110
3541
|
if not sub:
|
|
3111
3542
|
continue
|
|
3112
3543
|
if sub == 'q':
|
|
@@ -3137,7 +3568,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3137
3568
|
current_x_px = _px_value('_right_ylabel_manual_offset_x_pts')
|
|
3138
3569
|
current_y_px = _px_value('_right_ylabel_manual_offset_y_pts')
|
|
3139
3570
|
print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
|
|
3140
|
-
sub =
|
|
3571
|
+
sub = _safe_input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
|
|
3141
3572
|
if not sub:
|
|
3142
3573
|
continue
|
|
3143
3574
|
if sub == 'q':
|
|
@@ -3175,7 +3606,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3175
3606
|
print(" " + _colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
|
|
3176
3607
|
print(" " + _colorize_menu('r : reset all offsets'))
|
|
3177
3608
|
print(" " + _colorize_menu('q : return'))
|
|
3178
|
-
choice =
|
|
3609
|
+
choice = _safe_input(_colorize_prompt("p> ")).strip().lower()
|
|
3179
3610
|
if not choice:
|
|
3180
3611
|
continue
|
|
3181
3612
|
if choice == 'q':
|
|
@@ -3276,7 +3707,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3276
3707
|
elif key == 'g':
|
|
3277
3708
|
while True:
|
|
3278
3709
|
print("Geometry: p=plot frame, c=canvas, q=back")
|
|
3279
|
-
sub =
|
|
3710
|
+
sub = _safe_input("Geom> ").strip().lower()
|
|
3280
3711
|
if not sub:
|
|
3281
3712
|
continue
|
|
3282
3713
|
if sub == 'q':
|
|
@@ -3299,18 +3730,234 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3299
3730
|
# Rename axis titles
|
|
3300
3731
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
3301
3732
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
3302
|
-
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3733
|
+
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3303
3734
|
while True:
|
|
3304
|
-
print("Rename
|
|
3305
|
-
sub =
|
|
3735
|
+
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3736
|
+
sub = _safe_input("Rename> ").strip().lower()
|
|
3306
3737
|
if not sub:
|
|
3307
3738
|
continue
|
|
3308
3739
|
if sub == 'q':
|
|
3309
3740
|
break
|
|
3310
|
-
if sub == '
|
|
3741
|
+
if sub == 'l':
|
|
3742
|
+
# Rename legend labels (file name in legend)
|
|
3743
|
+
if not is_multi_file:
|
|
3744
|
+
# Single file mode: rename the default file
|
|
3745
|
+
current_file = file_data[0]
|
|
3746
|
+
sc_chg = current_file['sc_charge']
|
|
3747
|
+
sc_dchg = current_file['sc_discharge']
|
|
3748
|
+
sc_eff = current_file['sc_eff']
|
|
3749
|
+
|
|
3750
|
+
# Get current labels
|
|
3751
|
+
chg_label = sc_chg.get_label() or ''
|
|
3752
|
+
dchg_label = sc_dchg.get_label() or ''
|
|
3753
|
+
eff_label = sc_eff.get_label() or ''
|
|
3754
|
+
|
|
3755
|
+
# Extract base filename (everything before " charge", " discharge", or " efficiency")
|
|
3756
|
+
# Also handle patterns like "filename (Chg)", "filename (Dchg)", "filename (Eff)"
|
|
3757
|
+
base_name = current_file.get('filename', 'Data')
|
|
3758
|
+
|
|
3759
|
+
# Try to extract from labels
|
|
3760
|
+
import re
|
|
3761
|
+
for label in [chg_label, dchg_label, eff_label]:
|
|
3762
|
+
if label:
|
|
3763
|
+
# First try to extract from bracket pattern: "filename (Chg)" -> "filename"
|
|
3764
|
+
bracket_match = re.search(r'^(.+?)\s*\([^)]+\)\s*$', label)
|
|
3765
|
+
if bracket_match:
|
|
3766
|
+
potential_base = bracket_match.group(1).strip()
|
|
3767
|
+
if potential_base:
|
|
3768
|
+
base_name = potential_base
|
|
3769
|
+
break
|
|
3770
|
+
else:
|
|
3771
|
+
# Try to extract from text suffix patterns
|
|
3772
|
+
for suffix in [' charge', ' discharge', ' efficiency']:
|
|
3773
|
+
if label.endswith(suffix):
|
|
3774
|
+
potential_base = label[:-len(suffix)].strip()
|
|
3775
|
+
if potential_base:
|
|
3776
|
+
base_name = potential_base
|
|
3777
|
+
break
|
|
3778
|
+
if base_name != current_file.get('filename', 'Data'):
|
|
3779
|
+
break
|
|
3780
|
+
|
|
3781
|
+
print(f"Current file name in legend: '{base_name}'")
|
|
3782
|
+
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3783
|
+
if new_name and new_name.lower() != 'q':
|
|
3784
|
+
try:
|
|
3785
|
+
push_state("rename-legend")
|
|
3786
|
+
|
|
3787
|
+
# Extract bracket content from original labels if present
|
|
3788
|
+
import re
|
|
3789
|
+
chg_bracket = ''
|
|
3790
|
+
dchg_bracket = ''
|
|
3791
|
+
eff_bracket = ''
|
|
3792
|
+
|
|
3793
|
+
# Check for bracket patterns in original labels
|
|
3794
|
+
chg_match = re.search(r'\(([^)]+)\)', chg_label)
|
|
3795
|
+
if chg_match:
|
|
3796
|
+
chg_bracket = chg_match.group(1)
|
|
3797
|
+
dchg_match = re.search(r'\(([^)]+)\)', dchg_label)
|
|
3798
|
+
if dchg_match:
|
|
3799
|
+
dchg_bracket = dchg_match.group(1)
|
|
3800
|
+
# Fix capitalization: Dchg -> DChg
|
|
3801
|
+
if dchg_bracket.lower() == 'dchg':
|
|
3802
|
+
dchg_bracket = 'DChg'
|
|
3803
|
+
eff_match = re.search(r'\(([^)]+)\)', eff_label)
|
|
3804
|
+
if eff_match:
|
|
3805
|
+
eff_bracket = eff_match.group(1)
|
|
3806
|
+
|
|
3807
|
+
# If no brackets found, extract from label suffix or use defaults
|
|
3808
|
+
if not chg_bracket:
|
|
3809
|
+
# Try to extract from " charge" suffix
|
|
3810
|
+
if chg_label.endswith(' charge'):
|
|
3811
|
+
chg_bracket = 'Chg'
|
|
3812
|
+
else:
|
|
3813
|
+
chg_bracket = 'Chg'
|
|
3814
|
+
if not dchg_bracket:
|
|
3815
|
+
# Try to extract from " discharge" suffix
|
|
3816
|
+
if dchg_label.endswith(' discharge'):
|
|
3817
|
+
dchg_bracket = 'DChg'
|
|
3818
|
+
else:
|
|
3819
|
+
dchg_bracket = 'DChg'
|
|
3820
|
+
if not eff_bracket:
|
|
3821
|
+
# Try to extract from " efficiency" suffix
|
|
3822
|
+
if eff_label.endswith(' efficiency'):
|
|
3823
|
+
eff_bracket = 'Eff'
|
|
3824
|
+
else:
|
|
3825
|
+
eff_bracket = 'Eff'
|
|
3826
|
+
|
|
3827
|
+
# Build new labels with brackets preserved
|
|
3828
|
+
new_chg_label = f"{new_name} ({chg_bracket})"
|
|
3829
|
+
new_dchg_label = f"{new_name} ({dchg_bracket})"
|
|
3830
|
+
new_eff_label = f"{new_name} ({eff_bracket})"
|
|
3831
|
+
|
|
3832
|
+
# Update labels
|
|
3833
|
+
sc_chg.set_label(new_chg_label)
|
|
3834
|
+
sc_dchg.set_label(new_dchg_label)
|
|
3835
|
+
sc_eff.set_label(new_eff_label)
|
|
3836
|
+
|
|
3837
|
+
# Update filename in file_data
|
|
3838
|
+
current_file['filename'] = new_name
|
|
3839
|
+
|
|
3840
|
+
# Rebuild legend (preserve position)
|
|
3841
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
3842
|
+
fig.canvas.draw_idle()
|
|
3843
|
+
print(f"Legend labels updated: '{new_chg_label}', '{new_dchg_label}', '{new_eff_label}'")
|
|
3844
|
+
except Exception as e:
|
|
3845
|
+
print(f"Error: {e}")
|
|
3846
|
+
else:
|
|
3847
|
+
# Multi-file mode: show file list and let user select
|
|
3848
|
+
print("\nAvailable files:")
|
|
3849
|
+
_print_file_list(file_data, current_file_idx)
|
|
3850
|
+
file_choice = _safe_input("Enter file number to rename (q=cancel): ").strip()
|
|
3851
|
+
if file_choice and file_choice.lower() != 'q':
|
|
3852
|
+
try:
|
|
3853
|
+
file_idx = int(file_choice) - 1
|
|
3854
|
+
if 0 <= file_idx < len(file_data):
|
|
3855
|
+
current_file = file_data[file_idx]
|
|
3856
|
+
sc_chg = current_file['sc_charge']
|
|
3857
|
+
sc_dchg = current_file['sc_discharge']
|
|
3858
|
+
sc_eff = current_file['sc_eff']
|
|
3859
|
+
|
|
3860
|
+
# Get current labels
|
|
3861
|
+
chg_label = sc_chg.get_label() or ''
|
|
3862
|
+
dchg_label = sc_dchg.get_label() or ''
|
|
3863
|
+
eff_label = sc_eff.get_label() or ''
|
|
3864
|
+
|
|
3865
|
+
# Extract base filename
|
|
3866
|
+
base_name = current_file.get('filename', 'Data')
|
|
3867
|
+
import re
|
|
3868
|
+
for label in [chg_label, dchg_label, eff_label]:
|
|
3869
|
+
if label:
|
|
3870
|
+
# First try to extract from bracket pattern: "filename (Chg)" -> "filename"
|
|
3871
|
+
bracket_match = re.search(r'^(.+?)\s*\([^)]+\)\s*$', label)
|
|
3872
|
+
if bracket_match:
|
|
3873
|
+
potential_base = bracket_match.group(1).strip()
|
|
3874
|
+
if potential_base:
|
|
3875
|
+
base_name = potential_base
|
|
3876
|
+
break
|
|
3877
|
+
else:
|
|
3878
|
+
# Try to extract from text suffix patterns
|
|
3879
|
+
for suffix in [' charge', ' discharge', ' efficiency']:
|
|
3880
|
+
if label.endswith(suffix):
|
|
3881
|
+
potential_base = label[:-len(suffix)].strip()
|
|
3882
|
+
if potential_base:
|
|
3883
|
+
base_name = potential_base
|
|
3884
|
+
break
|
|
3885
|
+
if base_name != current_file.get('filename', 'Data'):
|
|
3886
|
+
break
|
|
3887
|
+
|
|
3888
|
+
print(f"Current file name in legend: '{base_name}'")
|
|
3889
|
+
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3890
|
+
if new_name and new_name.lower() != 'q':
|
|
3891
|
+
try:
|
|
3892
|
+
push_state("rename-legend")
|
|
3893
|
+
|
|
3894
|
+
# Extract bracket content from original labels if present
|
|
3895
|
+
import re
|
|
3896
|
+
chg_bracket = ''
|
|
3897
|
+
dchg_bracket = ''
|
|
3898
|
+
eff_bracket = ''
|
|
3899
|
+
|
|
3900
|
+
# Check for bracket patterns in original labels
|
|
3901
|
+
chg_match = re.search(r'\(([^)]+)\)', chg_label)
|
|
3902
|
+
if chg_match:
|
|
3903
|
+
chg_bracket = chg_match.group(1)
|
|
3904
|
+
dchg_match = re.search(r'\(([^)]+)\)', dchg_label)
|
|
3905
|
+
if dchg_match:
|
|
3906
|
+
dchg_bracket = dchg_match.group(1)
|
|
3907
|
+
# Fix capitalization: Dchg -> DChg
|
|
3908
|
+
if dchg_bracket.lower() == 'dchg':
|
|
3909
|
+
dchg_bracket = 'DChg'
|
|
3910
|
+
eff_match = re.search(r'\(([^)]+)\)', eff_label)
|
|
3911
|
+
if eff_match:
|
|
3912
|
+
eff_bracket = eff_match.group(1)
|
|
3913
|
+
|
|
3914
|
+
# If no brackets found, extract from label suffix or use defaults
|
|
3915
|
+
if not chg_bracket:
|
|
3916
|
+
# Try to extract from " charge" suffix
|
|
3917
|
+
if chg_label.endswith(' charge'):
|
|
3918
|
+
chg_bracket = 'Chg'
|
|
3919
|
+
else:
|
|
3920
|
+
chg_bracket = 'Chg'
|
|
3921
|
+
if not dchg_bracket:
|
|
3922
|
+
# Try to extract from " discharge" suffix
|
|
3923
|
+
if dchg_label.endswith(' discharge'):
|
|
3924
|
+
dchg_bracket = 'DChg'
|
|
3925
|
+
else:
|
|
3926
|
+
dchg_bracket = 'DChg'
|
|
3927
|
+
if not eff_bracket:
|
|
3928
|
+
# Try to extract from " efficiency" suffix
|
|
3929
|
+
if eff_label.endswith(' efficiency'):
|
|
3930
|
+
eff_bracket = 'Eff'
|
|
3931
|
+
else:
|
|
3932
|
+
eff_bracket = 'Eff'
|
|
3933
|
+
|
|
3934
|
+
# Build new labels with brackets preserved
|
|
3935
|
+
new_chg_label = f"{new_name} ({chg_bracket})"
|
|
3936
|
+
new_dchg_label = f"{new_name} ({dchg_bracket})"
|
|
3937
|
+
new_eff_label = f"{new_name} ({eff_bracket})"
|
|
3938
|
+
|
|
3939
|
+
# Update labels
|
|
3940
|
+
sc_chg.set_label(new_chg_label)
|
|
3941
|
+
sc_dchg.set_label(new_dchg_label)
|
|
3942
|
+
sc_eff.set_label(new_eff_label)
|
|
3943
|
+
|
|
3944
|
+
# Update filename in file_data
|
|
3945
|
+
current_file['filename'] = new_name
|
|
3946
|
+
|
|
3947
|
+
# Rebuild legend (preserve position)
|
|
3948
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
3949
|
+
fig.canvas.draw_idle()
|
|
3950
|
+
print(f"Legend labels updated: '{new_chg_label}', '{new_dchg_label}', '{new_eff_label}'")
|
|
3951
|
+
except Exception as e:
|
|
3952
|
+
print(f"Error: {e}")
|
|
3953
|
+
else:
|
|
3954
|
+
print("Invalid file number.")
|
|
3955
|
+
except (ValueError, KeyboardInterrupt):
|
|
3956
|
+
print("Invalid input.")
|
|
3957
|
+
elif sub == 'x':
|
|
3311
3958
|
current = ax.get_xlabel()
|
|
3312
3959
|
print(f"Current x-axis title: '{current}'")
|
|
3313
|
-
new_title =
|
|
3960
|
+
new_title = _safe_input("Enter new x-axis title (q=cancel): ")
|
|
3314
3961
|
if new_title and new_title.lower() != 'q':
|
|
3315
3962
|
try:
|
|
3316
3963
|
push_state("rename-x")
|
|
@@ -3329,7 +3976,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3329
3976
|
elif sub == 'ly':
|
|
3330
3977
|
current = ax.get_ylabel()
|
|
3331
3978
|
print(f"Current left y-axis title: '{current}'")
|
|
3332
|
-
new_title =
|
|
3979
|
+
new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
|
|
3333
3980
|
if new_title and new_title.lower() != 'q':
|
|
3334
3981
|
try:
|
|
3335
3982
|
push_state("rename-ly")
|
|
@@ -3343,7 +3990,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3343
3990
|
elif sub == 'ry':
|
|
3344
3991
|
current = ax2.get_ylabel()
|
|
3345
3992
|
print(f"Current right y-axis title: '{current}'")
|
|
3346
|
-
new_title =
|
|
3993
|
+
new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
|
|
3347
3994
|
if new_title and new_title.lower() != 'q':
|
|
3348
3995
|
try:
|
|
3349
3996
|
push_state("rename-ry")
|
|
@@ -3363,7 +4010,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3363
4010
|
while True:
|
|
3364
4011
|
current_xlim = ax.get_xlim()
|
|
3365
4012
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3366
|
-
rng =
|
|
4013
|
+
rng = _safe_input("Enter x-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3367
4014
|
if not rng or rng.lower() == 'q':
|
|
3368
4015
|
break
|
|
3369
4016
|
if rng.lower() == 'w':
|
|
@@ -3371,7 +4018,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3371
4018
|
while True:
|
|
3372
4019
|
current_xlim = ax.get_xlim()
|
|
3373
4020
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3374
|
-
val =
|
|
4021
|
+
val = _safe_input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
|
|
3375
4022
|
if not val or val.lower() == 'q':
|
|
3376
4023
|
break
|
|
3377
4024
|
try:
|
|
@@ -3398,7 +4045,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3398
4045
|
while True:
|
|
3399
4046
|
current_xlim = ax.get_xlim()
|
|
3400
4047
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3401
|
-
val =
|
|
4048
|
+
val = _safe_input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
|
|
3402
4049
|
if not val or val.lower() == 'q':
|
|
3403
4050
|
break
|
|
3404
4051
|
try:
|
|
@@ -3461,208 +4108,208 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3461
4108
|
elif key == 'y':
|
|
3462
4109
|
while True:
|
|
3463
4110
|
print("Y-ranges: ly=left axis, ry=right axis, q=back")
|
|
3464
|
-
ycmd =
|
|
4111
|
+
ycmd = _safe_input("Y> ").strip().lower()
|
|
3465
4112
|
if not ycmd:
|
|
3466
4113
|
continue
|
|
3467
4114
|
if ycmd == 'q':
|
|
3468
4115
|
break
|
|
3469
4116
|
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)
|
|
4117
|
+
while True:
|
|
4118
|
+
current_ylim = ax.get_ylim()
|
|
4119
|
+
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4120
|
+
rng = _safe_input("Enter left y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
4121
|
+
if not rng or rng.lower() == 'q':
|
|
4122
|
+
break
|
|
4123
|
+
if rng.lower() == 'w':
|
|
4124
|
+
# Upper only: change upper limit, fix lower - stay in loop
|
|
4125
|
+
while True:
|
|
4126
|
+
current_ylim = ax.get_ylim()
|
|
4127
|
+
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4128
|
+
val = _safe_input(f"Enter new upper left Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
4129
|
+
if not val or val.lower() == 'q':
|
|
4130
|
+
break
|
|
4131
|
+
try:
|
|
4132
|
+
new_upper = float(val)
|
|
4133
|
+
except (ValueError, KeyboardInterrupt):
|
|
4134
|
+
print("Invalid value, ignored.")
|
|
4135
|
+
continue
|
|
4136
|
+
push_state("y-left-range")
|
|
4137
|
+
ax.set_ylim(current_ylim[0], new_upper)
|
|
3544
4138
|
ax.relim()
|
|
3545
4139
|
ax.autoscale_view(scalex=False, scaley=True)
|
|
4140
|
+
# Reapply legend position after axis change to prevent movement
|
|
4141
|
+
try:
|
|
4142
|
+
leg = ax.get_legend()
|
|
4143
|
+
if leg is not None and leg.get_visible():
|
|
4144
|
+
_apply_legend_position()
|
|
4145
|
+
except Exception:
|
|
4146
|
+
pass
|
|
3546
4147
|
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
|
|
4148
|
+
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
4149
|
+
continue
|
|
4150
|
+
if rng.lower() == 's':
|
|
4151
|
+
# Lower only: change lower limit, fix upper - stay in loop
|
|
4152
|
+
while True:
|
|
4153
|
+
current_ylim = ax.get_ylim()
|
|
4154
|
+
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4155
|
+
val = _safe_input(f"Enter new lower left Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
4156
|
+
if not val or val.lower() == 'q':
|
|
4157
|
+
break
|
|
4158
|
+
try:
|
|
4159
|
+
new_lower = float(val)
|
|
4160
|
+
except (ValueError, KeyboardInterrupt):
|
|
4161
|
+
print("Invalid value, ignored.")
|
|
4162
|
+
continue
|
|
4163
|
+
push_state("y-left-range")
|
|
4164
|
+
ax.set_ylim(new_lower, current_ylim[1])
|
|
4165
|
+
ax.relim()
|
|
4166
|
+
ax.autoscale_view(scalex=False, scaley=True)
|
|
4167
|
+
# Reapply legend position after axis change to prevent movement
|
|
4168
|
+
try:
|
|
4169
|
+
leg = ax.get_legend()
|
|
4170
|
+
if leg is not None and leg.get_visible():
|
|
4171
|
+
_apply_legend_position()
|
|
4172
|
+
except Exception:
|
|
4173
|
+
pass
|
|
4174
|
+
fig.canvas.draw_idle()
|
|
4175
|
+
print(f"Left Y range updated: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
4176
|
+
continue
|
|
4177
|
+
if rng.lower() == 'a':
|
|
4178
|
+
# Auto: restore original range from scatter plots
|
|
4179
|
+
push_state("y-left-range-auto")
|
|
3624
4180
|
try:
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
4181
|
+
all_y = []
|
|
4182
|
+
for sc in [sc_charge, sc_discharge]:
|
|
4183
|
+
if sc is not None and hasattr(sc, 'get_offsets'):
|
|
4184
|
+
offsets = sc.get_offsets()
|
|
4185
|
+
if offsets.size > 0:
|
|
4186
|
+
all_y.extend([offsets[:, 1].min(), offsets[:, 1].max()])
|
|
4187
|
+
if all_y:
|
|
4188
|
+
orig_min = min(all_y)
|
|
4189
|
+
orig_max = max(all_y)
|
|
4190
|
+
ax.set_ylim(orig_min, orig_max)
|
|
4191
|
+
ax.relim()
|
|
4192
|
+
ax.autoscale_view(scalex=False, scaley=True)
|
|
4193
|
+
fig.canvas.draw_idle()
|
|
4194
|
+
print(f"Left Y range restored to original: {ax.get_ylim()[0]:.6g} to {ax.get_ylim()[1]:.6g}")
|
|
4195
|
+
else:
|
|
4196
|
+
print("No original data available.")
|
|
4197
|
+
except Exception as e:
|
|
4198
|
+
print(f"Error restoring original left Y range: {e}")
|
|
4199
|
+
continue
|
|
4200
|
+
parts = rng.replace(',', ' ').split()
|
|
4201
|
+
if len(parts) != 2:
|
|
4202
|
+
print("Need two numbers."); continue
|
|
4203
|
+
try:
|
|
4204
|
+
lo = float(parts[0]); hi = float(parts[1])
|
|
4205
|
+
if lo == hi:
|
|
4206
|
+
print("Min and max cannot be equal."); continue
|
|
4207
|
+
push_state("y-left-range")
|
|
4208
|
+
ax.set_ylim(min(lo, hi), max(lo, hi))
|
|
3630
4209
|
fig.canvas.draw_idle()
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
push_state("y-right-range-auto")
|
|
4210
|
+
except Exception:
|
|
4211
|
+
print("Invalid numbers.")
|
|
4212
|
+
elif ycmd == 'ry':
|
|
4213
|
+
while True:
|
|
3636
4214
|
try:
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
4215
|
+
eff_on = bool(sc_eff.get_visible())
|
|
4216
|
+
except Exception:
|
|
4217
|
+
eff_on = True
|
|
4218
|
+
if not eff_on:
|
|
4219
|
+
print("Right Y is not shown; enable efficiency with 'ry' first.")
|
|
4220
|
+
break
|
|
4221
|
+
current_ylim = ax2.get_ylim()
|
|
4222
|
+
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4223
|
+
rng = _safe_input("Enter right y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
4224
|
+
if not rng or rng.lower() == 'q':
|
|
4225
|
+
break
|
|
4226
|
+
if rng.lower() == 'w':
|
|
4227
|
+
# Upper only: change upper limit, fix lower - stay in loop
|
|
4228
|
+
while True:
|
|
4229
|
+
current_ylim = ax2.get_ylim()
|
|
4230
|
+
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4231
|
+
val = _safe_input(f"Enter new upper right Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
4232
|
+
if not val or val.lower() == 'q':
|
|
4233
|
+
break
|
|
4234
|
+
try:
|
|
4235
|
+
new_upper = float(val)
|
|
4236
|
+
except (ValueError, KeyboardInterrupt):
|
|
4237
|
+
print("Invalid value, ignored.")
|
|
4238
|
+
continue
|
|
4239
|
+
push_state("y-right-range")
|
|
4240
|
+
ax2.set_ylim(current_ylim[0], new_upper)
|
|
4241
|
+
ax2.relim()
|
|
4242
|
+
ax2.autoscale_view(scalex=False, scaley=True)
|
|
4243
|
+
# Reapply legend position after axis change to prevent movement
|
|
4244
|
+
try:
|
|
4245
|
+
leg = ax.get_legend()
|
|
4246
|
+
if leg is not None and leg.get_visible():
|
|
4247
|
+
_apply_legend_position()
|
|
4248
|
+
except Exception:
|
|
4249
|
+
pass
|
|
4250
|
+
fig.canvas.draw_idle()
|
|
4251
|
+
print(f"Right Y range updated: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
4252
|
+
continue
|
|
4253
|
+
if rng.lower() == 's':
|
|
4254
|
+
# Lower only: change lower limit, fix upper - stay in loop
|
|
4255
|
+
while True:
|
|
4256
|
+
current_ylim = ax2.get_ylim()
|
|
4257
|
+
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4258
|
+
val = _safe_input(f"Enter new lower right Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
4259
|
+
if not val or val.lower() == 'q':
|
|
4260
|
+
break
|
|
4261
|
+
try:
|
|
4262
|
+
new_lower = float(val)
|
|
4263
|
+
except (ValueError, KeyboardInterrupt):
|
|
4264
|
+
print("Invalid value, ignored.")
|
|
4265
|
+
continue
|
|
4266
|
+
push_state("y-right-range")
|
|
4267
|
+
ax2.set_ylim(new_lower, current_ylim[1])
|
|
4268
|
+
ax2.relim()
|
|
4269
|
+
ax2.autoscale_view(scalex=False, scaley=True)
|
|
4270
|
+
# Reapply legend position after axis change to prevent movement
|
|
4271
|
+
try:
|
|
4272
|
+
leg = ax.get_legend()
|
|
4273
|
+
if leg is not None and leg.get_visible():
|
|
4274
|
+
_apply_legend_position()
|
|
4275
|
+
except Exception:
|
|
4276
|
+
pass
|
|
4277
|
+
fig.canvas.draw_idle()
|
|
4278
|
+
print(f"Right Y range updated: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
4279
|
+
continue
|
|
4280
|
+
if rng.lower() == 'a':
|
|
4281
|
+
# Auto: restore original range from efficiency scatter plot
|
|
4282
|
+
push_state("y-right-range-auto")
|
|
4283
|
+
try:
|
|
4284
|
+
if sc_eff is not None and hasattr(sc_eff, 'get_offsets'):
|
|
4285
|
+
offsets = sc_eff.get_offsets()
|
|
4286
|
+
if offsets.size > 0:
|
|
4287
|
+
orig_min = float(offsets[:, 1].min())
|
|
4288
|
+
orig_max = float(offsets[:, 1].max())
|
|
4289
|
+
ax2.set_ylim(orig_min, orig_max)
|
|
4290
|
+
ax2.relim()
|
|
4291
|
+
ax2.autoscale_view(scalex=False, scaley=True)
|
|
4292
|
+
fig.canvas.draw_idle()
|
|
4293
|
+
print(f"Right Y range restored to original: {ax2.get_ylim()[0]:.6g} to {ax2.get_ylim()[1]:.6g}")
|
|
4294
|
+
else:
|
|
4295
|
+
print("No original data available.")
|
|
3647
4296
|
else:
|
|
3648
4297
|
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.")
|
|
4298
|
+
except Exception as e:
|
|
4299
|
+
print(f"Error restoring original right Y range: {e}")
|
|
4300
|
+
continue
|
|
4301
|
+
parts = rng.replace(',', ' ').split()
|
|
4302
|
+
if len(parts) != 2:
|
|
4303
|
+
print("Need two numbers."); continue
|
|
4304
|
+
try:
|
|
4305
|
+
lo = float(parts[0]); hi = float(parts[1])
|
|
4306
|
+
if lo == hi:
|
|
4307
|
+
print("Min and max cannot be equal."); continue
|
|
4308
|
+
push_state("y-right-range")
|
|
4309
|
+
ax2.set_ylim(min(lo, hi), max(lo, hi))
|
|
4310
|
+
fig.canvas.draw_idle()
|
|
4311
|
+
except Exception:
|
|
4312
|
+
print("Invalid numbers.")
|
|
3666
4313
|
_print_menu(); continue
|
|
3667
4314
|
else:
|
|
3668
4315
|
print("Unknown key.")
|