batplot 1.8.39__tar.gz → 1.8.40__tar.gz
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-1.8.39/batplot.egg-info → batplot-1.8.40}/PKG-INFO +1 -1
- {batplot-1.8.39 → batplot-1.8.40}/batplot/__init__.py +1 -1
- {batplot-1.8.39 → batplot-1.8.40}/batplot/args.py +18 -7
- {batplot-1.8.39 → batplot-1.8.40}/batplot/batch.py +40 -19
- {batplot-1.8.39 → batplot-1.8.40}/batplot/batplot.py +37 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/color_utils.py +33 -14
- {batplot-1.8.39 → batplot-1.8.40}/batplot/config.py +44 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/cpc_interactive.py +124 -51
- {batplot-1.8.39 → batplot-1.8.40}/batplot/data/CHANGELOG.md +4 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/electrochem_interactive.py +839 -69
- {batplot-1.8.39 → batplot-1.8.40}/batplot/interactive.py +37 -48
- {batplot-1.8.39 → batplot-1.8.40}/batplot/operando.py +44 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/operando_ec_interactive.py +574 -181
- {batplot-1.8.39 → batplot-1.8.40}/batplot/readers.py +2 -2
- {batplot-1.8.39 → batplot-1.8.40}/batplot/session.py +255 -183
- {batplot-1.8.39 → batplot-1.8.40}/batplot/style.py +4 -50
- {batplot-1.8.39 → batplot-1.8.40}/batplot/ui.py +150 -1
- {batplot-1.8.39 → batplot-1.8.40}/batplot/utils.py +52 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/version_check.py +2 -2
- {batplot-1.8.39 → batplot-1.8.40/batplot.egg-info}/PKG-INFO +1 -1
- {batplot-1.8.39 → batplot-1.8.40}/pyproject.toml +1 -1
- {batplot-1.8.39 → batplot-1.8.40}/LICENSE +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/MANIFEST.in +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/NOTICE +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/README.md +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/canvas_interactive.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/cif.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/cli.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/converters.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/data/USER_MANUAL.md +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/dev_upgrade.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/manual.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/modes.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/plotting.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot/showcol.py +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/setup.cfg +0 -0
- {batplot-1.8.39 → batplot-1.8.40}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.40
|
|
4
4
|
Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
|
|
5
5
|
Author-email: Tian Dai <tianda@uio.no>
|
|
6
6
|
License: MIT License
|
|
@@ -37,6 +37,8 @@ import re
|
|
|
37
37
|
#
|
|
38
38
|
# If rich is not installed, we fall back to plain text (still works fine).
|
|
39
39
|
# ====================================================================
|
|
40
|
+
from .utils import parse_mass_mg_from_cli
|
|
41
|
+
|
|
40
42
|
try:
|
|
41
43
|
from rich.console import Console # type: ignore[import]
|
|
42
44
|
from rich.markup import escape # type: ignore[import]
|
|
@@ -278,8 +280,9 @@ def _print_ec_help() -> None:
|
|
|
278
280
|
" • Neware: Customized report — check all boxes\n"
|
|
279
281
|
" • Biologic: Export all info to .mpt file\n\n"
|
|
280
282
|
"Use --i for styling, colors, line widths, axis scales, etc.\n"
|
|
281
|
-
"GC from .mpt: requires active mass
|
|
282
|
-
" batplot --gc file.mpt --mass 6.5 --i\n
|
|
283
|
+
"GC from .mpt or .npt: requires active mass to compute mAh g⁻¹ (default unit: mg; use a ``g`` suffix for grams, e.g. --mass 0.0065g).\n"
|
|
284
|
+
" batplot --gc file.mpt --mass 6.5 --i\n"
|
|
285
|
+
" batplot --gc file.npt --mass 10g --i\n\n"
|
|
283
286
|
"GC from supported .csv: specific capacity read directly when available; use --mass for\n"
|
|
284
287
|
" Neware absolute-capacity files (Cycle Index / Step Index / DataPoint format).\n"
|
|
285
288
|
" batplot --gc file.csv\n"
|
|
@@ -289,9 +292,11 @@ def _print_ec_help() -> None:
|
|
|
289
292
|
" batplot f1.csv --mass 3.52 f2.mpt --mass 5.0 --cpc\n"
|
|
290
293
|
" # Files without --mass between them use the global --mass value (or none)\n"
|
|
291
294
|
" # Single --mass applies to all files: batplot f1.mpt f2.mpt --gc --mass 7.0\n\n"
|
|
292
|
-
"dQ/dV from supported .csv (pre-calculated column or computed from GC data):\n"
|
|
295
|
+
"dQ/dV from supported .csv (pre-calculated column or computed from GC data), or from Biologic .mpt/.npt (numerical dQ/dV from GC; requires --mass):\n"
|
|
293
296
|
" batplot --dqdv file.csv\n"
|
|
294
|
-
" batplot --dqdv file.csv --mass 3.52 # Neware absolute-capacity CSV\n
|
|
297
|
+
" batplot --dqdv file.csv --mass 3.52 # Neware absolute-capacity CSV\n"
|
|
298
|
+
" batplot --dqdv file.mpt --mass 6.5\n"
|
|
299
|
+
" batplot --dqdv file.npt --mass 0.01g\n\n"
|
|
295
300
|
"Cyclic voltammetry (CV) from .mpt or .txt: plots potential vs current for each cycle.\n"
|
|
296
301
|
" batplot --cv file.mpt\n"
|
|
297
302
|
" batplot --cv file.txt\n\n"
|
|
@@ -300,7 +305,7 @@ def _print_ec_help() -> None:
|
|
|
300
305
|
" batplot --cpc file.csv # Neware CSV (specific capacity)\n"
|
|
301
306
|
" batplot --cpc file.csv --mass 3.52 # Neware absolute-capacity CSV\n"
|
|
302
307
|
" batplot --cpc file.xlsx # Landt/Lanhe Excel (Chinese tester)\n"
|
|
303
|
-
" batplot --cpc file.mpt --mass 1.2 # Biologic
|
|
308
|
+
" batplot --cpc file.mpt --mass 1.2 # Biologic .mpt / .npt\n"
|
|
304
309
|
" batplot file1.csv --mass 3.52 file2.mpt --mass 1.2 --cpc # Per-file mass\n"
|
|
305
310
|
" batplot --cpc file1.csv file2.xlsx file3.mpt --mass 1.2 --i\n\n"
|
|
306
311
|
"Excel support: Landt/Lanhe (蓝电/蓝河) .xlsx files with Chinese headers:\n"
|
|
@@ -342,6 +347,8 @@ def _print_op_help() -> None:
|
|
|
342
347
|
" batplot --operando --xaxis 2theta # Using 2theta axis\n"
|
|
343
348
|
" batplot --operando --1d --i # Plot derivatives as contour with interactive menu\n"
|
|
344
349
|
" batplot --operando --2d --i # Plot derivatives (alias for --1d)\n\n"
|
|
350
|
+
" batplot --operando --average 2 --i # Average every 2 scans before contouring\n\n"
|
|
351
|
+
" batplot --operando --sum 2 --i # Sum every 2 scans to boost intensity\n\n"
|
|
345
352
|
"Bruker operando (.brml):\n"
|
|
346
353
|
" • Place .brml files (e.g. XX_cyc1.brml, XX_cyc2.brml) in the folder.\n"
|
|
347
354
|
" • Each .brml is expanded into per-scan rows; files sorted by cyc1/cyc2/cyc3.\n"
|
|
@@ -355,6 +362,8 @@ def _print_op_help() -> None:
|
|
|
355
362
|
" • If a .mpt file is present, a side panel is added for dual-panel mode (time/potential/temp/etc.).\n"
|
|
356
363
|
" • Without a .mpt file, operando-only mode shows the contour plot alone.\n"
|
|
357
364
|
" • --1d / --2d: plot the first derivative (dy/dx) of each scan as a contour plot.\n\n"
|
|
365
|
+
" • --average N: average every N consecutive scans to improve S/N (e.g., N=2 averages scans 1+2, 3+4, ...).\n\n"
|
|
366
|
+
" • --sum N: sum every N consecutive scans (same binning as --average, but without division by N).\n\n"
|
|
358
367
|
"Column selection (operando-specific):\n"
|
|
359
368
|
" --readcolc <x> <y> : columns for contour plot (x,y in .xy/.xye/.qye/.dat files)\n"
|
|
360
369
|
" --readcols <x> <y> : columns for side panel (x,y in .mpt file)\n"
|
|
@@ -384,7 +393,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
384
393
|
--------------
|
|
385
394
|
- Positional arguments: 'files' - list of file paths (can be 0 or more)
|
|
386
395
|
- Flags (boolean): '--i' - True if present, False if absent
|
|
387
|
-
- Options with values: '--mass 7.0'
|
|
396
|
+
- Options with values: '--mass 7.0' or '--mass 0.01g' - mass in mg, or grams with a ``g`` suffix
|
|
388
397
|
- Optional arguments: '--help xy' - can have optional value
|
|
389
398
|
|
|
390
399
|
WHY add_help=False?
|
|
@@ -461,9 +470,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
461
470
|
parser.add_argument("--ry", action="store_true", help=argparse.SUPPRESS)
|
|
462
471
|
parser.add_argument("--txaxis", action="store_true", help=argparse.SUPPRESS)
|
|
463
472
|
parser.add_argument("--operando", "--contour", action="store_true", dest="operando", help=argparse.SUPPRESS)
|
|
473
|
+
parser.add_argument("--average", type=int, help=argparse.SUPPRESS)
|
|
474
|
+
parser.add_argument("--sum", dest="scan_sum", type=int, help=argparse.SUPPRESS)
|
|
464
475
|
parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
|
|
465
476
|
parser.add_argument("--gc", action="store_true", help=argparse.SUPPRESS)
|
|
466
|
-
parser.add_argument("--mass", type=
|
|
477
|
+
parser.add_argument("--mass", type=parse_mass_mg_from_cli, action='append', help=argparse.SUPPRESS)
|
|
467
478
|
parser.add_argument("--dqdv", action="store_true", help=argparse.SUPPRESS)
|
|
468
479
|
parser.add_argument("--cv", action="store_true", help=argparse.SUPPRESS)
|
|
469
480
|
parser.add_argument("--cpc", action="store_true", help=argparse.SUPPRESS)
|
|
@@ -28,6 +28,13 @@ from .readers import (
|
|
|
28
28
|
read_biologic_txt_file,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
# BioLogic EC-Lab ASCII exports use ``.mpt``; ``.npt`` is accepted as the same format.
|
|
32
|
+
_MPT_LIKE_EXTS = frozenset({'.mpt', '.npt'})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_mpt_like_ext(ext: str) -> bool:
|
|
36
|
+
return (ext or '').lower() in _MPT_LIKE_EXTS
|
|
37
|
+
|
|
31
38
|
|
|
32
39
|
def _resolve_mass(mass_arg, file_idx: int = 0):
|
|
33
40
|
"""Return mass (mg) for file at file_idx from a --mass list or single value."""
|
|
@@ -488,7 +495,7 @@ def batch_process(directory: str, args):
|
|
|
488
495
|
known_ext = {'.xye', '.xy', '.qye', '.dat', '.csv', '.gr', '.nor', '.chik', '.chir', '.txt', '.brml', '.raw', '.xrdml', '.rasx'}
|
|
489
496
|
|
|
490
497
|
# Extensions to exclude (not data files, or require special handling)
|
|
491
|
-
excluded_ext = {'.cif', '.pkl', '.py', '.md', '.json', '.yml', '.yaml', '.sh', '.bat', '.mpt'}
|
|
498
|
+
excluded_ext = {'.cif', '.pkl', '.py', '.md', '.json', '.yml', '.yaml', '.sh', '.bat', '.mpt', '.npt'}
|
|
492
499
|
|
|
493
500
|
# Create output directory for saved plots
|
|
494
501
|
out_dir = ensure_subdirectory('Figures', directory)
|
|
@@ -842,7 +849,7 @@ def batch_process(directory: str, args):
|
|
|
842
849
|
def batch_process_ec(directory: str, args):
|
|
843
850
|
"""Batch process electrochemistry files in a directory.
|
|
844
851
|
|
|
845
|
-
Supports GC (.mpt/.csv), CV (.mpt),
|
|
852
|
+
Supports GC (.mpt/.npt/.csv), CV (.mpt/.npt/.txt), dQ/dV (.mpt/.npt/.csv), and CPC (.mpt/.npt/.csv) modes.
|
|
846
853
|
Exports SVG plots to batplot_svg subdirectory.
|
|
847
854
|
|
|
848
855
|
Can apply style/geometry from .bps/.bpsg files using --all flag:
|
|
@@ -852,7 +859,7 @@ def batch_process_ec(directory: str, args):
|
|
|
852
859
|
batplot --all --cpc config.bpsg # Apply to all CPC files
|
|
853
860
|
|
|
854
861
|
Note: For GC and CPC modes with .csv files, --mass is not required as the
|
|
855
|
-
capacity data is already in the file. For .mpt files, --mass is required.
|
|
862
|
+
capacity data is already in the file. For .mpt/.npt files, --mass is required (mg by default; use a ``g`` suffix for grams, e.g. ``--mass 0.01g``).
|
|
856
863
|
|
|
857
864
|
Args:
|
|
858
865
|
directory: Directory containing EC files
|
|
@@ -906,19 +913,19 @@ def batch_process_ec(directory: str, args):
|
|
|
906
913
|
mode = None
|
|
907
914
|
if getattr(args, 'gc', False):
|
|
908
915
|
mode = 'gc'
|
|
909
|
-
supported_ext = {'.mpt', '.csv'}
|
|
916
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
910
917
|
elif getattr(args, 'cv', False):
|
|
911
918
|
mode = 'cv'
|
|
912
|
-
supported_ext = {'.mpt', '.txt'}
|
|
919
|
+
supported_ext = {'.mpt', '.npt', '.txt'}
|
|
913
920
|
elif getattr(args, 'dqdv', False):
|
|
914
921
|
mode = 'dqdv'
|
|
915
|
-
supported_ext = {'.csv'}
|
|
922
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
916
923
|
elif getattr(args, 'cpc', False):
|
|
917
924
|
mode = 'cpc'
|
|
918
|
-
supported_ext = {'.mpt', '.csv'}
|
|
925
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
919
926
|
elif getattr(args, 'epc', False):
|
|
920
927
|
mode = 'epc'
|
|
921
|
-
supported_ext = {'.mpt', '.csv'}
|
|
928
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
922
929
|
else:
|
|
923
930
|
print("EC batch mode requires one of: --gc, --cv, --dqdv, or --cpc")
|
|
924
931
|
return
|
|
@@ -1007,9 +1014,9 @@ def batch_process_ec(directory: str, args):
|
|
|
1007
1014
|
|
|
1008
1015
|
# ---- GC Mode ----
|
|
1009
1016
|
if mode == 'gc':
|
|
1010
|
-
if ext
|
|
1017
|
+
if _is_mpt_like_ext(ext):
|
|
1011
1018
|
if mass_mg is None:
|
|
1012
|
-
print(f" Skipped {fname}: GC mode (.mpt) requires --mass parameter")
|
|
1019
|
+
print(f" Skipped {fname}: GC mode (.mpt/.npt) requires --mass parameter")
|
|
1013
1020
|
plt.close(fig_b)
|
|
1014
1021
|
continue
|
|
1015
1022
|
specific_capacity, voltage, cycle_numbers, charge_mask, discharge_mask = cast(
|
|
@@ -1089,13 +1096,13 @@ def batch_process_ec(directory: str, args):
|
|
|
1089
1096
|
elif mode == 'cv':
|
|
1090
1097
|
if ext == '.txt':
|
|
1091
1098
|
voltage, current, cycles = read_biologic_txt_file(fpath, mode='cv')
|
|
1092
|
-
elif ext
|
|
1099
|
+
elif _is_mpt_like_ext(ext):
|
|
1093
1100
|
voltage, current, cycles = cast(
|
|
1094
1101
|
Tuple[np.ndarray, np.ndarray, np.ndarray],
|
|
1095
1102
|
read_mpt_file(fpath, mode='cv'),
|
|
1096
1103
|
)
|
|
1097
1104
|
else:
|
|
1098
|
-
raise ValueError("CV mode requires .mpt or .txt file")
|
|
1105
|
+
raise ValueError("CV mode requires .mpt, .npt, or .txt file")
|
|
1099
1106
|
|
|
1100
1107
|
cyc_int_raw = np.array(np.rint(cycles), dtype=int)
|
|
1101
1108
|
if cyc_int_raw.size:
|
|
@@ -1125,13 +1132,27 @@ def batch_process_ec(directory: str, args):
|
|
|
1125
1132
|
|
|
1126
1133
|
# ---- dQdV Mode ----
|
|
1127
1134
|
elif mode == 'dqdv':
|
|
1128
|
-
if ext != '.csv':
|
|
1129
|
-
raise ValueError("dQdV mode requires .csv file")
|
|
1130
|
-
|
|
1131
1135
|
# Try to load pre-calculated dQ/dV columns; fall back to numerical computation
|
|
1132
1136
|
_b_dqdv_header = None
|
|
1133
1137
|
_b_loaded = False
|
|
1134
|
-
if
|
|
1138
|
+
if _is_mpt_like_ext(ext):
|
|
1139
|
+
if mass_mg is None or mass_mg <= 0:
|
|
1140
|
+
print(f" Skipped {fname}: dQ/dV (.mpt/.npt) requires --mass parameter")
|
|
1141
|
+
plt.close(fig_b)
|
|
1142
|
+
continue
|
|
1143
|
+
_b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm = cast(
|
|
1144
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray],
|
|
1145
|
+
read_mpt_file(fpath, mode='gc', mass_mg=mass_mg),
|
|
1146
|
+
)
|
|
1147
|
+
voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
|
|
1148
|
+
compute_dqdv_numerical(
|
|
1149
|
+
_b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm,
|
|
1150
|
+
)
|
|
1151
|
+
_b_loaded = True
|
|
1152
|
+
print(f"dQ/dV batch: computing numerically from GC data for {fname!r}.")
|
|
1153
|
+
elif ext != '.csv':
|
|
1154
|
+
raise ValueError(f"dQ/dV mode requires .csv or Biologic .mpt/.npt file, got {ext}")
|
|
1155
|
+
elif is_biologic_datalogger_csv(fpath):
|
|
1135
1156
|
if mass_mg is None or mass_mg <= 0:
|
|
1136
1157
|
print(f" Skipped {fname}: dQ/dV (Biologic DataLogger CSV) requires --mass parameter")
|
|
1137
1158
|
plt.close(fig_b)
|
|
@@ -1164,7 +1185,7 @@ def batch_process_ec(directory: str, args):
|
|
|
1164
1185
|
if _b_mass and _b_mass > 0:
|
|
1165
1186
|
_b_gc_cap = _b_gc_cap * (1000.0 / float(_b_mass))
|
|
1166
1187
|
else:
|
|
1167
|
-
print(f"dQ/dV batch: {fname!r} — pass --mass
|
|
1188
|
+
print(f"dQ/dV batch: {fname!r} — pass --mass (mg, or e.g. 0.01g) for specific dQ/dV.")
|
|
1168
1189
|
voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
|
|
1169
1190
|
compute_dqdv_numerical(_b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm)
|
|
1170
1191
|
print(f"dQ/dV batch: computing numerically from GC data for {fname!r}.")
|
|
@@ -1219,9 +1240,9 @@ def batch_process_ec(directory: str, args):
|
|
|
1219
1240
|
|
|
1220
1241
|
# ---- CPC / EPC Mode ----
|
|
1221
1242
|
elif mode in ('cpc', 'epc'):
|
|
1222
|
-
if ext
|
|
1243
|
+
if _is_mpt_like_ext(ext):
|
|
1223
1244
|
if mass_mg is None:
|
|
1224
|
-
print(f" Skipped {fname}: {mode.upper()} mode (.mpt) requires --mass parameter")
|
|
1245
|
+
print(f" Skipped {fname}: {mode.upper()} mode (.mpt/.npt) requires --mass parameter")
|
|
1225
1246
|
plt.close(fig_b)
|
|
1226
1247
|
continue
|
|
1227
1248
|
if mode == 'cpc':
|
|
@@ -106,6 +106,39 @@ try:
|
|
|
106
106
|
except ImportError:
|
|
107
107
|
operando_ec_interactive_menu = None
|
|
108
108
|
|
|
109
|
+
|
|
110
|
+
def _run_saved_dqdv_2d_companion(fig, sess_path: str) -> None:
|
|
111
|
+
"""If load_ec_session attached a saved dQ/dV 2D bundle, run its operando menu after the EC menu."""
|
|
112
|
+
b = getattr(fig, "_dqdv_2d_companion_bundle", None)
|
|
113
|
+
if not b or len(b) < 4:
|
|
114
|
+
return
|
|
115
|
+
cfig, cax, im, cbar = b[0], b[1], b[2], b[3]
|
|
116
|
+
if operando_ec_interactive_menu is None:
|
|
117
|
+
print("Operando interactive not available; skipping saved dQ/dV 2D map.")
|
|
118
|
+
return
|
|
119
|
+
paths = list(getattr(fig, "_bp_source_paths", []) or [])
|
|
120
|
+
if not paths:
|
|
121
|
+
paths = [sess_path]
|
|
122
|
+
print("\nSession includes a saved dQ/dV 2D map — opening contour interactive menu (q exits to finish).")
|
|
123
|
+
try:
|
|
124
|
+
plt.show(block=False)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
try:
|
|
128
|
+
operando_ec_interactive_menu(cfig, cax, im, cbar, None, file_paths=paths, canvas_mode=False)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"Saved dQ/dV 2D map menu failed: {e}")
|
|
131
|
+
finally:
|
|
132
|
+
try:
|
|
133
|
+
plt.close(cfig)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
try:
|
|
137
|
+
delattr(fig, "_dqdv_2d_companion_bundle")
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
109
142
|
try:
|
|
110
143
|
from .cpc_interactive import cpc_interactive_menu, _generate_similar_color, _build_compact_cpc_legend
|
|
111
144
|
except ImportError:
|
|
@@ -2790,6 +2823,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
2790
2823
|
electrochem_interactive_menu(fig, ax, cycle_lines, file_path=sess_path)
|
|
2791
2824
|
except Exception as _ie:
|
|
2792
2825
|
print(f"Interactive menu failed: {_ie}")
|
|
2826
|
+
try:
|
|
2827
|
+
_run_saved_dqdv_2d_companion(fig, sess_path)
|
|
2828
|
+
except Exception as _c2d:
|
|
2829
|
+
print(f"Saved dQ/dV 2D companion: {_c2d}")
|
|
2793
2830
|
plt.show()
|
|
2794
2831
|
exit()
|
|
2795
2832
|
except Exception as e:
|
|
@@ -104,6 +104,37 @@ def ensure_colormap(name: Optional[str]) -> bool:
|
|
|
104
104
|
"""
|
|
105
105
|
if not name:
|
|
106
106
|
return False
|
|
107
|
+
|
|
108
|
+
def _register_cmap_safe(cmap_name: str, cmap_obj) -> bool:
|
|
109
|
+
"""Register cmap across matplotlib API variants."""
|
|
110
|
+
# matplotlib >= 3.5 style registry API
|
|
111
|
+
try:
|
|
112
|
+
reg = getattr(plt, "colormaps", None)
|
|
113
|
+
if reg is not None and hasattr(reg, "register"):
|
|
114
|
+
try:
|
|
115
|
+
reg.register(cmap_obj, name=cmap_name, force=True)
|
|
116
|
+
except TypeError:
|
|
117
|
+
# Older signature may not support force kwarg
|
|
118
|
+
reg.register(cmap_obj, name=cmap_name)
|
|
119
|
+
return True
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
# Older pyplot API fallback
|
|
123
|
+
try:
|
|
124
|
+
if hasattr(plt, "register_cmap"):
|
|
125
|
+
plt.register_cmap(name=cmap_name, cmap=cmap_obj)
|
|
126
|
+
return True
|
|
127
|
+
except ValueError:
|
|
128
|
+
# Already registered
|
|
129
|
+
return True
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
# As a last resort, accept cmap object usability even if registration fails.
|
|
133
|
+
try:
|
|
134
|
+
_ = cmap_obj(0.5)
|
|
135
|
+
return True
|
|
136
|
+
except Exception:
|
|
137
|
+
return False
|
|
107
138
|
|
|
108
139
|
# Handle reversed colormaps (remove '_r' suffix to get base name)
|
|
109
140
|
# Example: 'viridis_r' → base = 'viridis', we'll reverse it later if needed
|
|
@@ -120,13 +151,7 @@ def ensure_colormap(name: Optional[str]) -> bool:
|
|
|
120
151
|
import cmcrameri.cm as cmc
|
|
121
152
|
if hasattr(cmc, base_lower):
|
|
122
153
|
cmap_obj = getattr(cmc, base_lower)
|
|
123
|
-
|
|
124
|
-
# Register it with matplotlib so it can be used like built-in colormaps
|
|
125
|
-
plt.register_cmap(name=base_lower, cmap=cmap_obj)
|
|
126
|
-
except ValueError:
|
|
127
|
-
# Already registered, that's fine
|
|
128
|
-
pass
|
|
129
|
-
return True
|
|
154
|
+
return _register_cmap_safe(base_lower, cmap_obj)
|
|
130
155
|
except Exception:
|
|
131
156
|
# cmcrameri not installed or colormap not found, continue to next step
|
|
132
157
|
pass
|
|
@@ -139,13 +164,7 @@ def ensure_colormap(name: Optional[str]) -> bool:
|
|
|
139
164
|
# N=256 means create 256 intermediate colors by interpolating between the given colors
|
|
140
165
|
# This creates a smooth gradient
|
|
141
166
|
cmap_obj = LinearSegmentedColormap.from_list(base_lower, custom, N=256)
|
|
142
|
-
|
|
143
|
-
# Register with matplotlib
|
|
144
|
-
plt.register_cmap(name=base_lower, cmap=cmap_obj)
|
|
145
|
-
except ValueError:
|
|
146
|
-
# Already registered, that's fine
|
|
147
|
-
pass
|
|
148
|
-
return True
|
|
167
|
+
return _register_cmap_safe(base_lower, cmap_obj)
|
|
149
168
|
except Exception:
|
|
150
169
|
return False
|
|
151
170
|
|
|
@@ -18,6 +18,10 @@ Example config.json structure:
|
|
|
18
18
|
"#0000FF",
|
|
19
19
|
"red",
|
|
20
20
|
"blue"
|
|
21
|
+
],
|
|
22
|
+
"recent_axis_names": [
|
|
23
|
+
"Potential (V)",
|
|
24
|
+
"dQ/dV (mAh V$^{-1}$)"
|
|
21
25
|
]
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -192,7 +196,47 @@ def save_user_colors(colors: List[str]) -> None:
|
|
|
192
196
|
save_config(config) # Save entire config back to file
|
|
193
197
|
|
|
194
198
|
|
|
199
|
+
_RECENT_AXIS_NAMES_KEY = 'recent_axis_names'
|
|
200
|
+
RECENT_AXIS_NAMES_MAX = 20
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_recent_axis_names() -> List[str]:
|
|
204
|
+
"""Return up to :data:`RECENT_AXIS_NAMES_MAX` recently typed axis labels (newest first)."""
|
|
205
|
+
raw = load_config().get(_RECENT_AXIS_NAMES_KEY, [])
|
|
206
|
+
if not isinstance(raw, list):
|
|
207
|
+
return []
|
|
208
|
+
out: List[str] = []
|
|
209
|
+
for item in raw:
|
|
210
|
+
s = str(item).strip()
|
|
211
|
+
if s and s not in out:
|
|
212
|
+
out.append(s)
|
|
213
|
+
if len(out) >= RECENT_AXIS_NAMES_MAX:
|
|
214
|
+
break
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def record_recent_axis_name(name: str) -> None:
|
|
219
|
+
"""Add an axis label to the shared recent list (dedupe, newest first, max 20)."""
|
|
220
|
+
s = str(name or '').strip()
|
|
221
|
+
if not s:
|
|
222
|
+
return
|
|
223
|
+
config = load_config()
|
|
224
|
+
raw = config.get(_RECENT_AXIS_NAMES_KEY, [])
|
|
225
|
+
names: List[str] = []
|
|
226
|
+
if isinstance(raw, list):
|
|
227
|
+
for item in raw:
|
|
228
|
+
t = str(item).strip()
|
|
229
|
+
if t and t != s:
|
|
230
|
+
names.append(t)
|
|
231
|
+
names.insert(0, s)
|
|
232
|
+
config[_RECENT_AXIS_NAMES_KEY] = names[:RECENT_AXIS_NAMES_MAX]
|
|
233
|
+
save_config(config)
|
|
234
|
+
|
|
235
|
+
|
|
195
236
|
__all__ = [
|
|
196
237
|
'get_user_colors',
|
|
197
238
|
'save_user_colors',
|
|
239
|
+
'get_recent_axis_names',
|
|
240
|
+
'record_recent_axis_name',
|
|
241
|
+
'RECENT_AXIS_NAMES_MAX',
|
|
198
242
|
]
|