batplot 1.7.25__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 +62 -31
- batplot/color_utils.py +13 -6
- batplot/cpc_interactive.py +511 -320
- batplot/electrochem_interactive.py +20 -2
- batplot/interactive.py +8 -4
- batplot/operando_ec_interactive.py +1 -1
- batplot/session.py +11 -3
- batplot/ui.py +13 -29
- {batplot-1.7.25.dist-info → batplot-1.7.26.dist-info}/METADATA +1 -1
- {batplot-1.7.25.dist-info → batplot-1.7.26.dist-info}/RECORD +15 -15
- {batplot-1.7.25.dist-info → batplot-1.7.26.dist-info}/WHEEL +0 -0
- {batplot-1.7.25.dist-info → batplot-1.7.26.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.25.dist-info → batplot-1.7.26.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.25.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:
|
|
@@ -548,16 +592,19 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
548
592
|
'series': {
|
|
549
593
|
'charge': {
|
|
550
594
|
'color': _color_of(sc_charge),
|
|
595
|
+
'marker': getattr(sc_charge, 'get_marker', lambda: 'o')(),
|
|
551
596
|
'markersize': float(getattr(sc_charge, 'get_sizes', lambda: [32])()[0]) if hasattr(sc_charge, 'get_sizes') else 32.0,
|
|
552
597
|
'alpha': float(sc_charge.get_alpha()) if sc_charge.get_alpha() is not None else 1.0,
|
|
553
598
|
},
|
|
554
599
|
'discharge': {
|
|
555
600
|
'color': _color_of(sc_discharge),
|
|
601
|
+
'marker': getattr(sc_discharge, 'get_marker', lambda: 's')(),
|
|
556
602
|
'markersize': float(getattr(sc_discharge, 'get_sizes', lambda: [32])()[0]) if hasattr(sc_discharge, 'get_sizes') else 32.0,
|
|
557
603
|
'alpha': float(sc_discharge.get_alpha()) if sc_discharge.get_alpha() is not None else 1.0,
|
|
558
604
|
},
|
|
559
605
|
'efficiency': {
|
|
560
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: '^')(),
|
|
561
608
|
'markersize': float(getattr(sc_eff, 'get_sizes', lambda: [40])()[0]) if hasattr(sc_eff, 'get_sizes') else 40.0,
|
|
562
609
|
'alpha': float(sc_eff.get_alpha()) if sc_eff.get_alpha() is not None else 1.0,
|
|
563
610
|
'visible': bool(getattr(sc_eff, 'get_visible', lambda: True)()),
|
|
@@ -569,12 +616,18 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
569
616
|
if file_data and isinstance(file_data, list) and len(file_data) > 0:
|
|
570
617
|
multi_files = []
|
|
571
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')
|
|
572
622
|
file_info = {
|
|
573
623
|
'filename': f.get('filename', 'unknown'),
|
|
574
624
|
'visible': f.get('visible', True),
|
|
575
|
-
'charge_color': _color_of(
|
|
576
|
-
'
|
|
577
|
-
'
|
|
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 '^',
|
|
578
631
|
}
|
|
579
632
|
# Save legend labels
|
|
580
633
|
try:
|
|
@@ -726,6 +779,13 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
726
779
|
# Apply marker sizes and alpha globally to all files in multi-file mode
|
|
727
780
|
if is_multi_file:
|
|
728
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'])
|
|
729
789
|
# Marker sizes (global)
|
|
730
790
|
if ch.get('markersize') is not None and hasattr(f['sc_charge'], 'set_sizes'):
|
|
731
791
|
f['sc_charge'].set_sizes([float(ch['markersize'])])
|
|
@@ -759,6 +819,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
759
819
|
if ch:
|
|
760
820
|
if ch.get('color') is not None:
|
|
761
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'])
|
|
762
824
|
if ch.get('markersize') is not None and hasattr(sc_charge, 'set_sizes'):
|
|
763
825
|
sc_charge.set_sizes([float(ch['markersize'])])
|
|
764
826
|
if ch.get('alpha') is not None:
|
|
@@ -766,6 +828,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
766
828
|
if dh:
|
|
767
829
|
if dh.get('color') is not None:
|
|
768
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'])
|
|
769
833
|
if dh.get('markersize') is not None and hasattr(sc_discharge, 'set_sizes'):
|
|
770
834
|
sc_discharge.set_sizes([float(dh['markersize'])])
|
|
771
835
|
if dh.get('alpha') is not None:
|
|
@@ -776,6 +840,8 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
776
840
|
sc_eff.set_color(ef['color'])
|
|
777
841
|
except Exception:
|
|
778
842
|
pass
|
|
843
|
+
if ef.get('marker') is not None and hasattr(sc_eff, 'set_marker'):
|
|
844
|
+
sc_eff.set_marker(ef['marker'])
|
|
779
845
|
if ef.get('markersize') is not None and hasattr(sc_eff, 'set_sizes'):
|
|
780
846
|
sc_eff.set_sizes([float(ef['markersize'])])
|
|
781
847
|
if ef.get('alpha') is not None:
|
|
@@ -803,6 +869,27 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
803
869
|
leg.set_visible(leg_visible)
|
|
804
870
|
if leg_visible:
|
|
805
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
|
|
806
893
|
except Exception:
|
|
807
894
|
pass
|
|
808
895
|
# Apply tick visibility/widths and spines
|
|
@@ -1050,6 +1137,42 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
1050
1137
|
for i, f_info in enumerate(multi_files):
|
|
1051
1138
|
if i < len(file_data):
|
|
1052
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
|
|
1053
1176
|
# Restore legend labels
|
|
1054
1177
|
if 'charge_label' in f_info and f.get('sc_charge'):
|
|
1055
1178
|
try:
|
|
@@ -1206,8 +1329,22 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1206
1329
|
if file_data is None:
|
|
1207
1330
|
# Backward compatibility: create file_data structure from single file
|
|
1208
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
|
|
1209
1346
|
file_data = [{
|
|
1210
|
-
'filename':
|
|
1347
|
+
'filename': filename,
|
|
1211
1348
|
'sc_charge': sc_charge, # Charge capacity scatter artist
|
|
1212
1349
|
'sc_discharge': sc_discharge, # Discharge capacity scatter artist
|
|
1213
1350
|
'sc_eff': sc_eff, # Efficiency scatter artist
|
|
@@ -1327,6 +1464,27 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1327
1464
|
if k in tick_state:
|
|
1328
1465
|
tick_state[k] = bool(v)
|
|
1329
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
|
|
1330
1488
|
try:
|
|
1331
1489
|
fig.canvas.draw()
|
|
1332
1490
|
except Exception:
|
|
@@ -1443,7 +1601,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1443
1601
|
# Update current file's scatter artists for commands that need them
|
|
1444
1602
|
sc_charge, sc_discharge, sc_eff = _get_current_file_artists(file_data, current_file_idx)
|
|
1445
1603
|
|
|
1446
|
-
key =
|
|
1604
|
+
key = _safe_input("Press a key: ").strip().lower()
|
|
1447
1605
|
except (KeyboardInterrupt, EOFError):
|
|
1448
1606
|
print("\n\nExiting interactive menu...")
|
|
1449
1607
|
break
|
|
@@ -1455,7 +1613,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1455
1613
|
try:
|
|
1456
1614
|
if is_multi_file:
|
|
1457
1615
|
_print_file_list(file_data, current_file_idx)
|
|
1458
|
-
choice =
|
|
1616
|
+
choice = _safe_input(f"Toggle visibility for file (1-{len(file_data)}), 'a' for all, or q=cancel: ").strip()
|
|
1459
1617
|
if choice.lower() == 'q':
|
|
1460
1618
|
_print_menu()
|
|
1461
1619
|
_print_file_list(file_data, current_file_idx)
|
|
@@ -1505,7 +1663,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1505
1663
|
|
|
1506
1664
|
if key == 'q':
|
|
1507
1665
|
try:
|
|
1508
|
-
confirm =
|
|
1666
|
+
confirm = _safe_input(_colorize_prompt("Quit CPC interactive? Remember to save! Quit now? (y/n): ")).strip().lower()
|
|
1509
1667
|
except Exception:
|
|
1510
1668
|
confirm = 'y'
|
|
1511
1669
|
if confirm == 'y':
|
|
@@ -1516,283 +1674,282 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1516
1674
|
restore_state()
|
|
1517
1675
|
_print_menu(); continue
|
|
1518
1676
|
elif key == 'c':
|
|
1519
|
-
# 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
|
|
1520
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
|
+
|
|
1521
1749
|
while True:
|
|
1522
|
-
print("\nColors: ly=capacity curves, ry=efficiency triangles, q=back")
|
|
1523
|
-
sub =
|
|
1750
|
+
print("\nColors: ly=capacity curves, ry=efficiency triangles, u=user colors, q=back")
|
|
1751
|
+
sub = _safe_input("Colors> ").strip().lower()
|
|
1524
1752
|
if not sub:
|
|
1525
1753
|
continue
|
|
1526
1754
|
if sub == 'q':
|
|
1527
1755
|
break
|
|
1756
|
+
if sub == 'u':
|
|
1757
|
+
manage_user_colors(fig); continue
|
|
1528
1758
|
if sub == 'ly':
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
print("
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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:
|
|
1550
1786
|
continue
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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)
|
|
1570
1813
|
else:
|
|
1571
|
-
|
|
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
|
|
1572
1835
|
discharge_col = _generate_similar_color(charge_col)
|
|
1573
1836
|
try:
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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)
|
|
1577
1849
|
except Exception:
|
|
1578
1850
|
pass
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
try:
|
|
1582
|
-
idx = int(choice) - 1
|
|
1583
|
-
if 0 <= idx < len(file_data):
|
|
1584
|
-
print("\nCharge color palettes (discharge will be auto-generated):")
|
|
1585
|
-
print(" 1. Reds: #d62728, #c62828, #b71c1c, #8b0000, #a30000")
|
|
1586
|
-
print(" 2. Oranges: #ff7f0e, #ff6f00, #ff5722, #f4511e, #e64a19")
|
|
1587
|
-
print(" 3. Pinks/Magentas: #e377c2, #d81b60, #c2185b, #ad1457, #880e4f")
|
|
1588
|
-
print(" 4. Purples: #9c27b0, #8e24aa, #7b1fa2, #6a1b9a, #4a148c")
|
|
1589
|
-
print(" 5. Deep oranges/reds: #d84315, #bf360c, #c2185b, #d32f2f, #c62828")
|
|
1590
|
-
spec = input("Enter color (name/hex), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1591
|
-
if not spec or spec.lower() == 'q':
|
|
1592
|
-
continue
|
|
1593
|
-
if spec.lower() == 'r':
|
|
1594
|
-
# Use Viridis colormap
|
|
1595
|
-
import matplotlib.cm as cm
|
|
1596
|
-
import matplotlib.colors as mcolors
|
|
1597
|
-
viridis = cm.get_cmap('viridis', 10)
|
|
1598
|
-
charge_col = mcolors.rgb2hex(viridis(_random.random())[:3])
|
|
1599
|
-
elif spec in ['1', '2', '3', '4', '5']:
|
|
1600
|
-
# Use selected palette
|
|
1601
|
-
charge_palettes = [
|
|
1602
|
-
['#d62728', '#c62828', '#b71c1c', '#8b0000', '#a30000'],
|
|
1603
|
-
['#ff7f0e', '#ff6f00', '#ff5722', '#f4511e', '#e64a19'],
|
|
1604
|
-
['#e377c2', '#d81b60', '#c2185b', '#ad1457', '#880e4f'],
|
|
1605
|
-
['#9c27b0', '#8e24aa', '#7b1fa2', '#6a1b9a', '#4a148c'],
|
|
1606
|
-
['#d84315', '#bf360c', '#c2185b', '#d32f2f', '#c62828']
|
|
1607
|
-
]
|
|
1608
|
-
palette = charge_palettes[int(spec) - 1]
|
|
1609
|
-
charge_col = palette[0] # Use first color from palette for single file
|
|
1610
|
-
else:
|
|
1611
|
-
charge_col = spec
|
|
1612
|
-
discharge_col = _generate_similar_color(charge_col)
|
|
1613
|
-
try:
|
|
1614
|
-
file_data[idx]['sc_charge'].set_color(charge_col)
|
|
1615
|
-
file_data[idx]['sc_discharge'].set_color(discharge_col)
|
|
1616
|
-
file_data[idx]['color'] = charge_col
|
|
1617
|
-
except Exception:
|
|
1618
|
-
pass
|
|
1619
|
-
else:
|
|
1620
|
-
print("Invalid file number.")
|
|
1621
|
-
except ValueError:
|
|
1622
|
-
print("Invalid input.")
|
|
1623
|
-
else:
|
|
1624
|
-
# Single file mode
|
|
1625
|
-
push_state("colors-ly")
|
|
1626
|
-
print("\nCharge color palettes (discharge will be auto-generated):")
|
|
1627
|
-
print(" 1. Reds: #d62728, #c62828, #b71c1c, #8b0000, #a30000")
|
|
1628
|
-
print(" 2. Oranges: #ff7f0e, #ff6f00, #ff5722, #f4511e, #e64a19")
|
|
1629
|
-
print(" 3. Pinks/Magentas: #e377c2, #d81b60, #c2185b, #ad1457, #880e4f")
|
|
1630
|
-
print(" 4. Purples: #9c27b0, #8e24aa, #7b1fa2, #6a1b9a, #4a148c")
|
|
1631
|
-
print(" 5. Deep oranges/reds: #d84315, #bf360c, #c2185b, #d32f2f, #c62828")
|
|
1632
|
-
spec = input("Enter color (name/hex), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1633
|
-
if not spec or spec.lower() == 'q':
|
|
1634
|
-
continue
|
|
1635
|
-
if spec.strip().lower() == 'r':
|
|
1636
|
-
# Use Viridis colormap
|
|
1637
|
-
import matplotlib.cm as cm
|
|
1638
|
-
import matplotlib.colors as mcolors
|
|
1639
|
-
viridis = cm.get_cmap('viridis', 10)
|
|
1640
|
-
charge_col = mcolors.rgb2hex(viridis(_random.random())[:3])
|
|
1641
|
-
elif spec in ['1', '2', '3', '4', '5']:
|
|
1642
|
-
# Use selected palette
|
|
1643
|
-
charge_palettes = [
|
|
1644
|
-
['#d62728', '#c62828', '#b71c1c', '#8b0000', '#a30000'],
|
|
1645
|
-
['#ff7f0e', '#ff6f00', '#ff5722', '#f4511e', '#e64a19'],
|
|
1646
|
-
['#e377c2', '#d81b60', '#c2185b', '#ad1457', '#880e4f'],
|
|
1647
|
-
['#9c27b0', '#8e24aa', '#7b1fa2', '#6a1b9a', '#4a148c'],
|
|
1648
|
-
['#d84315', '#bf360c', '#c2185b', '#d32f2f', '#c62828']
|
|
1649
|
-
]
|
|
1650
|
-
palette = charge_palettes[int(spec) - 1]
|
|
1651
|
-
charge_col = palette[0] # Use first color from palette
|
|
1652
|
-
else:
|
|
1653
|
-
charge_col = spec
|
|
1654
|
-
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):
|
|
1655
1853
|
try:
|
|
1656
|
-
sc_charge
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
if not is_multi_file and getattr(fig, '_cpc_spine_auto', False):
|
|
1660
|
-
_set_spine_color('left', charge_col)
|
|
1854
|
+
cur_col = _color_of(sc_charge)
|
|
1855
|
+
if cur_col:
|
|
1856
|
+
_set_spine_color('left', cur_col)
|
|
1661
1857
|
except Exception:
|
|
1662
1858
|
pass
|
|
1663
1859
|
try:
|
|
1664
|
-
_rebuild_legend(ax, ax2, file_data)
|
|
1665
|
-
fig.canvas.draw_idle()
|
|
1860
|
+
_rebuild_legend(ax, ax2, file_data); fig.canvas.draw_idle()
|
|
1666
1861
|
except Exception:
|
|
1667
1862
|
pass
|
|
1668
1863
|
elif sub == 'ry':
|
|
1669
1864
|
push_state("colors-ry")
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
print("
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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:
|
|
1689
1891
|
continue
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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)
|
|
1708
1911
|
else:
|
|
1709
|
-
|
|
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:
|
|
1921
|
+
try:
|
|
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
|
|
1710
1933
|
try:
|
|
1711
|
-
|
|
1712
|
-
|
|
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)
|
|
1713
1941
|
except Exception:
|
|
1714
1942
|
pass
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
idx = int(choice) - 1
|
|
1718
|
-
if 0 <= idx < len(file_data):
|
|
1719
|
-
print("\nEfficiency color palettes:")
|
|
1720
|
-
print(" 1. Blues: #1f77b4, #1976d2, #1565c0, #0d47a1, #01579b")
|
|
1721
|
-
print(" 2. Cyans/Teals: #17becf, #00acc1, #0097a7, #00838f, #006064")
|
|
1722
|
-
print(" 3. Purples/Indigos: #9467bd, #5e35b1, #512da8, #4527a0, #311b92")
|
|
1723
|
-
print(" 4. Deep blues: #2196f3, #1e88e5, #1976d2, #1565c0, #0d47a1")
|
|
1724
|
-
print(" 5. Dark cyans/purples: #0097a7, #00838f, #006064, #5e35b1, #4527a0")
|
|
1725
|
-
val = input("Enter color (hex/name), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1726
|
-
if not val or val.lower() == 'q':
|
|
1727
|
-
continue
|
|
1728
|
-
if val.lower() == 'r':
|
|
1729
|
-
# Use Plasma colormap
|
|
1730
|
-
import matplotlib.cm as cm
|
|
1731
|
-
import matplotlib.colors as mcolors
|
|
1732
|
-
plasma = cm.get_cmap('plasma', 10)
|
|
1733
|
-
col = mcolors.rgb2hex(plasma(_random.random())[:3])
|
|
1734
|
-
elif val in ['1', '2', '3', '4', '5']:
|
|
1735
|
-
# Use selected palette
|
|
1736
|
-
efficiency_palettes = [
|
|
1737
|
-
['#1f77b4', '#1976d2', '#1565c0', '#0d47a1', '#01579b'],
|
|
1738
|
-
['#17becf', '#00acc1', '#0097a7', '#00838f', '#006064'],
|
|
1739
|
-
['#9467bd', '#5e35b1', '#512da8', '#4527a0', '#311b92'],
|
|
1740
|
-
['#2196f3', '#1e88e5', '#1976d2', '#1565c0', '#0d47a1'],
|
|
1741
|
-
['#0097a7', '#00838f', '#006064', '#5e35b1', '#4527a0']
|
|
1742
|
-
]
|
|
1743
|
-
palette = efficiency_palettes[int(val) - 1]
|
|
1744
|
-
col = palette[0] # Use first color from palette for single file
|
|
1745
|
-
else:
|
|
1746
|
-
col = val
|
|
1747
|
-
try:
|
|
1748
|
-
file_data[idx]['sc_eff'].set_color(col)
|
|
1749
|
-
file_data[idx]['eff_color'] = col # Store efficiency color
|
|
1750
|
-
except Exception:
|
|
1751
|
-
pass
|
|
1752
|
-
else:
|
|
1753
|
-
print("Invalid file number.")
|
|
1754
|
-
except ValueError:
|
|
1755
|
-
print("Invalid input.")
|
|
1756
|
-
else:
|
|
1757
|
-
# Single file mode
|
|
1758
|
-
print("\nEfficiency color palettes:")
|
|
1759
|
-
print(" 1. Blues: #1f77b4, #1976d2, #1565c0, #0d47a1, #01579b")
|
|
1760
|
-
print(" 2. Cyans/Teals: #17becf, #00acc1, #0097a7, #00838f, #006064")
|
|
1761
|
-
print(" 3. Purples/Indigos: #9467bd, #5e35b1, #512da8, #4527a0, #311b92")
|
|
1762
|
-
print(" 4. Deep blues: #2196f3, #1e88e5, #1976d2, #1565c0, #0d47a1")
|
|
1763
|
-
print(" 5. Dark cyans/purples: #0097a7, #00838f, #006064, #5e35b1, #4527a0")
|
|
1764
|
-
val = input("Enter color (hex/name), palette number (1-5), or 'r' for random (q=cancel): ").strip()
|
|
1765
|
-
if not val or val.lower() == 'q':
|
|
1766
|
-
continue
|
|
1767
|
-
if val.lower() == 'r':
|
|
1768
|
-
# Use Plasma colormap
|
|
1769
|
-
import matplotlib.cm as cm
|
|
1770
|
-
import matplotlib.colors as mcolors
|
|
1771
|
-
plasma = cm.get_cmap('plasma', 10)
|
|
1772
|
-
col = mcolors.rgb2hex(plasma(_random.random())[:3])
|
|
1773
|
-
elif val in ['1', '2', '3', '4', '5']:
|
|
1774
|
-
# Use selected palette
|
|
1775
|
-
efficiency_palettes = [
|
|
1776
|
-
['#1f77b4', '#1976d2', '#1565c0', '#0d47a1', '#01579b'],
|
|
1777
|
-
['#17becf', '#00acc1', '#0097a7', '#00838f', '#006064'],
|
|
1778
|
-
['#9467bd', '#5e35b1', '#512da8', '#4527a0', '#311b92'],
|
|
1779
|
-
['#2196f3', '#1e88e5', '#1976d2', '#1565c0', '#0d47a1'],
|
|
1780
|
-
['#0097a7', '#00838f', '#006064', '#5e35b1', '#4527a0']
|
|
1781
|
-
]
|
|
1782
|
-
palette = efficiency_palettes[int(val) - 1]
|
|
1783
|
-
col = palette[0] # Use first color from palette
|
|
1784
|
-
else:
|
|
1785
|
-
col = val
|
|
1943
|
+
_apply_manual_entries_eff(tokens)
|
|
1944
|
+
if not is_multi_file and getattr(fig, '_cpc_spine_auto', False):
|
|
1786
1945
|
try:
|
|
1787
|
-
sc_eff
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
_set_spine_color('right', col)
|
|
1946
|
+
cur_col = _color_of(sc_eff)
|
|
1947
|
+
if cur_col:
|
|
1948
|
+
_set_spine_color('right', cur_col)
|
|
1791
1949
|
except Exception:
|
|
1792
1950
|
pass
|
|
1793
1951
|
try:
|
|
1794
|
-
_rebuild_legend(ax, ax2, file_data)
|
|
1795
|
-
fig.canvas.draw_idle()
|
|
1952
|
+
_rebuild_legend(ax, ax2, file_data); fig.canvas.draw_idle()
|
|
1796
1953
|
except Exception:
|
|
1797
1954
|
pass
|
|
1798
1955
|
else:
|
|
@@ -1817,7 +1974,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1817
1974
|
auto_status = "ON" if auto_enabled else "OFF"
|
|
1818
1975
|
print(_colorize_inline_commands(f" a : auto (apply capacity curve color to left y-axis, efficiency to right y-axis) [{auto_status}]"))
|
|
1819
1976
|
print("q: back to main menu")
|
|
1820
|
-
line =
|
|
1977
|
+
line = _safe_input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
|
|
1821
1978
|
if not line or line.lower() == 'q':
|
|
1822
1979
|
break
|
|
1823
1980
|
# Handle auto toggle when only one file is loaded
|
|
@@ -1889,9 +2046,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1889
2046
|
|
|
1890
2047
|
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
1891
2048
|
if last_figure_path:
|
|
1892
|
-
fname =
|
|
2049
|
+
fname = _safe_input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
1893
2050
|
else:
|
|
1894
|
-
fname =
|
|
2051
|
+
fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
1895
2052
|
if not fname or fname.lower() == 'q':
|
|
1896
2053
|
_print_menu(); continue
|
|
1897
2054
|
|
|
@@ -1903,7 +2060,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1903
2060
|
if not os.path.exists(last_figure_path):
|
|
1904
2061
|
print(f"Previous export file not found: {last_figure_path}")
|
|
1905
2062
|
_print_menu(); continue
|
|
1906
|
-
yn =
|
|
2063
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
1907
2064
|
if yn != 'y':
|
|
1908
2065
|
_print_menu(); continue
|
|
1909
2066
|
target = last_figure_path
|
|
@@ -1912,7 +2069,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1912
2069
|
idx = int(fname)
|
|
1913
2070
|
if 1 <= idx <= len(files):
|
|
1914
2071
|
name = files[idx-1]
|
|
1915
|
-
yn =
|
|
2072
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
1916
2073
|
if yn != 'y':
|
|
1917
2074
|
_print_menu(); continue
|
|
1918
2075
|
target = file_list[idx-1][1] # Full path from list
|
|
@@ -1929,10 +2086,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1929
2086
|
else:
|
|
1930
2087
|
target = get_organized_path(fname, 'figure', base_path=base_path)
|
|
1931
2088
|
if os.path.exists(target):
|
|
1932
|
-
yn =
|
|
2089
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
1933
2090
|
if yn != 'y':
|
|
1934
2091
|
_print_menu(); continue
|
|
1935
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
|
+
|
|
1936
2100
|
# Remove numbering from legend labels before export
|
|
1937
2101
|
original_labels = {}
|
|
1938
2102
|
if is_multi_file:
|
|
@@ -1997,19 +2161,46 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1997
2161
|
ax2.patch.set_alpha(1.0); ax2.patch.set_facecolor(_ax2_fc)
|
|
1998
2162
|
except Exception:
|
|
1999
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
|
|
2000
2183
|
else:
|
|
2001
2184
|
fig.savefig(target, bbox_inches='tight')
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
|
2013
2204
|
except Exception as e:
|
|
2014
2205
|
print(f"Export failed: {e}")
|
|
2015
2206
|
_print_menu(); continue
|
|
@@ -2091,7 +2282,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2091
2282
|
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
2092
2283
|
else:
|
|
2093
2284
|
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
2094
|
-
choice =
|
|
2285
|
+
choice = _safe_input(prompt).strip()
|
|
2095
2286
|
if not choice or choice.lower() == 'q':
|
|
2096
2287
|
_print_menu(); continue
|
|
2097
2288
|
if choice.lower() == 'o':
|
|
@@ -2102,7 +2293,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2102
2293
|
if not os.path.exists(last_session_path):
|
|
2103
2294
|
print(f"Previous save file not found: {last_session_path}")
|
|
2104
2295
|
_print_menu(); continue
|
|
2105
|
-
yn =
|
|
2296
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2106
2297
|
if yn != 'y':
|
|
2107
2298
|
_print_menu(); continue
|
|
2108
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)
|
|
@@ -2112,7 +2303,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2112
2303
|
idx = int(choice)
|
|
2113
2304
|
if 1 <= idx <= len(files):
|
|
2114
2305
|
name = files[idx-1]
|
|
2115
|
-
yn =
|
|
2306
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
2116
2307
|
if yn != 'y':
|
|
2117
2308
|
_print_menu(); continue
|
|
2118
2309
|
target = os.path.join(folder, name)
|
|
@@ -2129,7 +2320,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2129
2320
|
name = name + '.pkl'
|
|
2130
2321
|
target = name if os.path.isabs(name) else os.path.join(folder, name)
|
|
2131
2322
|
if os.path.exists(target):
|
|
2132
|
-
yn =
|
|
2323
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2133
2324
|
if yn != 'y':
|
|
2134
2325
|
_print_menu(); continue
|
|
2135
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)
|
|
@@ -2259,9 +2450,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2259
2450
|
|
|
2260
2451
|
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
2261
2452
|
if last_style_path:
|
|
2262
|
-
sub =
|
|
2453
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
2263
2454
|
else:
|
|
2264
|
-
sub =
|
|
2455
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
2265
2456
|
if sub == 'q':
|
|
2266
2457
|
break
|
|
2267
2458
|
if sub == 'r' or sub == '':
|
|
@@ -2274,7 +2465,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2274
2465
|
if not os.path.exists(last_style_path):
|
|
2275
2466
|
print(f"Previous export file not found: {last_style_path}")
|
|
2276
2467
|
continue
|
|
2277
|
-
yn =
|
|
2468
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2278
2469
|
if yn != 'y':
|
|
2279
2470
|
continue
|
|
2280
2471
|
# Rebuild config based on current state
|
|
@@ -2300,7 +2491,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2300
2491
|
print("Export options:")
|
|
2301
2492
|
print(" ps = style only (.bps)")
|
|
2302
2493
|
print(" psg = style + geometry (.bpsg)")
|
|
2303
|
-
exp_choice =
|
|
2494
|
+
exp_choice = _safe_input(_colorize_prompt("Export choice (ps/psg, q=cancel): ")).strip().lower()
|
|
2304
2495
|
if not exp_choice or exp_choice == 'q':
|
|
2305
2496
|
print("Style export canceled.")
|
|
2306
2497
|
continue
|
|
@@ -2353,9 +2544,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2353
2544
|
else:
|
|
2354
2545
|
print(f" {i}: {fname}")
|
|
2355
2546
|
if last_style_path:
|
|
2356
|
-
choice =
|
|
2547
|
+
choice = _safe_input("Enter new filename, number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
2357
2548
|
else:
|
|
2358
|
-
choice =
|
|
2549
|
+
choice = _safe_input("Enter new filename or number to overwrite (q=cancel): ").strip()
|
|
2359
2550
|
if not choice or choice.lower() == 'q':
|
|
2360
2551
|
print("Style export canceled.")
|
|
2361
2552
|
continue
|
|
@@ -2367,7 +2558,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2367
2558
|
if not os.path.exists(last_style_path):
|
|
2368
2559
|
print(f"Previous export file not found: {last_style_path}")
|
|
2369
2560
|
continue
|
|
2370
|
-
yn =
|
|
2561
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2371
2562
|
if yn != 'y':
|
|
2372
2563
|
continue
|
|
2373
2564
|
# Rebuild config based on current state
|
|
@@ -2393,7 +2584,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2393
2584
|
idx = int(choice)
|
|
2394
2585
|
if 1 <= idx <= len(files):
|
|
2395
2586
|
name = files[idx-1]
|
|
2396
|
-
yn =
|
|
2587
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
2397
2588
|
if yn == 'y':
|
|
2398
2589
|
target = file_list[idx-1][1] # Full path from list
|
|
2399
2590
|
else:
|
|
@@ -2410,7 +2601,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2410
2601
|
else:
|
|
2411
2602
|
target = get_organized_path(name, 'style', base_path=save_base)
|
|
2412
2603
|
if os.path.exists(target):
|
|
2413
|
-
yn =
|
|
2604
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2414
2605
|
if yn != 'y':
|
|
2415
2606
|
target = None
|
|
2416
2607
|
if target:
|
|
@@ -2587,7 +2778,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2587
2778
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2588
2779
|
print(f"Legend is {'ON' if vis else 'off'}; position (inches from center): x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2589
2780
|
while True:
|
|
2590
|
-
sub =
|
|
2781
|
+
sub = _safe_input("Legend: t=toggle, p=set position, q=back: ").strip().lower()
|
|
2591
2782
|
if not sub:
|
|
2592
2783
|
continue
|
|
2593
2784
|
if sub == 'q':
|
|
@@ -2617,7 +2808,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2617
2808
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2618
2809
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2619
2810
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2620
|
-
pos_cmd =
|
|
2811
|
+
pos_cmd = _safe_input("Position: (x y) or x=x only, y=y only, q=back: ").strip().lower()
|
|
2621
2812
|
if not pos_cmd or pos_cmd == 'q':
|
|
2622
2813
|
break
|
|
2623
2814
|
if pos_cmd == 'x':
|
|
@@ -2626,7 +2817,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2626
2817
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2627
2818
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2628
2819
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2629
|
-
val =
|
|
2820
|
+
val = _safe_input(f"Enter new x position (current y: {xy_in[1]:.2f}, q=back): ").strip()
|
|
2630
2821
|
if not val or val.lower() == 'q':
|
|
2631
2822
|
break
|
|
2632
2823
|
try:
|
|
@@ -2649,7 +2840,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2649
2840
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2650
2841
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2651
2842
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2652
|
-
val =
|
|
2843
|
+
val = _safe_input(f"Enter new y position (current x: {xy_in[0]:.2f}, q=back): ").strip()
|
|
2653
2844
|
if not val or val.lower() == 'q':
|
|
2654
2845
|
break
|
|
2655
2846
|
try:
|
|
@@ -2690,11 +2881,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2690
2881
|
pass
|
|
2691
2882
|
_print_menu(); continue
|
|
2692
2883
|
elif key == 'f':
|
|
2693
|
-
sub =
|
|
2884
|
+
sub = _safe_input("Font: f=family, s=size, q=back: ").strip().lower()
|
|
2694
2885
|
if sub == 'q' or not sub:
|
|
2695
2886
|
_print_menu(); continue
|
|
2696
2887
|
if sub == 'f':
|
|
2697
|
-
fam =
|
|
2888
|
+
fam = _safe_input("Enter font family (e.g., Arial, DejaVu Sans): ").strip()
|
|
2698
2889
|
if fam:
|
|
2699
2890
|
try:
|
|
2700
2891
|
push_state("font-family")
|
|
@@ -2750,7 +2941,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2750
2941
|
except Exception:
|
|
2751
2942
|
pass
|
|
2752
2943
|
elif sub == 's':
|
|
2753
|
-
val =
|
|
2944
|
+
val = _safe_input("Enter font size (number): ").strip()
|
|
2754
2945
|
try:
|
|
2755
2946
|
size = float(val)
|
|
2756
2947
|
push_state("font-size")
|
|
@@ -2844,13 +3035,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2844
3035
|
print(f" {_colorize_menu('f : change frame (axes spines) and tick widths')}")
|
|
2845
3036
|
print(f" {_colorize_menu('g : toggle grid lines')}")
|
|
2846
3037
|
print(f" {_colorize_menu('q : return')}")
|
|
2847
|
-
sub =
|
|
3038
|
+
sub = _safe_input(_colorize_prompt("Choose (f/g/q): ")).strip().lower()
|
|
2848
3039
|
if not sub:
|
|
2849
3040
|
continue
|
|
2850
3041
|
if sub == 'q':
|
|
2851
3042
|
break
|
|
2852
3043
|
if sub == 'f':
|
|
2853
|
-
fw_in =
|
|
3044
|
+
fw_in = _safe_input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
|
|
2854
3045
|
if not fw_in or fw_in.lower() == 'q':
|
|
2855
3046
|
print("Canceled.")
|
|
2856
3047
|
continue
|
|
@@ -2922,7 +3113,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2922
3113
|
except Exception:
|
|
2923
3114
|
e_ms = 40
|
|
2924
3115
|
print(f" charge ms={c_ms}, discharge ms={d_ms}, efficiency ms={e_ms}")
|
|
2925
|
-
spec =
|
|
3116
|
+
spec = _safe_input("Set marker size: 'c <ms>', 'd <ms>', 'e <ms>' (q=cancel): ").strip().lower()
|
|
2926
3117
|
if not spec or spec == 'q':
|
|
2927
3118
|
_print_menu(); continue
|
|
2928
3119
|
parts = spec.split()
|
|
@@ -3179,7 +3370,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3179
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."))
|
|
3180
3371
|
print(_colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
|
|
3181
3372
|
while True:
|
|
3182
|
-
cmd =
|
|
3373
|
+
cmd = _safe_input(_colorize_prompt("t> ")).strip().lower()
|
|
3183
3374
|
if not cmd:
|
|
3184
3375
|
continue
|
|
3185
3376
|
if cmd == 'q':
|
|
@@ -3204,7 +3395,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3204
3395
|
# Get current major tick length from axes
|
|
3205
3396
|
current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
|
|
3206
3397
|
print(f"Current major tick length: {current_major}")
|
|
3207
|
-
new_length_str =
|
|
3398
|
+
new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
|
|
3208
3399
|
if not new_length_str:
|
|
3209
3400
|
continue
|
|
3210
3401
|
new_major = float(new_length_str)
|
|
@@ -3279,7 +3470,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3279
3470
|
current_y_px = _px_value('_top_xlabel_manual_offset_y_pts')
|
|
3280
3471
|
current_x_px = _px_value('_top_xlabel_manual_offset_x_pts')
|
|
3281
3472
|
print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
|
|
3282
|
-
sub =
|
|
3473
|
+
sub = _safe_input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
3283
3474
|
if not sub:
|
|
3284
3475
|
continue
|
|
3285
3476
|
if sub == 'q':
|
|
@@ -3316,7 +3507,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3316
3507
|
while True:
|
|
3317
3508
|
current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts')
|
|
3318
3509
|
print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
|
|
3319
|
-
sub =
|
|
3510
|
+
sub = _safe_input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
|
|
3320
3511
|
if not sub:
|
|
3321
3512
|
continue
|
|
3322
3513
|
if sub == 'q':
|
|
@@ -3346,7 +3537,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3346
3537
|
while True:
|
|
3347
3538
|
current_x_px = _px_value('_left_ylabel_manual_offset_x_pts')
|
|
3348
3539
|
print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
|
|
3349
|
-
sub =
|
|
3540
|
+
sub = _safe_input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
3350
3541
|
if not sub:
|
|
3351
3542
|
continue
|
|
3352
3543
|
if sub == 'q':
|
|
@@ -3377,7 +3568,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3377
3568
|
current_x_px = _px_value('_right_ylabel_manual_offset_x_pts')
|
|
3378
3569
|
current_y_px = _px_value('_right_ylabel_manual_offset_y_pts')
|
|
3379
3570
|
print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
|
|
3380
|
-
sub =
|
|
3571
|
+
sub = _safe_input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
|
|
3381
3572
|
if not sub:
|
|
3382
3573
|
continue
|
|
3383
3574
|
if sub == 'q':
|
|
@@ -3415,7 +3606,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3415
3606
|
print(" " + _colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
|
|
3416
3607
|
print(" " + _colorize_menu('r : reset all offsets'))
|
|
3417
3608
|
print(" " + _colorize_menu('q : return'))
|
|
3418
|
-
choice =
|
|
3609
|
+
choice = _safe_input(_colorize_prompt("p> ")).strip().lower()
|
|
3419
3610
|
if not choice:
|
|
3420
3611
|
continue
|
|
3421
3612
|
if choice == 'q':
|
|
@@ -3516,7 +3707,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3516
3707
|
elif key == 'g':
|
|
3517
3708
|
while True:
|
|
3518
3709
|
print("Geometry: p=plot frame, c=canvas, q=back")
|
|
3519
|
-
sub =
|
|
3710
|
+
sub = _safe_input("Geom> ").strip().lower()
|
|
3520
3711
|
if not sub:
|
|
3521
3712
|
continue
|
|
3522
3713
|
if sub == 'q':
|
|
@@ -3542,7 +3733,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3542
3733
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3543
3734
|
while True:
|
|
3544
3735
|
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3545
|
-
sub =
|
|
3736
|
+
sub = _safe_input("Rename> ").strip().lower()
|
|
3546
3737
|
if not sub:
|
|
3547
3738
|
continue
|
|
3548
3739
|
if sub == 'q':
|
|
@@ -3588,7 +3779,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3588
3779
|
break
|
|
3589
3780
|
|
|
3590
3781
|
print(f"Current file name in legend: '{base_name}'")
|
|
3591
|
-
new_name =
|
|
3782
|
+
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3592
3783
|
if new_name and new_name.lower() != 'q':
|
|
3593
3784
|
try:
|
|
3594
3785
|
push_state("rename-legend")
|
|
@@ -3656,7 +3847,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3656
3847
|
# Multi-file mode: show file list and let user select
|
|
3657
3848
|
print("\nAvailable files:")
|
|
3658
3849
|
_print_file_list(file_data, current_file_idx)
|
|
3659
|
-
file_choice =
|
|
3850
|
+
file_choice = _safe_input("Enter file number to rename (q=cancel): ").strip()
|
|
3660
3851
|
if file_choice and file_choice.lower() != 'q':
|
|
3661
3852
|
try:
|
|
3662
3853
|
file_idx = int(file_choice) - 1
|
|
@@ -3695,7 +3886,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3695
3886
|
break
|
|
3696
3887
|
|
|
3697
3888
|
print(f"Current file name in legend: '{base_name}'")
|
|
3698
|
-
new_name =
|
|
3889
|
+
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3699
3890
|
if new_name and new_name.lower() != 'q':
|
|
3700
3891
|
try:
|
|
3701
3892
|
push_state("rename-legend")
|
|
@@ -3766,7 +3957,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3766
3957
|
elif sub == 'x':
|
|
3767
3958
|
current = ax.get_xlabel()
|
|
3768
3959
|
print(f"Current x-axis title: '{current}'")
|
|
3769
|
-
new_title =
|
|
3960
|
+
new_title = _safe_input("Enter new x-axis title (q=cancel): ")
|
|
3770
3961
|
if new_title and new_title.lower() != 'q':
|
|
3771
3962
|
try:
|
|
3772
3963
|
push_state("rename-x")
|
|
@@ -3785,7 +3976,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3785
3976
|
elif sub == 'ly':
|
|
3786
3977
|
current = ax.get_ylabel()
|
|
3787
3978
|
print(f"Current left y-axis title: '{current}'")
|
|
3788
|
-
new_title =
|
|
3979
|
+
new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
|
|
3789
3980
|
if new_title and new_title.lower() != 'q':
|
|
3790
3981
|
try:
|
|
3791
3982
|
push_state("rename-ly")
|
|
@@ -3799,7 +3990,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3799
3990
|
elif sub == 'ry':
|
|
3800
3991
|
current = ax2.get_ylabel()
|
|
3801
3992
|
print(f"Current right y-axis title: '{current}'")
|
|
3802
|
-
new_title =
|
|
3993
|
+
new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
|
|
3803
3994
|
if new_title and new_title.lower() != 'q':
|
|
3804
3995
|
try:
|
|
3805
3996
|
push_state("rename-ry")
|
|
@@ -3819,7 +4010,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3819
4010
|
while True:
|
|
3820
4011
|
current_xlim = ax.get_xlim()
|
|
3821
4012
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3822
|
-
rng =
|
|
4013
|
+
rng = _safe_input("Enter x-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3823
4014
|
if not rng or rng.lower() == 'q':
|
|
3824
4015
|
break
|
|
3825
4016
|
if rng.lower() == 'w':
|
|
@@ -3827,7 +4018,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3827
4018
|
while True:
|
|
3828
4019
|
current_xlim = ax.get_xlim()
|
|
3829
4020
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3830
|
-
val =
|
|
4021
|
+
val = _safe_input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
|
|
3831
4022
|
if not val or val.lower() == 'q':
|
|
3832
4023
|
break
|
|
3833
4024
|
try:
|
|
@@ -3854,7 +4045,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3854
4045
|
while True:
|
|
3855
4046
|
current_xlim = ax.get_xlim()
|
|
3856
4047
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3857
|
-
val =
|
|
4048
|
+
val = _safe_input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
|
|
3858
4049
|
if not val or val.lower() == 'q':
|
|
3859
4050
|
break
|
|
3860
4051
|
try:
|
|
@@ -3917,7 +4108,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3917
4108
|
elif key == 'y':
|
|
3918
4109
|
while True:
|
|
3919
4110
|
print("Y-ranges: ly=left axis, ry=right axis, q=back")
|
|
3920
|
-
ycmd =
|
|
4111
|
+
ycmd = _safe_input("Y> ").strip().lower()
|
|
3921
4112
|
if not ycmd:
|
|
3922
4113
|
continue
|
|
3923
4114
|
if ycmd == 'q':
|
|
@@ -3926,7 +4117,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3926
4117
|
while True:
|
|
3927
4118
|
current_ylim = ax.get_ylim()
|
|
3928
4119
|
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3929
|
-
rng =
|
|
4120
|
+
rng = _safe_input("Enter left y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3930
4121
|
if not rng or rng.lower() == 'q':
|
|
3931
4122
|
break
|
|
3932
4123
|
if rng.lower() == 'w':
|
|
@@ -3934,7 +4125,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3934
4125
|
while True:
|
|
3935
4126
|
current_ylim = ax.get_ylim()
|
|
3936
4127
|
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3937
|
-
val =
|
|
4128
|
+
val = _safe_input(f"Enter new upper left Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
3938
4129
|
if not val or val.lower() == 'q':
|
|
3939
4130
|
break
|
|
3940
4131
|
try:
|
|
@@ -3961,7 +4152,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3961
4152
|
while True:
|
|
3962
4153
|
current_ylim = ax.get_ylim()
|
|
3963
4154
|
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3964
|
-
val =
|
|
4155
|
+
val = _safe_input(f"Enter new lower left Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3965
4156
|
if not val or val.lower() == 'q':
|
|
3966
4157
|
break
|
|
3967
4158
|
try:
|
|
@@ -4029,7 +4220,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4029
4220
|
break
|
|
4030
4221
|
current_ylim = ax2.get_ylim()
|
|
4031
4222
|
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4032
|
-
rng =
|
|
4223
|
+
rng = _safe_input("Enter right y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
4033
4224
|
if not rng or rng.lower() == 'q':
|
|
4034
4225
|
break
|
|
4035
4226
|
if rng.lower() == 'w':
|
|
@@ -4037,7 +4228,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4037
4228
|
while True:
|
|
4038
4229
|
current_ylim = ax2.get_ylim()
|
|
4039
4230
|
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4040
|
-
val =
|
|
4231
|
+
val = _safe_input(f"Enter new upper right Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
4041
4232
|
if not val or val.lower() == 'q':
|
|
4042
4233
|
break
|
|
4043
4234
|
try:
|
|
@@ -4064,7 +4255,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4064
4255
|
while True:
|
|
4065
4256
|
current_ylim = ax2.get_ylim()
|
|
4066
4257
|
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4067
|
-
val =
|
|
4258
|
+
val = _safe_input(f"Enter new lower right Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
4068
4259
|
if not val or val.lower() == 'q':
|
|
4069
4260
|
break
|
|
4070
4261
|
try:
|