batplot 1.7.25__py3-none-any.whl → 1.7.27__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 +515 -320
- batplot/electrochem_interactive.py +24 -2
- batplot/interactive.py +12 -4
- batplot/operando.py +3 -3
- batplot/operando_ec_interactive.py +22 -1
- batplot/session.py +29 -3
- batplot/style.py +4 -0
- batplot/ui.py +13 -29
- batplot/utils.py +48 -0
- {batplot-1.7.25.dist-info → batplot-1.7.27.dist-info}/METADATA +1 -1
- {batplot-1.7.25.dist-info → batplot-1.7.27.dist-info}/RECORD +18 -18
- {batplot-1.7.25.dist-info → batplot-1.7.27.dist-info}/WHEEL +0 -0
- {batplot-1.7.25.dist-info → batplot-1.7.27.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.25.dist-info → batplot-1.7.27.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.25.dist-info → batplot-1.7.27.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:
|
|
1710
1921
|
try:
|
|
1711
|
-
|
|
1712
|
-
|
|
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)
|
|
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,21 @@ 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
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
2094
|
+
from .utils import ensure_exact_case_filename
|
|
2095
|
+
target = ensure_exact_case_filename(target)
|
|
2096
|
+
|
|
2097
|
+
# Save current legend position before export (savefig can change layout)
|
|
2098
|
+
saved_legend_pos = None
|
|
2099
|
+
try:
|
|
2100
|
+
saved_legend_pos = getattr(fig, '_cpc_legend_xy_in', None)
|
|
2101
|
+
except Exception:
|
|
2102
|
+
pass
|
|
2103
|
+
|
|
1936
2104
|
# Remove numbering from legend labels before export
|
|
1937
2105
|
original_labels = {}
|
|
1938
2106
|
if is_multi_file:
|
|
@@ -1997,19 +2165,46 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1997
2165
|
ax2.patch.set_alpha(1.0); ax2.patch.set_facecolor(_ax2_fc)
|
|
1998
2166
|
except Exception:
|
|
1999
2167
|
pass
|
|
2168
|
+
print(f"Exported figure to {target}")
|
|
2169
|
+
fig._last_figure_export_path = target
|
|
2170
|
+
|
|
2171
|
+
# Restore original labels and legend position
|
|
2172
|
+
if is_multi_file and original_labels:
|
|
2173
|
+
try:
|
|
2174
|
+
for artist, label in original_labels.items():
|
|
2175
|
+
artist.set_label(label)
|
|
2176
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2177
|
+
except Exception:
|
|
2178
|
+
pass
|
|
2179
|
+
# Restore legend position after savefig (which may have changed layout)
|
|
2180
|
+
if saved_legend_pos is not None:
|
|
2181
|
+
try:
|
|
2182
|
+
fig._cpc_legend_xy_in = saved_legend_pos
|
|
2183
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2184
|
+
fig.canvas.draw_idle()
|
|
2185
|
+
except Exception:
|
|
2186
|
+
pass
|
|
2000
2187
|
else:
|
|
2001
2188
|
fig.savefig(target, bbox_inches='tight')
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2189
|
+
print(f"Exported figure to {target}")
|
|
2190
|
+
fig._last_figure_export_path = target
|
|
2191
|
+
|
|
2192
|
+
# Restore original labels and legend position
|
|
2193
|
+
if is_multi_file and original_labels:
|
|
2194
|
+
try:
|
|
2195
|
+
for artist, label in original_labels.items():
|
|
2196
|
+
artist.set_label(label)
|
|
2197
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2198
|
+
except Exception:
|
|
2199
|
+
pass
|
|
2200
|
+
# Restore legend position after savefig (which may have changed layout)
|
|
2201
|
+
if saved_legend_pos is not None:
|
|
2202
|
+
try:
|
|
2203
|
+
fig._cpc_legend_xy_in = saved_legend_pos
|
|
2204
|
+
_rebuild_legend(ax, ax2, file_data)
|
|
2205
|
+
fig.canvas.draw_idle()
|
|
2206
|
+
except Exception:
|
|
2207
|
+
pass
|
|
2013
2208
|
except Exception as e:
|
|
2014
2209
|
print(f"Export failed: {e}")
|
|
2015
2210
|
_print_menu(); continue
|
|
@@ -2091,7 +2286,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2091
2286
|
prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
|
|
2092
2287
|
else:
|
|
2093
2288
|
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
2094
|
-
choice =
|
|
2289
|
+
choice = _safe_input(prompt).strip()
|
|
2095
2290
|
if not choice or choice.lower() == 'q':
|
|
2096
2291
|
_print_menu(); continue
|
|
2097
2292
|
if choice.lower() == 'o':
|
|
@@ -2102,7 +2297,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2102
2297
|
if not os.path.exists(last_session_path):
|
|
2103
2298
|
print(f"Previous save file not found: {last_session_path}")
|
|
2104
2299
|
_print_menu(); continue
|
|
2105
|
-
yn =
|
|
2300
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2106
2301
|
if yn != 'y':
|
|
2107
2302
|
_print_menu(); continue
|
|
2108
2303
|
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 +2307,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2112
2307
|
idx = int(choice)
|
|
2113
2308
|
if 1 <= idx <= len(files):
|
|
2114
2309
|
name = files[idx-1]
|
|
2115
|
-
yn =
|
|
2310
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
2116
2311
|
if yn != 'y':
|
|
2117
2312
|
_print_menu(); continue
|
|
2118
2313
|
target = os.path.join(folder, name)
|
|
@@ -2129,7 +2324,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2129
2324
|
name = name + '.pkl'
|
|
2130
2325
|
target = name if os.path.isabs(name) else os.path.join(folder, name)
|
|
2131
2326
|
if os.path.exists(target):
|
|
2132
|
-
yn =
|
|
2327
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2133
2328
|
if yn != 'y':
|
|
2134
2329
|
_print_menu(); continue
|
|
2135
2330
|
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 +2454,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2259
2454
|
|
|
2260
2455
|
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
2261
2456
|
if last_style_path:
|
|
2262
|
-
sub =
|
|
2457
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
2263
2458
|
else:
|
|
2264
|
-
sub =
|
|
2459
|
+
sub = _safe_input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
2265
2460
|
if sub == 'q':
|
|
2266
2461
|
break
|
|
2267
2462
|
if sub == 'r' or sub == '':
|
|
@@ -2274,7 +2469,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2274
2469
|
if not os.path.exists(last_style_path):
|
|
2275
2470
|
print(f"Previous export file not found: {last_style_path}")
|
|
2276
2471
|
continue
|
|
2277
|
-
yn =
|
|
2472
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2278
2473
|
if yn != 'y':
|
|
2279
2474
|
continue
|
|
2280
2475
|
# Rebuild config based on current state
|
|
@@ -2300,7 +2495,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2300
2495
|
print("Export options:")
|
|
2301
2496
|
print(" ps = style only (.bps)")
|
|
2302
2497
|
print(" psg = style + geometry (.bpsg)")
|
|
2303
|
-
exp_choice =
|
|
2498
|
+
exp_choice = _safe_input(_colorize_prompt("Export choice (ps/psg, q=cancel): ")).strip().lower()
|
|
2304
2499
|
if not exp_choice or exp_choice == 'q':
|
|
2305
2500
|
print("Style export canceled.")
|
|
2306
2501
|
continue
|
|
@@ -2353,9 +2548,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2353
2548
|
else:
|
|
2354
2549
|
print(f" {i}: {fname}")
|
|
2355
2550
|
if last_style_path:
|
|
2356
|
-
choice =
|
|
2551
|
+
choice = _safe_input("Enter new filename, number to overwrite, or o to overwrite last (q=cancel): ").strip()
|
|
2357
2552
|
else:
|
|
2358
|
-
choice =
|
|
2553
|
+
choice = _safe_input("Enter new filename or number to overwrite (q=cancel): ").strip()
|
|
2359
2554
|
if not choice or choice.lower() == 'q':
|
|
2360
2555
|
print("Style export canceled.")
|
|
2361
2556
|
continue
|
|
@@ -2367,7 +2562,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2367
2562
|
if not os.path.exists(last_style_path):
|
|
2368
2563
|
print(f"Previous export file not found: {last_style_path}")
|
|
2369
2564
|
continue
|
|
2370
|
-
yn =
|
|
2565
|
+
yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
|
|
2371
2566
|
if yn != 'y':
|
|
2372
2567
|
continue
|
|
2373
2568
|
# Rebuild config based on current state
|
|
@@ -2393,7 +2588,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2393
2588
|
idx = int(choice)
|
|
2394
2589
|
if 1 <= idx <= len(files):
|
|
2395
2590
|
name = files[idx-1]
|
|
2396
|
-
yn =
|
|
2591
|
+
yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
|
|
2397
2592
|
if yn == 'y':
|
|
2398
2593
|
target = file_list[idx-1][1] # Full path from list
|
|
2399
2594
|
else:
|
|
@@ -2410,7 +2605,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2410
2605
|
else:
|
|
2411
2606
|
target = get_organized_path(name, 'style', base_path=save_base)
|
|
2412
2607
|
if os.path.exists(target):
|
|
2413
|
-
yn =
|
|
2608
|
+
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2414
2609
|
if yn != 'y':
|
|
2415
2610
|
target = None
|
|
2416
2611
|
if target:
|
|
@@ -2587,7 +2782,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2587
2782
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2588
2783
|
print(f"Legend is {'ON' if vis else 'off'}; position (inches from center): x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2589
2784
|
while True:
|
|
2590
|
-
sub =
|
|
2785
|
+
sub = _safe_input("Legend: t=toggle, p=set position, q=back: ").strip().lower()
|
|
2591
2786
|
if not sub:
|
|
2592
2787
|
continue
|
|
2593
2788
|
if sub == 'q':
|
|
@@ -2617,7 +2812,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2617
2812
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2618
2813
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2619
2814
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2620
|
-
pos_cmd =
|
|
2815
|
+
pos_cmd = _safe_input("Position: (x y) or x=x only, y=y only, q=back: ").strip().lower()
|
|
2621
2816
|
if not pos_cmd or pos_cmd == 'q':
|
|
2622
2817
|
break
|
|
2623
2818
|
if pos_cmd == 'x':
|
|
@@ -2626,7 +2821,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2626
2821
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2627
2822
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2628
2823
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2629
|
-
val =
|
|
2824
|
+
val = _safe_input(f"Enter new x position (current y: {xy_in[1]:.2f}, q=back): ").strip()
|
|
2630
2825
|
if not val or val.lower() == 'q':
|
|
2631
2826
|
break
|
|
2632
2827
|
try:
|
|
@@ -2649,7 +2844,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2649
2844
|
xy_in = getattr(fig, '_cpc_legend_xy_in', (0.0, 0.0))
|
|
2650
2845
|
xy_in = _sanitize_legend_offset(xy_in) or (0.0, 0.0)
|
|
2651
2846
|
print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
|
|
2652
|
-
val =
|
|
2847
|
+
val = _safe_input(f"Enter new y position (current x: {xy_in[0]:.2f}, q=back): ").strip()
|
|
2653
2848
|
if not val or val.lower() == 'q':
|
|
2654
2849
|
break
|
|
2655
2850
|
try:
|
|
@@ -2690,11 +2885,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2690
2885
|
pass
|
|
2691
2886
|
_print_menu(); continue
|
|
2692
2887
|
elif key == 'f':
|
|
2693
|
-
sub =
|
|
2888
|
+
sub = _safe_input("Font: f=family, s=size, q=back: ").strip().lower()
|
|
2694
2889
|
if sub == 'q' or not sub:
|
|
2695
2890
|
_print_menu(); continue
|
|
2696
2891
|
if sub == 'f':
|
|
2697
|
-
fam =
|
|
2892
|
+
fam = _safe_input("Enter font family (e.g., Arial, DejaVu Sans): ").strip()
|
|
2698
2893
|
if fam:
|
|
2699
2894
|
try:
|
|
2700
2895
|
push_state("font-family")
|
|
@@ -2750,7 +2945,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2750
2945
|
except Exception:
|
|
2751
2946
|
pass
|
|
2752
2947
|
elif sub == 's':
|
|
2753
|
-
val =
|
|
2948
|
+
val = _safe_input("Enter font size (number): ").strip()
|
|
2754
2949
|
try:
|
|
2755
2950
|
size = float(val)
|
|
2756
2951
|
push_state("font-size")
|
|
@@ -2844,13 +3039,13 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2844
3039
|
print(f" {_colorize_menu('f : change frame (axes spines) and tick widths')}")
|
|
2845
3040
|
print(f" {_colorize_menu('g : toggle grid lines')}")
|
|
2846
3041
|
print(f" {_colorize_menu('q : return')}")
|
|
2847
|
-
sub =
|
|
3042
|
+
sub = _safe_input(_colorize_prompt("Choose (f/g/q): ")).strip().lower()
|
|
2848
3043
|
if not sub:
|
|
2849
3044
|
continue
|
|
2850
3045
|
if sub == 'q':
|
|
2851
3046
|
break
|
|
2852
3047
|
if sub == 'f':
|
|
2853
|
-
fw_in =
|
|
3048
|
+
fw_in = _safe_input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
|
|
2854
3049
|
if not fw_in or fw_in.lower() == 'q':
|
|
2855
3050
|
print("Canceled.")
|
|
2856
3051
|
continue
|
|
@@ -2922,7 +3117,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2922
3117
|
except Exception:
|
|
2923
3118
|
e_ms = 40
|
|
2924
3119
|
print(f" charge ms={c_ms}, discharge ms={d_ms}, efficiency ms={e_ms}")
|
|
2925
|
-
spec =
|
|
3120
|
+
spec = _safe_input("Set marker size: 'c <ms>', 'd <ms>', 'e <ms>' (q=cancel): ").strip().lower()
|
|
2926
3121
|
if not spec or spec == 'q':
|
|
2927
3122
|
_print_menu(); continue
|
|
2928
3123
|
parts = spec.split()
|
|
@@ -3179,7 +3374,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3179
3374
|
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
3375
|
print(_colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
|
|
3181
3376
|
while True:
|
|
3182
|
-
cmd =
|
|
3377
|
+
cmd = _safe_input(_colorize_prompt("t> ")).strip().lower()
|
|
3183
3378
|
if not cmd:
|
|
3184
3379
|
continue
|
|
3185
3380
|
if cmd == 'q':
|
|
@@ -3204,7 +3399,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3204
3399
|
# Get current major tick length from axes
|
|
3205
3400
|
current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
|
|
3206
3401
|
print(f"Current major tick length: {current_major}")
|
|
3207
|
-
new_length_str =
|
|
3402
|
+
new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
|
|
3208
3403
|
if not new_length_str:
|
|
3209
3404
|
continue
|
|
3210
3405
|
new_major = float(new_length_str)
|
|
@@ -3279,7 +3474,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3279
3474
|
current_y_px = _px_value('_top_xlabel_manual_offset_y_pts')
|
|
3280
3475
|
current_x_px = _px_value('_top_xlabel_manual_offset_x_pts')
|
|
3281
3476
|
print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
|
|
3282
|
-
sub =
|
|
3477
|
+
sub = _safe_input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
3283
3478
|
if not sub:
|
|
3284
3479
|
continue
|
|
3285
3480
|
if sub == 'q':
|
|
@@ -3316,7 +3511,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3316
3511
|
while True:
|
|
3317
3512
|
current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts')
|
|
3318
3513
|
print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
|
|
3319
|
-
sub =
|
|
3514
|
+
sub = _safe_input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
|
|
3320
3515
|
if not sub:
|
|
3321
3516
|
continue
|
|
3322
3517
|
if sub == 'q':
|
|
@@ -3346,7 +3541,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3346
3541
|
while True:
|
|
3347
3542
|
current_x_px = _px_value('_left_ylabel_manual_offset_x_pts')
|
|
3348
3543
|
print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
|
|
3349
|
-
sub =
|
|
3544
|
+
sub = _safe_input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
|
|
3350
3545
|
if not sub:
|
|
3351
3546
|
continue
|
|
3352
3547
|
if sub == 'q':
|
|
@@ -3377,7 +3572,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3377
3572
|
current_x_px = _px_value('_right_ylabel_manual_offset_x_pts')
|
|
3378
3573
|
current_y_px = _px_value('_right_ylabel_manual_offset_y_pts')
|
|
3379
3574
|
print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
|
|
3380
|
-
sub =
|
|
3575
|
+
sub = _safe_input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
|
|
3381
3576
|
if not sub:
|
|
3382
3577
|
continue
|
|
3383
3578
|
if sub == 'q':
|
|
@@ -3415,7 +3610,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3415
3610
|
print(" " + _colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
|
|
3416
3611
|
print(" " + _colorize_menu('r : reset all offsets'))
|
|
3417
3612
|
print(" " + _colorize_menu('q : return'))
|
|
3418
|
-
choice =
|
|
3613
|
+
choice = _safe_input(_colorize_prompt("p> ")).strip().lower()
|
|
3419
3614
|
if not choice:
|
|
3420
3615
|
continue
|
|
3421
3616
|
if choice == 'q':
|
|
@@ -3516,7 +3711,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3516
3711
|
elif key == 'g':
|
|
3517
3712
|
while True:
|
|
3518
3713
|
print("Geometry: p=plot frame, c=canvas, q=back")
|
|
3519
|
-
sub =
|
|
3714
|
+
sub = _safe_input("Geom> ").strip().lower()
|
|
3520
3715
|
if not sub:
|
|
3521
3716
|
continue
|
|
3522
3717
|
if sub == 'q':
|
|
@@ -3542,7 +3737,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3542
3737
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3543
3738
|
while True:
|
|
3544
3739
|
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3545
|
-
sub =
|
|
3740
|
+
sub = _safe_input("Rename> ").strip().lower()
|
|
3546
3741
|
if not sub:
|
|
3547
3742
|
continue
|
|
3548
3743
|
if sub == 'q':
|
|
@@ -3588,7 +3783,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3588
3783
|
break
|
|
3589
3784
|
|
|
3590
3785
|
print(f"Current file name in legend: '{base_name}'")
|
|
3591
|
-
new_name =
|
|
3786
|
+
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3592
3787
|
if new_name and new_name.lower() != 'q':
|
|
3593
3788
|
try:
|
|
3594
3789
|
push_state("rename-legend")
|
|
@@ -3656,7 +3851,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3656
3851
|
# Multi-file mode: show file list and let user select
|
|
3657
3852
|
print("\nAvailable files:")
|
|
3658
3853
|
_print_file_list(file_data, current_file_idx)
|
|
3659
|
-
file_choice =
|
|
3854
|
+
file_choice = _safe_input("Enter file number to rename (q=cancel): ").strip()
|
|
3660
3855
|
if file_choice and file_choice.lower() != 'q':
|
|
3661
3856
|
try:
|
|
3662
3857
|
file_idx = int(file_choice) - 1
|
|
@@ -3695,7 +3890,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3695
3890
|
break
|
|
3696
3891
|
|
|
3697
3892
|
print(f"Current file name in legend: '{base_name}'")
|
|
3698
|
-
new_name =
|
|
3893
|
+
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3699
3894
|
if new_name and new_name.lower() != 'q':
|
|
3700
3895
|
try:
|
|
3701
3896
|
push_state("rename-legend")
|
|
@@ -3766,7 +3961,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3766
3961
|
elif sub == 'x':
|
|
3767
3962
|
current = ax.get_xlabel()
|
|
3768
3963
|
print(f"Current x-axis title: '{current}'")
|
|
3769
|
-
new_title =
|
|
3964
|
+
new_title = _safe_input("Enter new x-axis title (q=cancel): ")
|
|
3770
3965
|
if new_title and new_title.lower() != 'q':
|
|
3771
3966
|
try:
|
|
3772
3967
|
push_state("rename-x")
|
|
@@ -3785,7 +3980,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3785
3980
|
elif sub == 'ly':
|
|
3786
3981
|
current = ax.get_ylabel()
|
|
3787
3982
|
print(f"Current left y-axis title: '{current}'")
|
|
3788
|
-
new_title =
|
|
3983
|
+
new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
|
|
3789
3984
|
if new_title and new_title.lower() != 'q':
|
|
3790
3985
|
try:
|
|
3791
3986
|
push_state("rename-ly")
|
|
@@ -3799,7 +3994,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3799
3994
|
elif sub == 'ry':
|
|
3800
3995
|
current = ax2.get_ylabel()
|
|
3801
3996
|
print(f"Current right y-axis title: '{current}'")
|
|
3802
|
-
new_title =
|
|
3997
|
+
new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
|
|
3803
3998
|
if new_title and new_title.lower() != 'q':
|
|
3804
3999
|
try:
|
|
3805
4000
|
push_state("rename-ry")
|
|
@@ -3819,7 +4014,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3819
4014
|
while True:
|
|
3820
4015
|
current_xlim = ax.get_xlim()
|
|
3821
4016
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3822
|
-
rng =
|
|
4017
|
+
rng = _safe_input("Enter x-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3823
4018
|
if not rng or rng.lower() == 'q':
|
|
3824
4019
|
break
|
|
3825
4020
|
if rng.lower() == 'w':
|
|
@@ -3827,7 +4022,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3827
4022
|
while True:
|
|
3828
4023
|
current_xlim = ax.get_xlim()
|
|
3829
4024
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3830
|
-
val =
|
|
4025
|
+
val = _safe_input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
|
|
3831
4026
|
if not val or val.lower() == 'q':
|
|
3832
4027
|
break
|
|
3833
4028
|
try:
|
|
@@ -3854,7 +4049,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3854
4049
|
while True:
|
|
3855
4050
|
current_xlim = ax.get_xlim()
|
|
3856
4051
|
print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
|
|
3857
|
-
val =
|
|
4052
|
+
val = _safe_input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
|
|
3858
4053
|
if not val or val.lower() == 'q':
|
|
3859
4054
|
break
|
|
3860
4055
|
try:
|
|
@@ -3917,7 +4112,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3917
4112
|
elif key == 'y':
|
|
3918
4113
|
while True:
|
|
3919
4114
|
print("Y-ranges: ly=left axis, ry=right axis, q=back")
|
|
3920
|
-
ycmd =
|
|
4115
|
+
ycmd = _safe_input("Y> ").strip().lower()
|
|
3921
4116
|
if not ycmd:
|
|
3922
4117
|
continue
|
|
3923
4118
|
if ycmd == 'q':
|
|
@@ -3926,7 +4121,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3926
4121
|
while True:
|
|
3927
4122
|
current_ylim = ax.get_ylim()
|
|
3928
4123
|
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3929
|
-
rng =
|
|
4124
|
+
rng = _safe_input("Enter left y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
3930
4125
|
if not rng or rng.lower() == 'q':
|
|
3931
4126
|
break
|
|
3932
4127
|
if rng.lower() == 'w':
|
|
@@ -3934,7 +4129,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3934
4129
|
while True:
|
|
3935
4130
|
current_ylim = ax.get_ylim()
|
|
3936
4131
|
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3937
|
-
val =
|
|
4132
|
+
val = _safe_input(f"Enter new upper left Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
3938
4133
|
if not val or val.lower() == 'q':
|
|
3939
4134
|
break
|
|
3940
4135
|
try:
|
|
@@ -3961,7 +4156,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3961
4156
|
while True:
|
|
3962
4157
|
current_ylim = ax.get_ylim()
|
|
3963
4158
|
print(f"Current left Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
3964
|
-
val =
|
|
4159
|
+
val = _safe_input(f"Enter new lower left Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
3965
4160
|
if not val or val.lower() == 'q':
|
|
3966
4161
|
break
|
|
3967
4162
|
try:
|
|
@@ -4029,7 +4224,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4029
4224
|
break
|
|
4030
4225
|
current_ylim = ax2.get_ylim()
|
|
4031
4226
|
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4032
|
-
rng =
|
|
4227
|
+
rng = _safe_input("Enter right y-range: min max, w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
|
|
4033
4228
|
if not rng or rng.lower() == 'q':
|
|
4034
4229
|
break
|
|
4035
4230
|
if rng.lower() == 'w':
|
|
@@ -4037,7 +4232,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4037
4232
|
while True:
|
|
4038
4233
|
current_ylim = ax2.get_ylim()
|
|
4039
4234
|
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4040
|
-
val =
|
|
4235
|
+
val = _safe_input(f"Enter new upper right Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
|
|
4041
4236
|
if not val or val.lower() == 'q':
|
|
4042
4237
|
break
|
|
4043
4238
|
try:
|
|
@@ -4064,7 +4259,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4064
4259
|
while True:
|
|
4065
4260
|
current_ylim = ax2.get_ylim()
|
|
4066
4261
|
print(f"Current right Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
|
|
4067
|
-
val =
|
|
4262
|
+
val = _safe_input(f"Enter new lower right Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
|
|
4068
4263
|
if not val or val.lower() == 'q':
|
|
4069
4264
|
break
|
|
4070
4265
|
try:
|