batplot 1.8.4__py3-none-any.whl → 1.8.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- batplot/__init__.py +1 -1
- batplot/args.py +22 -4
- batplot/batch.py +12 -0
- batplot/batplot.py +340 -126
- batplot/converters.py +170 -122
- batplot/cpc_interactive.py +319 -161
- batplot/data/USER_MANUAL.md +49 -0
- batplot/electrochem_interactive.py +120 -80
- batplot/interactive.py +1763 -75
- batplot/modes.py +12 -11
- batplot/operando.py +22 -0
- batplot/operando_ec_interactive.py +390 -16
- batplot/session.py +85 -9
- batplot/style.py +198 -21
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/METADATA +1 -1
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/RECORD +20 -20
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/WHEEL +1 -1
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/top_level.txt +0 -0
batplot/cpc_interactive.py
CHANGED
|
@@ -96,7 +96,7 @@ from .color_utils import resolve_color_token, color_block, palette_preview, mana
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _legend_no_frame(ax, *args, **kwargs):
|
|
99
|
-
# Compact legend defaults
|
|
99
|
+
# Compact legend defaults
|
|
100
100
|
kwargs.setdefault('frameon', False)
|
|
101
101
|
kwargs.setdefault('handlelength', 1.0)
|
|
102
102
|
kwargs.setdefault('handletextpad', 0.35)
|
|
@@ -104,8 +104,8 @@ def _legend_no_frame(ax, *args, **kwargs):
|
|
|
104
104
|
kwargs.setdefault('borderaxespad', 0.5)
|
|
105
105
|
kwargs.setdefault('borderpad', 0.3)
|
|
106
106
|
kwargs.setdefault('columnspacing', 0.6)
|
|
107
|
-
#
|
|
108
|
-
|
|
107
|
+
# Don't use labelcolor='linecolor' by default as it causes issues
|
|
108
|
+
# with scatter plots that have facecolor='none' (hollow markers)
|
|
109
109
|
leg = ax.legend(*args, **kwargs)
|
|
110
110
|
if leg is not None:
|
|
111
111
|
try:
|
|
@@ -267,7 +267,7 @@ def _generate_similar_color(base_color):
|
|
|
267
267
|
return base_color
|
|
268
268
|
|
|
269
269
|
|
|
270
|
-
def _print_menu():
|
|
270
|
+
def _print_menu(fig=None):
|
|
271
271
|
col1 = [
|
|
272
272
|
" f: font",
|
|
273
273
|
" l: line",
|
|
@@ -284,6 +284,7 @@ def _print_menu():
|
|
|
284
284
|
"r: rename",
|
|
285
285
|
"x: x range",
|
|
286
286
|
"y: y ranges",
|
|
287
|
+
"ie: invert efficiency",
|
|
287
288
|
]
|
|
288
289
|
col3 = [
|
|
289
290
|
"p: print(export) style/geom",
|
|
@@ -293,6 +294,30 @@ def _print_menu():
|
|
|
293
294
|
"b: undo",
|
|
294
295
|
"q: quit",
|
|
295
296
|
]
|
|
297
|
+
|
|
298
|
+
# Conditional shortcuts that depend on figure state
|
|
299
|
+
# 1) Hide multi-file-only commands (v) when we know we're in single-file mode
|
|
300
|
+
if fig is not None:
|
|
301
|
+
try:
|
|
302
|
+
is_multi = bool(getattr(fig, '_cpc_is_multi_file', False))
|
|
303
|
+
except Exception:
|
|
304
|
+
is_multi = True
|
|
305
|
+
if not is_multi:
|
|
306
|
+
# Remove "v: show/hide files" in single-file mode
|
|
307
|
+
col1 = [item for item in col1 if not item.strip().startswith("v:")]
|
|
308
|
+
|
|
309
|
+
# 2) Conditional overwrite shortcuts under (Options) if figure is available
|
|
310
|
+
if fig is not None:
|
|
311
|
+
last_session = getattr(fig, "_last_session_save_path", None)
|
|
312
|
+
last_style = getattr(fig, "_last_style_export_path", None)
|
|
313
|
+
last_figure = getattr(fig, "_last_figure_export_path", None)
|
|
314
|
+
if last_session:
|
|
315
|
+
col3.append("os: overwrite session")
|
|
316
|
+
if last_style:
|
|
317
|
+
col3.append("ops: overwrite style")
|
|
318
|
+
col3.append("opsg: overwrite style+geom")
|
|
319
|
+
if last_figure:
|
|
320
|
+
col3.append("oe: overwrite figure")
|
|
296
321
|
w1 = max(18, *(len(s) for s in col1))
|
|
297
322
|
w2 = max(18, *(len(s) for s in col2))
|
|
298
323
|
w3 = max(12, *(len(s) for s in col3))
|
|
@@ -413,6 +438,21 @@ def _get_geometry_snapshot(ax, ax2) -> Dict:
|
|
|
413
438
|
return geom
|
|
414
439
|
|
|
415
440
|
|
|
441
|
+
def _is_hollow_marker(artist) -> bool:
|
|
442
|
+
"""Check if a scatter artist has hollow markers (facecolor='none' or transparent)."""
|
|
443
|
+
try:
|
|
444
|
+
if hasattr(artist, 'get_facecolors'):
|
|
445
|
+
face_arr = artist.get_facecolors()
|
|
446
|
+
if face_arr is not None and len(face_arr):
|
|
447
|
+
# Check if facecolor is fully transparent (alpha == 0)
|
|
448
|
+
fc = face_arr[0]
|
|
449
|
+
if len(fc) >= 4 and fc[3] == 0:
|
|
450
|
+
return True
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
|
|
416
456
|
def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=None) -> Dict:
|
|
417
457
|
try:
|
|
418
458
|
fig_w, fig_h = map(float, fig.get_size_inches())
|
|
@@ -420,18 +460,37 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
420
460
|
fig_w = fig_h = None
|
|
421
461
|
|
|
422
462
|
def _color_of(artist) -> Optional[str]:
|
|
463
|
+
"""Return a representative color for a scatter/artist.
|
|
464
|
+
|
|
465
|
+
For hollow markers (facecolor='none'), fall back to edgecolor so that
|
|
466
|
+
style snapshots still capture the intended color.
|
|
467
|
+
"""
|
|
423
468
|
try:
|
|
469
|
+
# Prefer explicit color if available
|
|
424
470
|
if hasattr(artist, 'get_color'):
|
|
425
471
|
c = artist.get_color()
|
|
426
|
-
# scatter returns array sometimes; pick first
|
|
427
472
|
if isinstance(c, (list, tuple)) and c and not isinstance(c, str):
|
|
428
473
|
return c[0]
|
|
429
474
|
return c
|
|
475
|
+
# Fall back to facecolors / edgecolors for scatter
|
|
476
|
+
face_arr = None
|
|
477
|
+
edge_arr = None
|
|
430
478
|
if hasattr(artist, 'get_facecolors'):
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
479
|
+
face_arr = artist.get_facecolors()
|
|
480
|
+
if hasattr(artist, 'get_edgecolors'):
|
|
481
|
+
edge_arr = artist.get_edgecolors()
|
|
482
|
+
from matplotlib.colors import to_hex
|
|
483
|
+
# If facecolor is 'none' or empty, use edgecolor instead
|
|
484
|
+
if face_arr is not None and len(face_arr):
|
|
485
|
+
# Some backends use fully transparent facecolor for 'none'
|
|
486
|
+
fc = face_arr[0]
|
|
487
|
+
try:
|
|
488
|
+
if fc[3] > 0:
|
|
489
|
+
return to_hex(fc)
|
|
490
|
+
except Exception:
|
|
491
|
+
return to_hex(fc)
|
|
492
|
+
if edge_arr is not None and len(edge_arr):
|
|
493
|
+
return to_hex(edge_arr[0])
|
|
435
494
|
except Exception:
|
|
436
495
|
pass
|
|
437
496
|
return None
|
|
@@ -621,12 +680,14 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
621
680
|
'marker': getattr(sc_charge, 'get_marker', lambda: 'o')(),
|
|
622
681
|
'markersize': float(getattr(sc_charge, 'get_sizes', lambda: [32])()[0]) if hasattr(sc_charge, 'get_sizes') else 32.0,
|
|
623
682
|
'alpha': float(sc_charge.get_alpha()) if sc_charge.get_alpha() is not None else 1.0,
|
|
683
|
+
'hollow': _is_hollow_marker(sc_charge),
|
|
624
684
|
},
|
|
625
685
|
'discharge': {
|
|
626
686
|
'color': _color_of(sc_discharge),
|
|
627
687
|
'marker': getattr(sc_discharge, 'get_marker', lambda: 's')(),
|
|
628
688
|
'markersize': float(getattr(sc_discharge, 'get_sizes', lambda: [32])()[0]) if hasattr(sc_discharge, 'get_sizes') else 32.0,
|
|
629
689
|
'alpha': float(sc_discharge.get_alpha()) if sc_discharge.get_alpha() is not None else 1.0,
|
|
690
|
+
'hollow': _is_hollow_marker(sc_discharge),
|
|
630
691
|
},
|
|
631
692
|
'efficiency': {
|
|
632
693
|
'color': (sc_eff.get_facecolors()[0].tolist() if hasattr(sc_eff, 'get_facecolors') and len(sc_eff.get_facecolors()) else '#2ca02c'),
|
|
@@ -634,6 +695,7 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
634
695
|
'markersize': float(getattr(sc_eff, 'get_sizes', lambda: [40])()[0]) if hasattr(sc_eff, 'get_sizes') else 40.0,
|
|
635
696
|
'alpha': float(sc_eff.get_alpha()) if sc_eff.get_alpha() is not None else 1.0,
|
|
636
697
|
'visible': bool(getattr(sc_eff, 'get_visible', lambda: True)()),
|
|
698
|
+
'hollow': _is_hollow_marker(sc_eff),
|
|
637
699
|
}
|
|
638
700
|
}
|
|
639
701
|
}
|
|
@@ -650,10 +712,13 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
|
|
|
650
712
|
'visible': f.get('visible', True),
|
|
651
713
|
'charge_color': _color_of(sc_chg),
|
|
652
714
|
'charge_marker': getattr(sc_chg, 'get_marker', lambda: 'o')() if sc_chg else 'o',
|
|
715
|
+
'charge_hollow': _is_hollow_marker(sc_chg) if sc_chg else False,
|
|
653
716
|
'discharge_color': _color_of(sc_dchg),
|
|
654
717
|
'discharge_marker': getattr(sc_dchg, 'get_marker', lambda: 's')() if sc_dchg else 's',
|
|
718
|
+
'discharge_hollow': _is_hollow_marker(sc_dchg) if sc_dchg else False,
|
|
655
719
|
'efficiency_color': _color_of(sc_eff),
|
|
656
720
|
'efficiency_marker': getattr(sc_eff, 'get_marker', lambda: '^')() if sc_eff else '^',
|
|
721
|
+
'efficiency_hollow': _is_hollow_marker(sc_eff) if sc_eff else False,
|
|
657
722
|
}
|
|
658
723
|
# Save legend labels
|
|
659
724
|
try:
|
|
@@ -692,6 +757,11 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
692
757
|
file_data: Optional list of file dicts for multi-file mode
|
|
693
758
|
"""
|
|
694
759
|
is_multi_file = file_data is not None and len(file_data) > 1
|
|
760
|
+
# Store multi-file flag on figure so the menu can hide/show multi-file commands correctly
|
|
761
|
+
try:
|
|
762
|
+
fig._cpc_is_multi_file = bool(is_multi_file)
|
|
763
|
+
except Exception:
|
|
764
|
+
pass
|
|
695
765
|
|
|
696
766
|
# Save current labelpad values BEFORE any style changes
|
|
697
767
|
saved_xlabelpad = None
|
|
@@ -844,7 +914,12 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
844
914
|
# Single file mode: apply to provided artists only
|
|
845
915
|
if ch:
|
|
846
916
|
if ch.get('color') is not None:
|
|
847
|
-
|
|
917
|
+
# Apply color respecting hollow marker style
|
|
918
|
+
if ch.get('hollow', False):
|
|
919
|
+
sc_charge.set_facecolors('none')
|
|
920
|
+
sc_charge.set_edgecolors(ch['color'])
|
|
921
|
+
else:
|
|
922
|
+
sc_charge.set_color(ch['color'])
|
|
848
923
|
if ch.get('marker') is not None and hasattr(sc_charge, 'set_marker'):
|
|
849
924
|
sc_charge.set_marker(ch['marker'])
|
|
850
925
|
if ch.get('markersize') is not None and hasattr(sc_charge, 'set_sizes'):
|
|
@@ -853,7 +928,12 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
853
928
|
sc_charge.set_alpha(float(ch['alpha']))
|
|
854
929
|
if dh:
|
|
855
930
|
if dh.get('color') is not None:
|
|
856
|
-
|
|
931
|
+
# Apply color respecting hollow marker style
|
|
932
|
+
if dh.get('hollow', False):
|
|
933
|
+
sc_discharge.set_facecolors('none')
|
|
934
|
+
sc_discharge.set_edgecolors(dh['color'])
|
|
935
|
+
else:
|
|
936
|
+
sc_discharge.set_color(dh['color'])
|
|
857
937
|
if dh.get('marker') is not None and hasattr(sc_discharge, 'set_marker'):
|
|
858
938
|
sc_discharge.set_marker(dh['marker'])
|
|
859
939
|
if dh.get('markersize') is not None and hasattr(sc_discharge, 'set_sizes'):
|
|
@@ -863,7 +943,12 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
863
943
|
if ef:
|
|
864
944
|
if ef.get('color') is not None:
|
|
865
945
|
try:
|
|
866
|
-
|
|
946
|
+
# Apply color respecting hollow marker style
|
|
947
|
+
if ef.get('hollow', False):
|
|
948
|
+
sc_eff.set_facecolors('none')
|
|
949
|
+
sc_eff.set_edgecolors(ef['color'])
|
|
950
|
+
else:
|
|
951
|
+
sc_eff.set_color(ef['color'])
|
|
867
952
|
except Exception:
|
|
868
953
|
pass
|
|
869
954
|
if ef.get('marker') is not None and hasattr(sc_eff, 'set_marker'):
|
|
@@ -878,6 +963,16 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
878
963
|
ax2.yaxis.label.set_visible(bool(ef['visible']))
|
|
879
964
|
except Exception:
|
|
880
965
|
pass
|
|
966
|
+
# Restore legend labels for single-file mode
|
|
967
|
+
try:
|
|
968
|
+
if 'label' in ch and hasattr(sc_charge, 'set_label'):
|
|
969
|
+
sc_charge.set_label(ch['label'])
|
|
970
|
+
if 'label' in dh and hasattr(sc_discharge, 'set_label'):
|
|
971
|
+
sc_discharge.set_label(dh['label'])
|
|
972
|
+
if 'label' in ef and hasattr(sc_eff, 'set_label'):
|
|
973
|
+
sc_eff.set_label(ef['label'])
|
|
974
|
+
except Exception:
|
|
975
|
+
pass
|
|
881
976
|
except Exception:
|
|
882
977
|
pass
|
|
883
978
|
# Apply legend state (h command)
|
|
@@ -1163,40 +1258,40 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
|
|
|
1163
1258
|
for i, f_info in enumerate(multi_files):
|
|
1164
1259
|
if i < len(file_data):
|
|
1165
1260
|
f = file_data[i]
|
|
1166
|
-
# Restore colors FIRST (before labels)
|
|
1261
|
+
# Restore colors FIRST (before labels), respecting hollow marker style
|
|
1167
1262
|
if 'charge_color' in f_info and f.get('sc_charge'):
|
|
1168
1263
|
try:
|
|
1169
1264
|
col = f_info['charge_color']
|
|
1170
|
-
|
|
1265
|
+
is_hollow = f_info.get('charge_hollow', False)
|
|
1266
|
+
if is_hollow:
|
|
1267
|
+
f['sc_charge'].set_facecolors('none')
|
|
1268
|
+
f['sc_charge'].set_edgecolors(col)
|
|
1269
|
+
else:
|
|
1270
|
+
f['sc_charge'].set_color(col)
|
|
1171
1271
|
f['color'] = col
|
|
1172
|
-
# Force update of facecolors for scatter plots
|
|
1173
|
-
if hasattr(f['sc_charge'], 'set_facecolors'):
|
|
1174
|
-
from matplotlib.colors import to_rgba
|
|
1175
|
-
rgba = to_rgba(col)
|
|
1176
|
-
f['sc_charge'].set_facecolors(rgba)
|
|
1177
1272
|
except Exception:
|
|
1178
1273
|
pass
|
|
1179
1274
|
if 'discharge_color' in f_info and f.get('sc_discharge'):
|
|
1180
1275
|
try:
|
|
1181
1276
|
col = f_info['discharge_color']
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
f['sc_discharge'].
|
|
1277
|
+
is_hollow = f_info.get('discharge_hollow', False)
|
|
1278
|
+
if is_hollow:
|
|
1279
|
+
f['sc_discharge'].set_facecolors('none')
|
|
1280
|
+
f['sc_discharge'].set_edgecolors(col)
|
|
1281
|
+
else:
|
|
1282
|
+
f['sc_discharge'].set_color(col)
|
|
1188
1283
|
except Exception:
|
|
1189
1284
|
pass
|
|
1190
1285
|
if 'efficiency_color' in f_info and f.get('sc_eff'):
|
|
1191
1286
|
try:
|
|
1192
1287
|
col = f_info['efficiency_color']
|
|
1193
|
-
|
|
1288
|
+
is_hollow = f_info.get('efficiency_hollow', False)
|
|
1289
|
+
if is_hollow:
|
|
1290
|
+
f['sc_eff'].set_facecolors('none')
|
|
1291
|
+
f['sc_eff'].set_edgecolors(col)
|
|
1292
|
+
else:
|
|
1293
|
+
f['sc_eff'].set_color(col)
|
|
1194
1294
|
f['eff_color'] = col
|
|
1195
|
-
# Force update of facecolors for scatter plots
|
|
1196
|
-
if hasattr(f['sc_eff'], 'set_facecolors'):
|
|
1197
|
-
from matplotlib.colors import to_rgba
|
|
1198
|
-
rgba = to_rgba(col)
|
|
1199
|
-
f['sc_eff'].set_facecolors(rgba)
|
|
1200
1295
|
except Exception:
|
|
1201
1296
|
pass
|
|
1202
1297
|
# Restore legend labels
|
|
@@ -1350,8 +1445,6 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1350
1445
|
# If file_data is provided, we're in multi-file mode.
|
|
1351
1446
|
# If not provided, we create a single-file structure for backward compatibility.
|
|
1352
1447
|
# ====================================================================
|
|
1353
|
-
is_multi_file = file_data is not None and len(file_data) > 1
|
|
1354
|
-
|
|
1355
1448
|
if file_data is None:
|
|
1356
1449
|
# Backward compatibility: create file_data structure from single file
|
|
1357
1450
|
# This allows the function to work with old code that passes individual artists
|
|
@@ -1376,12 +1469,18 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1376
1469
|
'sc_eff': sc_eff, # Efficiency scatter artist
|
|
1377
1470
|
'visible': True # File is visible by default
|
|
1378
1471
|
}]
|
|
1379
|
-
|
|
1380
1472
|
# Track which file is currently selected for editing (in multi-file mode)
|
|
1381
1473
|
current_file_idx = 0 # Index of currently selected file (0 = first file)
|
|
1382
1474
|
|
|
1383
1475
|
# Collect file paths for session saving (if available)
|
|
1384
1476
|
file_paths = _collect_file_paths(file_data)
|
|
1477
|
+
|
|
1478
|
+
# Multi-file flag: now that file_data is finalized
|
|
1479
|
+
is_multi_file = file_data is not None and len(file_data) > 1
|
|
1480
|
+
try:
|
|
1481
|
+
fig._cpc_is_multi_file = bool(is_multi_file)
|
|
1482
|
+
except Exception:
|
|
1483
|
+
pass
|
|
1385
1484
|
|
|
1386
1485
|
# ====================================================================
|
|
1387
1486
|
# TICK STATE MANAGEMENT
|
|
@@ -1619,7 +1718,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1619
1718
|
except Exception:
|
|
1620
1719
|
pass
|
|
1621
1720
|
|
|
1622
|
-
_print_menu()
|
|
1721
|
+
_print_menu(fig)
|
|
1623
1722
|
|
|
1624
1723
|
while True:
|
|
1625
1724
|
try:
|
|
@@ -1640,7 +1739,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1640
1739
|
_print_file_list(file_data, current_file_idx)
|
|
1641
1740
|
choice = _safe_input(f"Toggle visibility for file (1-{len(file_data)}), 'a' for all, or q=cancel: ").strip()
|
|
1642
1741
|
if choice.lower() == 'q':
|
|
1643
|
-
_print_menu()
|
|
1742
|
+
_print_menu(fig)
|
|
1644
1743
|
_print_file_list(file_data, current_file_idx)
|
|
1645
1744
|
continue
|
|
1646
1745
|
|
|
@@ -1665,48 +1764,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1665
1764
|
f['sc_eff'].set_visible(new_vis)
|
|
1666
1765
|
else:
|
|
1667
1766
|
print("Invalid file number.")
|
|
1767
|
+
|
|
1768
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
1769
|
+
fig.canvas.draw_idle()
|
|
1668
1770
|
else:
|
|
1669
|
-
# Single file mode:
|
|
1670
|
-
|
|
1671
|
-
# Capture current legend position BEFORE toggling visibility
|
|
1672
|
-
try:
|
|
1673
|
-
if not hasattr(fig, '_cpc_legend_xy_in') or getattr(fig, '_cpc_legend_xy_in') is None:
|
|
1674
|
-
leg0 = ax.get_legend()
|
|
1675
|
-
if leg0 is not None and leg0.get_visible():
|
|
1676
|
-
try:
|
|
1677
|
-
# Ensure renderer exists
|
|
1678
|
-
try:
|
|
1679
|
-
renderer = fig.canvas.get_renderer()
|
|
1680
|
-
except Exception:
|
|
1681
|
-
fig.canvas.draw()
|
|
1682
|
-
renderer = fig.canvas.get_renderer()
|
|
1683
|
-
bb = leg0.get_window_extent(renderer=renderer)
|
|
1684
|
-
cx = 0.5 * (bb.x0 + bb.x1)
|
|
1685
|
-
cy = 0.5 * (bb.y0 + bb.y1)
|
|
1686
|
-
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
1687
|
-
fw, fh = fig.get_size_inches()
|
|
1688
|
-
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
1689
|
-
offset = _sanitize_legend_offset(offset)
|
|
1690
|
-
if offset is not None:
|
|
1691
|
-
fig._cpc_legend_xy_in = offset
|
|
1692
|
-
except Exception:
|
|
1693
|
-
pass
|
|
1694
|
-
except Exception:
|
|
1695
|
-
pass
|
|
1696
|
-
vis = sc_eff.get_visible()
|
|
1697
|
-
sc_eff.set_visible(not vis)
|
|
1698
|
-
try:
|
|
1699
|
-
ax2.yaxis.label.set_visible(not vis)
|
|
1700
|
-
except Exception:
|
|
1701
|
-
pass
|
|
1702
|
-
|
|
1703
|
-
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
1704
|
-
fig.canvas.draw_idle()
|
|
1771
|
+
# Single file mode: v is not meaningful (no per-file visibility)
|
|
1772
|
+
print("File visibility (v) is only available in multi-file CPC mode.")
|
|
1705
1773
|
except ValueError:
|
|
1706
1774
|
print("Invalid input.")
|
|
1707
1775
|
except Exception as e:
|
|
1708
1776
|
print(f"Visibility toggle failed: {e}")
|
|
1709
|
-
_print_menu()
|
|
1777
|
+
_print_menu(fig)
|
|
1710
1778
|
if is_multi_file:
|
|
1711
1779
|
_print_file_list(file_data, current_file_idx)
|
|
1712
1780
|
continue
|
|
@@ -1719,10 +1787,10 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1719
1787
|
if confirm == 'y':
|
|
1720
1788
|
break
|
|
1721
1789
|
else:
|
|
1722
|
-
_print_menu(); continue
|
|
1790
|
+
_print_menu(fig); continue
|
|
1723
1791
|
elif key == 'b':
|
|
1724
1792
|
restore_state()
|
|
1725
|
-
_print_menu(); continue
|
|
1793
|
+
_print_menu(fig); continue
|
|
1726
1794
|
elif key == 'c':
|
|
1727
1795
|
# Colors submenu: ly (left Y series) and ry (right Y efficiency), with user colors and palettes
|
|
1728
1796
|
try:
|
|
@@ -1837,18 +1905,19 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1837
1905
|
continue
|
|
1838
1906
|
discharge_col = _generate_similar_color(charge_col)
|
|
1839
1907
|
try:
|
|
1840
|
-
f['sc_charge'].set_color(charge_col)
|
|
1841
|
-
f['sc_discharge'].set_color(discharge_col)
|
|
1842
1908
|
f['color'] = charge_col
|
|
1843
|
-
#
|
|
1909
|
+
# Chg: filled square; DChg: unfilled (hollow) square
|
|
1844
1910
|
if hasattr(f['sc_charge'], 'set_facecolors'):
|
|
1845
1911
|
from matplotlib.colors import to_rgba
|
|
1846
|
-
|
|
1847
|
-
f['sc_charge'].set_facecolors(
|
|
1912
|
+
f['sc_charge'].set_color(charge_col)
|
|
1913
|
+
f['sc_charge'].set_facecolors(to_rgba(charge_col))
|
|
1914
|
+
else:
|
|
1915
|
+
f['sc_charge'].set_color(charge_col)
|
|
1848
1916
|
if hasattr(f['sc_discharge'], 'set_facecolors'):
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1917
|
+
f['sc_discharge'].set_facecolors('none')
|
|
1918
|
+
f['sc_discharge'].set_edgecolors(discharge_col)
|
|
1919
|
+
else:
|
|
1920
|
+
f['sc_discharge'].set_color(discharge_col)
|
|
1852
1921
|
except Exception as e:
|
|
1853
1922
|
print(f"Error setting color: {e}")
|
|
1854
1923
|
pass
|
|
@@ -1885,18 +1954,19 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1885
1954
|
continue
|
|
1886
1955
|
discharge_col = _generate_similar_color(charge_col)
|
|
1887
1956
|
try:
|
|
1888
|
-
file_data[file_idx]['sc_charge'].set_color(charge_col)
|
|
1889
|
-
file_data[file_idx]['sc_discharge'].set_color(discharge_col)
|
|
1890
1957
|
file_data[file_idx]['color'] = charge_col
|
|
1891
|
-
#
|
|
1958
|
+
# Chg: filled square; DChg: unfilled (hollow) square
|
|
1892
1959
|
if hasattr(file_data[file_idx]['sc_charge'], 'set_facecolors'):
|
|
1893
1960
|
from matplotlib.colors import to_rgba
|
|
1894
|
-
|
|
1895
|
-
file_data[file_idx]['sc_charge'].set_facecolors(
|
|
1961
|
+
file_data[file_idx]['sc_charge'].set_color(charge_col)
|
|
1962
|
+
file_data[file_idx]['sc_charge'].set_facecolors(to_rgba(charge_col))
|
|
1963
|
+
else:
|
|
1964
|
+
file_data[file_idx]['sc_charge'].set_color(charge_col)
|
|
1896
1965
|
if hasattr(file_data[file_idx]['sc_discharge'], 'set_facecolors'):
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1966
|
+
file_data[file_idx]['sc_discharge'].set_facecolors('none')
|
|
1967
|
+
file_data[file_idx]['sc_discharge'].set_edgecolors(discharge_col)
|
|
1968
|
+
else:
|
|
1969
|
+
file_data[file_idx]['sc_discharge'].set_color(discharge_col)
|
|
1900
1970
|
except Exception:
|
|
1901
1971
|
pass
|
|
1902
1972
|
_apply_manual_entries(tokens)
|
|
@@ -2007,7 +2077,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2007
2077
|
print("Unknown option.")
|
|
2008
2078
|
except Exception as e:
|
|
2009
2079
|
print(f"Error in colors menu: {e}")
|
|
2010
|
-
_print_menu()
|
|
2080
|
+
_print_menu(fig)
|
|
2011
2081
|
if is_multi_file:
|
|
2012
2082
|
_print_file_list(file_data, current_file_idx)
|
|
2013
2083
|
continue
|
|
@@ -2070,7 +2140,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2070
2140
|
fig.canvas.draw()
|
|
2071
2141
|
except Exception as e:
|
|
2072
2142
|
print(f"Error in spine color menu: {e}")
|
|
2073
|
-
_print_menu()
|
|
2143
|
+
_print_menu(fig)
|
|
2074
2144
|
if is_multi_file:
|
|
2075
2145
|
_print_file_list(file_data, current_file_idx)
|
|
2076
2146
|
continue
|
|
@@ -2078,7 +2148,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2078
2148
|
try:
|
|
2079
2149
|
base_path = choose_save_path(file_paths, purpose="figure export")
|
|
2080
2150
|
if not base_path:
|
|
2081
|
-
_print_menu()
|
|
2151
|
+
_print_menu(fig)
|
|
2082
2152
|
continue
|
|
2083
2153
|
print(f"\nChosen path: {base_path}")
|
|
2084
2154
|
# List existing figure files from Figures/ subdirectory
|
|
@@ -2101,19 +2171,19 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2101
2171
|
else:
|
|
2102
2172
|
fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
|
|
2103
2173
|
if not fname or fname.lower() == 'q':
|
|
2104
|
-
_print_menu(); continue
|
|
2174
|
+
_print_menu(fig); continue
|
|
2105
2175
|
|
|
2106
2176
|
# Check for 'o' option
|
|
2107
2177
|
if fname.lower() == 'o':
|
|
2108
2178
|
if not last_figure_path:
|
|
2109
2179
|
print("No previous export found.")
|
|
2110
|
-
_print_menu(); continue
|
|
2180
|
+
_print_menu(fig); continue
|
|
2111
2181
|
if not os.path.exists(last_figure_path):
|
|
2112
2182
|
print(f"Previous export file not found: {last_figure_path}")
|
|
2113
|
-
_print_menu(); continue
|
|
2183
|
+
_print_menu(fig); continue
|
|
2114
2184
|
yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
|
|
2115
2185
|
if yn != 'y':
|
|
2116
|
-
_print_menu(); continue
|
|
2186
|
+
_print_menu(fig); continue
|
|
2117
2187
|
target = last_figure_path
|
|
2118
2188
|
# Check if user selected a number
|
|
2119
2189
|
elif fname.isdigit() and files:
|
|
@@ -2126,7 +2196,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2126
2196
|
target = file_list[idx-1][1] # Full path from list
|
|
2127
2197
|
else:
|
|
2128
2198
|
print("Invalid number.")
|
|
2129
|
-
_print_menu(); continue
|
|
2199
|
+
_print_menu(fig); continue
|
|
2130
2200
|
else:
|
|
2131
2201
|
root, ext = os.path.splitext(fname)
|
|
2132
2202
|
if ext == '':
|
|
@@ -2139,7 +2209,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2139
2209
|
if os.path.exists(target):
|
|
2140
2210
|
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2141
2211
|
if yn != 'y':
|
|
2142
|
-
_print_menu(); continue
|
|
2212
|
+
_print_menu(fig); continue
|
|
2143
2213
|
if target:
|
|
2144
2214
|
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
2145
2215
|
from .utils import ensure_exact_case_filename
|
|
@@ -2258,7 +2328,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2258
2328
|
pass
|
|
2259
2329
|
except Exception as e:
|
|
2260
2330
|
print(f"Export failed: {e}")
|
|
2261
|
-
_print_menu(); continue
|
|
2331
|
+
_print_menu(fig); continue
|
|
2262
2332
|
elif key == 's':
|
|
2263
2333
|
# Save CPC session (.pkl) with all data and styles
|
|
2264
2334
|
try:
|
|
@@ -2317,7 +2387,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2317
2387
|
pass
|
|
2318
2388
|
folder = choose_save_path(file_paths, purpose="CPC session save")
|
|
2319
2389
|
if not folder:
|
|
2320
|
-
_print_menu(); continue
|
|
2390
|
+
_print_menu(fig); continue
|
|
2321
2391
|
print(f"\nChosen path: {folder}")
|
|
2322
2392
|
try:
|
|
2323
2393
|
files = sorted([f for f in os.listdir(folder) if f.lower().endswith('.pkl')])
|
|
@@ -2339,18 +2409,18 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2339
2409
|
prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
|
|
2340
2410
|
choice = _safe_input(prompt).strip()
|
|
2341
2411
|
if not choice or choice.lower() == 'q':
|
|
2342
|
-
_print_menu(); continue
|
|
2412
|
+
_print_menu(fig); continue
|
|
2343
2413
|
if choice.lower() == 'o':
|
|
2344
2414
|
# Overwrite last saved session
|
|
2345
2415
|
if not last_session_path:
|
|
2346
2416
|
print("No previous save found.")
|
|
2347
|
-
_print_menu(); continue
|
|
2417
|
+
_print_menu(fig); continue
|
|
2348
2418
|
if not os.path.exists(last_session_path):
|
|
2349
2419
|
print(f"Previous save file not found: {last_session_path}")
|
|
2350
|
-
_print_menu(); continue
|
|
2420
|
+
_print_menu(fig); continue
|
|
2351
2421
|
yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2352
2422
|
if yn != 'y':
|
|
2353
|
-
_print_menu(); continue
|
|
2423
|
+
_print_menu(fig); continue
|
|
2354
2424
|
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)
|
|
2355
2425
|
print(f"Overwritten session to {last_session_path}")
|
|
2356
2426
|
_print_menu(); continue
|
|
@@ -2367,7 +2437,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2367
2437
|
_print_menu(); continue
|
|
2368
2438
|
else:
|
|
2369
2439
|
print("Invalid number.")
|
|
2370
|
-
_print_menu(); continue
|
|
2440
|
+
_print_menu(fig); continue
|
|
2371
2441
|
if choice.lower() != 'o':
|
|
2372
2442
|
name = choice
|
|
2373
2443
|
root, ext = os.path.splitext(name)
|
|
@@ -2377,12 +2447,12 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2377
2447
|
if os.path.exists(target):
|
|
2378
2448
|
yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
2379
2449
|
if yn != 'y':
|
|
2380
|
-
_print_menu(); continue
|
|
2450
|
+
_print_menu(fig); continue
|
|
2381
2451
|
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)
|
|
2382
2452
|
fig._last_session_save_path = target
|
|
2383
2453
|
except Exception as e:
|
|
2384
2454
|
print(f"Save failed: {e}")
|
|
2385
|
-
_print_menu(); continue
|
|
2455
|
+
_print_menu(fig); continue
|
|
2386
2456
|
elif key == 'p':
|
|
2387
2457
|
try:
|
|
2388
2458
|
style_menu_active = True
|
|
@@ -2670,12 +2740,12 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2670
2740
|
print("Unknown choice.")
|
|
2671
2741
|
except Exception as e:
|
|
2672
2742
|
print(f"Error in style submenu: {e}")
|
|
2673
|
-
_print_menu(); continue
|
|
2743
|
+
_print_menu(fig); continue
|
|
2674
2744
|
elif key == 'i':
|
|
2675
2745
|
try:
|
|
2676
2746
|
path = choose_style_file(file_paths, purpose="style import")
|
|
2677
2747
|
if not path:
|
|
2678
|
-
_print_menu(); continue
|
|
2748
|
+
_print_menu(fig); continue
|
|
2679
2749
|
push_state("import-style")
|
|
2680
2750
|
with open(path, 'r', encoding='utf-8') as f:
|
|
2681
2751
|
cfg = json.load(f)
|
|
@@ -2683,7 +2753,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2683
2753
|
# Check file type
|
|
2684
2754
|
kind = cfg.get('kind', '')
|
|
2685
2755
|
if kind not in ('cpc_style', 'cpc_style_geom'):
|
|
2686
|
-
print("Not a CPC style file."); _print_menu(); continue
|
|
2756
|
+
print("Not a CPC style file."); _print_menu(fig); continue
|
|
2687
2757
|
|
|
2688
2758
|
# Enforce compatibility between style/geom ro state and current figure ro state
|
|
2689
2759
|
file_ro = bool(cfg.get('ro_active', False))
|
|
@@ -2694,7 +2764,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2694
2764
|
else:
|
|
2695
2765
|
print("Warning: Style/geometry file was saved without --ro; current plot was created with --ro.")
|
|
2696
2766
|
print("Not applying CPC style/geometry to avoid corrupting axis orientation.")
|
|
2697
|
-
_print_menu(); continue
|
|
2767
|
+
_print_menu(fig); continue
|
|
2698
2768
|
|
|
2699
2769
|
has_geometry = (kind == 'cpc_style_geom' and 'geometry' in cfg)
|
|
2700
2770
|
|
|
@@ -2724,7 +2794,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2724
2794
|
|
|
2725
2795
|
except Exception as e:
|
|
2726
2796
|
print(f"Error importing style: {e}")
|
|
2727
|
-
_print_menu(); continue
|
|
2797
|
+
_print_menu(fig); continue
|
|
2728
2798
|
elif key == 'ry':
|
|
2729
2799
|
# Toggle efficiency visibility on the right axis
|
|
2730
2800
|
try:
|
|
@@ -2833,7 +2903,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2833
2903
|
fig.canvas.draw_idle()
|
|
2834
2904
|
except Exception:
|
|
2835
2905
|
pass
|
|
2836
|
-
_print_menu(); continue
|
|
2906
|
+
_print_menu(fig); continue
|
|
2837
2907
|
elif key == 'h':
|
|
2838
2908
|
# Legend submenu: toggle visibility, set position in inches relative to canvas center (0,0)
|
|
2839
2909
|
try:
|
|
@@ -2886,15 +2956,15 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2886
2956
|
# Ensure a legend exists at the stored position
|
|
2887
2957
|
H, L = _visible_handles_labels(ax, ax2)
|
|
2888
2958
|
if H:
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
else:
|
|
2894
|
-
_legend_no_frame(ax, H, L, loc='best', borderaxespad=1.0)
|
|
2959
|
+
# Always use _rebuild_legend for consistency
|
|
2960
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
2961
|
+
else:
|
|
2962
|
+
print("No visible legend items found.")
|
|
2895
2963
|
fig.canvas.draw_idle()
|
|
2896
|
-
except Exception:
|
|
2897
|
-
|
|
2964
|
+
except Exception as e:
|
|
2965
|
+
print(f"Error toggling legend: {e}")
|
|
2966
|
+
import traceback
|
|
2967
|
+
traceback.print_exc()
|
|
2898
2968
|
elif sub == 'p':
|
|
2899
2969
|
# Position submenu with x and y subcommands
|
|
2900
2970
|
while True:
|
|
@@ -2920,13 +2990,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2920
2990
|
continue
|
|
2921
2991
|
push_state("legend-position")
|
|
2922
2992
|
try:
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2993
|
+
# Sanitize and store the new position
|
|
2994
|
+
new_pos = _sanitize_legend_offset((x_in, xy_in[1]))
|
|
2995
|
+
if new_pos is not None:
|
|
2996
|
+
fig._cpc_legend_xy_in = new_pos
|
|
2997
|
+
_apply_legend_position()
|
|
2998
|
+
fig.canvas.draw_idle()
|
|
2999
|
+
print(f"Legend position updated: x={new_pos[0]:.2f}, y={new_pos[1]:.2f}")
|
|
3000
|
+
else:
|
|
3001
|
+
print(f"Invalid position: x={x_in:.2f} is out of bounds. Position not updated.")
|
|
3002
|
+
except Exception as e:
|
|
3003
|
+
print(f"Error updating legend position: {e}")
|
|
2930
3004
|
elif pos_cmd == 'y':
|
|
2931
3005
|
# Y only: stay in loop
|
|
2932
3006
|
while True:
|
|
@@ -2943,13 +3017,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2943
3017
|
continue
|
|
2944
3018
|
push_state("legend-position")
|
|
2945
3019
|
try:
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
3020
|
+
# Sanitize and store the new position
|
|
3021
|
+
new_pos = _sanitize_legend_offset((xy_in[0], y_in))
|
|
3022
|
+
if new_pos is not None:
|
|
3023
|
+
fig._cpc_legend_xy_in = new_pos
|
|
3024
|
+
_apply_legend_position()
|
|
3025
|
+
fig.canvas.draw_idle()
|
|
3026
|
+
print(f"Legend position updated: x={new_pos[0]:.2f}, y={new_pos[1]:.2f}")
|
|
3027
|
+
else:
|
|
3028
|
+
print(f"Invalid position: y={y_in:.2f} is out of bounds. Position not updated.")
|
|
3029
|
+
except Exception as e:
|
|
3030
|
+
print(f"Error updating legend position: {e}")
|
|
2953
3031
|
else:
|
|
2954
3032
|
# Try to parse as "x y" format
|
|
2955
3033
|
parts = pos_cmd.replace(',', ' ').split()
|
|
@@ -2961,13 +3039,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2961
3039
|
print("Invalid numbers."); continue
|
|
2962
3040
|
push_state("legend-position")
|
|
2963
3041
|
try:
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3042
|
+
# Sanitize and store the new position
|
|
3043
|
+
new_pos = _sanitize_legend_offset((x_in, y_in))
|
|
3044
|
+
if new_pos is not None:
|
|
3045
|
+
fig._cpc_legend_xy_in = new_pos
|
|
3046
|
+
_apply_legend_position()
|
|
3047
|
+
fig.canvas.draw_idle()
|
|
3048
|
+
print(f"Legend position updated: x={new_pos[0]:.2f}, y={new_pos[1]:.2f}")
|
|
3049
|
+
else:
|
|
3050
|
+
print(f"Invalid position: x={x_in:.2f}, y={y_in:.2f} is out of bounds. Position not updated.")
|
|
3051
|
+
except Exception as e:
|
|
3052
|
+
print(f"Error updating legend position: {e}")
|
|
2971
3053
|
else:
|
|
2972
3054
|
print("Unknown option.")
|
|
2973
3055
|
except Exception:
|
|
@@ -3206,28 +3288,46 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3206
3288
|
except Exception:
|
|
3207
3289
|
e_ms = 40
|
|
3208
3290
|
print(f" charge ms={c_ms}, discharge ms={d_ms}, efficiency ms={e_ms}")
|
|
3209
|
-
spec = _safe_input("Set marker size
|
|
3291
|
+
spec = _safe_input("Set new marker size for all series (q=cancel): ").strip().lower()
|
|
3210
3292
|
if not spec or spec == 'q':
|
|
3211
|
-
_print_menu(); continue
|
|
3212
|
-
parts = spec.split()
|
|
3213
|
-
if len(parts) != 2:
|
|
3214
|
-
print("Need two tokens."); _print_menu(); continue
|
|
3215
|
-
role, val = parts[0], parts[1]
|
|
3293
|
+
_print_menu(fig); continue
|
|
3216
3294
|
try:
|
|
3217
|
-
num = float(
|
|
3295
|
+
num = float(spec)
|
|
3218
3296
|
push_state("marker-size")
|
|
3219
|
-
|
|
3297
|
+
# Apply to current file's artists
|
|
3298
|
+
if hasattr(sc_charge, 'set_sizes'):
|
|
3220
3299
|
sc_charge.set_sizes([num])
|
|
3221
|
-
|
|
3300
|
+
if hasattr(sc_discharge, 'set_sizes'):
|
|
3222
3301
|
sc_discharge.set_sizes([num])
|
|
3223
|
-
|
|
3302
|
+
if hasattr(sc_eff, 'set_sizes'):
|
|
3224
3303
|
sc_eff.set_sizes([num])
|
|
3304
|
+
# In multi-file mode, also apply to all files' capacity/efficiency
|
|
3305
|
+
if is_multi_file and file_data:
|
|
3306
|
+
for f in file_data:
|
|
3307
|
+
ch = f.get('sc_charge')
|
|
3308
|
+
dh = f.get('sc_discharge')
|
|
3309
|
+
ef = f.get('sc_eff')
|
|
3310
|
+
try:
|
|
3311
|
+
if ch is not None and hasattr(ch, 'set_sizes'):
|
|
3312
|
+
ch.set_sizes([num])
|
|
3313
|
+
except Exception:
|
|
3314
|
+
pass
|
|
3315
|
+
try:
|
|
3316
|
+
if dh is not None and hasattr(dh, 'set_sizes'):
|
|
3317
|
+
dh.set_sizes([num])
|
|
3318
|
+
except Exception:
|
|
3319
|
+
pass
|
|
3320
|
+
try:
|
|
3321
|
+
if ef is not None and hasattr(ef, 'set_sizes'):
|
|
3322
|
+
ef.set_sizes([num])
|
|
3323
|
+
except Exception:
|
|
3324
|
+
pass
|
|
3225
3325
|
fig.canvas.draw_idle()
|
|
3226
3326
|
except Exception:
|
|
3227
3327
|
print("Invalid value.")
|
|
3228
3328
|
except Exception as e:
|
|
3229
3329
|
print(f"Error: {e}")
|
|
3230
|
-
_print_menu(); continue
|
|
3330
|
+
_print_menu(fig); continue
|
|
3231
3331
|
elif key == 't':
|
|
3232
3332
|
# Unified WASD toggles for spines/ticks/minor/labels/title per side
|
|
3233
3333
|
# Import UI positioning functions locally to ensure they're accessible in nested functions
|
|
@@ -3809,7 +3909,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3809
3909
|
fig.canvas.draw_idle()
|
|
3810
3910
|
except Exception as e:
|
|
3811
3911
|
print(f"Error in WASD tick menu: {e}")
|
|
3812
|
-
_print_menu(); continue
|
|
3912
|
+
_print_menu(fig); continue
|
|
3813
3913
|
elif key == 'g':
|
|
3814
3914
|
while True:
|
|
3815
3915
|
print("Geometry: p=plot frame, c=canvas, q=back")
|
|
@@ -3831,7 +3931,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3831
3931
|
resize_canvas(fig, ax)
|
|
3832
3932
|
except Exception as e:
|
|
3833
3933
|
print(f"Resize failed: {e}")
|
|
3834
|
-
_print_menu(); continue
|
|
3934
|
+
_print_menu(fig); continue
|
|
3835
3935
|
elif key == 'r':
|
|
3836
3936
|
# Rename axis titles
|
|
3837
3937
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
@@ -4117,7 +4217,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4117
4217
|
print(f"Error: {e}")
|
|
4118
4218
|
else:
|
|
4119
4219
|
print("Unknown option.")
|
|
4120
|
-
_print_menu(); continue
|
|
4220
|
+
_print_menu(fig); continue
|
|
4121
4221
|
elif key == 'x':
|
|
4122
4222
|
while True:
|
|
4123
4223
|
current_xlim = ax.get_xlim()
|
|
@@ -4216,7 +4316,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4216
4316
|
fig.canvas.draw_idle()
|
|
4217
4317
|
except Exception:
|
|
4218
4318
|
print("Invalid numbers.")
|
|
4219
|
-
_print_menu(); continue
|
|
4319
|
+
_print_menu(fig); continue
|
|
4220
4320
|
elif key == 'y':
|
|
4221
4321
|
while True:
|
|
4222
4322
|
print("Y-ranges: ly=left axis, ry=right axis, q=back")
|
|
@@ -4422,10 +4522,68 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
4422
4522
|
fig.canvas.draw_idle()
|
|
4423
4523
|
except Exception:
|
|
4424
4524
|
print("Invalid numbers.")
|
|
4425
|
-
_print_menu(); continue
|
|
4525
|
+
_print_menu(fig); continue
|
|
4526
|
+
elif key == 'ie':
|
|
4527
|
+
# Invert coulombic efficiency values around 100% for the current file(s)
|
|
4528
|
+
try:
|
|
4529
|
+
if sc_eff is None or not hasattr(sc_eff, 'get_offsets'):
|
|
4530
|
+
print("No efficiency data to invert.")
|
|
4531
|
+
_print_menu(fig); continue
|
|
4532
|
+
if is_multi_file:
|
|
4533
|
+
_print_file_list(file_data, current_file_idx)
|
|
4534
|
+
choice = _safe_input(
|
|
4535
|
+
f"Invert efficiency for file (1-{len(file_data)}), 'a' for all, or q=cancel: "
|
|
4536
|
+
).strip().lower()
|
|
4537
|
+
if not choice or choice == 'q':
|
|
4538
|
+
_print_menu(fig); continue
|
|
4539
|
+
targets = []
|
|
4540
|
+
if choice == 'a':
|
|
4541
|
+
targets = list(range(len(file_data)))
|
|
4542
|
+
else:
|
|
4543
|
+
try:
|
|
4544
|
+
idx = int(choice) - 1
|
|
4545
|
+
if 0 <= idx < len(file_data):
|
|
4546
|
+
targets = [idx]
|
|
4547
|
+
else:
|
|
4548
|
+
print("Invalid file number.")
|
|
4549
|
+
_print_menu(fig); continue
|
|
4550
|
+
except ValueError:
|
|
4551
|
+
print("Invalid choice.")
|
|
4552
|
+
_print_menu(fig); continue
|
|
4553
|
+
push_state("invert-efficiency")
|
|
4554
|
+
for idx in targets:
|
|
4555
|
+
f = file_data[idx]
|
|
4556
|
+
eff_sc = f.get('sc_eff')
|
|
4557
|
+
if eff_sc is None or not hasattr(eff_sc, 'get_offsets'):
|
|
4558
|
+
continue
|
|
4559
|
+
offsets = eff_sc.get_offsets()
|
|
4560
|
+
if offsets.size == 0:
|
|
4561
|
+
continue
|
|
4562
|
+
xs = offsets[:, 0]
|
|
4563
|
+
ys = offsets[:, 1]
|
|
4564
|
+
# Invert around 100% (y -> 100 - y + 100 = 200 - y)
|
|
4565
|
+
new_ys = 200.0 - ys
|
|
4566
|
+
eff_sc.set_offsets(list(zip(xs, new_ys)))
|
|
4567
|
+
fig.canvas.draw_idle()
|
|
4568
|
+
print("Inverted efficiency for selected file(s).")
|
|
4569
|
+
else:
|
|
4570
|
+
offsets = sc_eff.get_offsets()
|
|
4571
|
+
if offsets.size == 0:
|
|
4572
|
+
print("No efficiency data to invert.")
|
|
4573
|
+
_print_menu(fig); continue
|
|
4574
|
+
xs = offsets[:, 0]
|
|
4575
|
+
ys = offsets[:, 1]
|
|
4576
|
+
push_state("invert-efficiency")
|
|
4577
|
+
new_ys = 200.0 - ys
|
|
4578
|
+
sc_eff.set_offsets(list(zip(xs, new_ys)))
|
|
4579
|
+
fig.canvas.draw_idle()
|
|
4580
|
+
print("Inverted efficiency for current dataset.")
|
|
4581
|
+
except Exception as e:
|
|
4582
|
+
print(f"Error in efficiency inversion: {e}")
|
|
4583
|
+
_print_menu(fig); continue
|
|
4426
4584
|
else:
|
|
4427
4585
|
print("Unknown key.")
|
|
4428
|
-
_print_menu(); continue
|
|
4586
|
+
_print_menu(fig); continue
|
|
4429
4587
|
|
|
4430
4588
|
|
|
4431
4589
|
__all__ = ["cpc_interactive_menu"]
|