batplot 1.8.37__tar.gz → 1.8.39__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.37/batplot.egg-info → batplot-1.8.39}/PKG-INFO +1 -1
- {batplot-1.8.37 → batplot-1.8.39}/batplot/__init__.py +1 -1
- {batplot-1.8.37 → batplot-1.8.39}/batplot/args.py +2 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/batplot.py +33 -11
- {batplot-1.8.37 → batplot-1.8.39}/batplot/cpc_interactive.py +44 -35
- {batplot-1.8.37 → batplot-1.8.39}/batplot/data/CHANGELOG.md +8 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/interactive.py +144 -65
- {batplot-1.8.37 → batplot-1.8.39}/batplot/operando.py +62 -55
- {batplot-1.8.37 → batplot-1.8.39}/batplot/style.py +13 -1
- {batplot-1.8.37 → batplot-1.8.39}/batplot/version_check.py +2 -2
- {batplot-1.8.37 → batplot-1.8.39/batplot.egg-info}/PKG-INFO +1 -1
- {batplot-1.8.37 → batplot-1.8.39}/pyproject.toml +1 -1
- {batplot-1.8.37 → batplot-1.8.39}/LICENSE +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/MANIFEST.in +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/NOTICE +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/README.md +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/batch.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/canvas_interactive.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/cif.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/cli.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/color_utils.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/config.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/converters.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/data/USER_MANUAL.md +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/dev_upgrade.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/electrochem_interactive.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/manual.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/modes.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/operando_ec_interactive.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/plotting.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/readers.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/session.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/showcol.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/ui.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot/utils.py +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/setup.cfg +0 -0
- {batplot-1.8.37 → batplot-1.8.39}/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.39
|
|
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
|
|
@@ -234,6 +234,8 @@ def _print_xy_help() -> None:
|
|
|
234
234
|
" batplot f1.txt --readcol 2 3 f2.txt --readcol 5 6 --convert 1.54 q\n"
|
|
235
235
|
" Directory: pass a folder to convert all .xy/.xye/.qye/.dat/.csv/.txt files:\n"
|
|
236
236
|
" batplot /path/to/folder --convert 0.25448 1.54\n"
|
|
237
|
+
" Batch in current folder: use allfiles token (non-convertible files are skipped):\n"
|
|
238
|
+
" batplot allfiles --convert q 1.54\n"
|
|
237
239
|
" Examples:\n"
|
|
238
240
|
" batplot file.xye --convert 1.54 0.25\n"
|
|
239
241
|
" batplot file.xye --convert 1.54 q\n"
|
|
@@ -2624,7 +2624,11 @@ def batplot_main() -> int: # type: ignore
|
|
|
2624
2624
|
expanded = []
|
|
2625
2625
|
for p in args.files:
|
|
2626
2626
|
if os.path.isfile(p):
|
|
2627
|
-
|
|
2627
|
+
ext = os.path.splitext(p)[1].lower()
|
|
2628
|
+
if ext in convert_ext:
|
|
2629
|
+
expanded.append(p)
|
|
2630
|
+
else:
|
|
2631
|
+
print(f"Warning: Skipping non-convertible file: {p}")
|
|
2628
2632
|
elif os.path.isdir(p):
|
|
2629
2633
|
for f in sorted(os.listdir(p), key=natural_sort_key):
|
|
2630
2634
|
fp = os.path.join(p, f)
|
|
@@ -3351,6 +3355,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
3351
3355
|
stack_label_bottom = bool(sess.get('stack_label_at_bottom', False))
|
|
3352
3356
|
update_labels(ax, y_data_list, label_text_objects, saved_stack, stack_label_bottom)
|
|
3353
3357
|
if cif_tick_series:
|
|
3358
|
+
try:
|
|
3359
|
+
fig._batplot_cif_tick_series = cif_tick_series
|
|
3360
|
+
except Exception:
|
|
3361
|
+
pass
|
|
3354
3362
|
# Provide draw/extend helpers compatible with interactive menu using original placement logic
|
|
3355
3363
|
def _session_q_to_2theta(peaksQ, wl):
|
|
3356
3364
|
if wl is None:
|
|
@@ -3364,7 +3372,8 @@ def batplot_main() -> int: # type: ignore
|
|
|
3364
3372
|
|
|
3365
3373
|
def _session_ensure_wavelength(default_wl=1.5406):
|
|
3366
3374
|
# Prefer any stored wl, else args.wl, else provided default
|
|
3367
|
-
|
|
3375
|
+
_ser = getattr(fig, '_batplot_cif_tick_series', None) or cif_tick_series
|
|
3376
|
+
for _lab,_fname,_peaks,_wl,_qmax,_color in _ser:
|
|
3368
3377
|
if _wl is not None:
|
|
3369
3378
|
return _wl
|
|
3370
3379
|
return getattr(args, 'wl', None) or default_wl
|
|
@@ -3374,7 +3383,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
3374
3383
|
return
|
|
3375
3384
|
|
|
3376
3385
|
def _session_cif_draw():
|
|
3377
|
-
|
|
3386
|
+
cif_series_draw = getattr(fig, '_batplot_cif_tick_series', None)
|
|
3387
|
+
if cif_series_draw is None:
|
|
3388
|
+
cif_series_draw = cif_tick_series
|
|
3389
|
+
if not cif_series_draw:
|
|
3378
3390
|
return
|
|
3379
3391
|
try:
|
|
3380
3392
|
# Preserve current limits before drawing - use actual current limits
|
|
@@ -3435,7 +3447,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
3435
3447
|
stacked_or_multi_y=_stacked_s,
|
|
3436
3448
|
)
|
|
3437
3449
|
_cif_bottom_m = xy_cif_stack_bottom_margin_yr(fixed_yr, show_titles=show_titles_local)
|
|
3438
|
-
needed_min = base - (len(
|
|
3450
|
+
needed_min = base - (len(cif_series_draw) - 1) * spacing - _cif_bottom_m
|
|
3439
3451
|
if not show_titles_local:
|
|
3440
3452
|
ylim_draw = tuple(prev_ylim)
|
|
3441
3453
|
elif needed_min >= prev_ylim[0]:
|
|
@@ -3457,7 +3469,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
3457
3469
|
wl_any = _session_ensure_wavelength()
|
|
3458
3470
|
|
|
3459
3471
|
# Draw each series
|
|
3460
|
-
for i,(lab,fname,peaksQ,wl,qmax_sim,color) in enumerate(
|
|
3472
|
+
for i,(lab,fname,peaksQ,wl,qmax_sim,color) in enumerate(cif_series_draw):
|
|
3461
3473
|
y_line = base - i * spacing + xy_cif_stack_y_offset(fig, i)
|
|
3462
3474
|
tick_h, hkl_y = xy_cif_tick_stack_layout(y_line, yr)
|
|
3463
3475
|
# Convert peaks to axis domain
|
|
@@ -3621,7 +3633,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
3621
3633
|
cif_globals_dict = None
|
|
3622
3634
|
if cif_tick_series:
|
|
3623
3635
|
cif_globals_dict = {
|
|
3624
|
-
'cif_tick_series':
|
|
3636
|
+
'cif_tick_series': cif_tick_series,
|
|
3625
3637
|
'cif_hkl_map': cif_hkl_map,
|
|
3626
3638
|
'cif_hkl_label_map': cif_hkl_label_map,
|
|
3627
3639
|
'show_cif_hkl': bool(show_cif_hkl),
|
|
@@ -4457,7 +4469,13 @@ def batplot_main() -> int: # type: ignore
|
|
|
4457
4469
|
draw_cif_ticks()
|
|
4458
4470
|
|
|
4459
4471
|
def draw_cif_ticks():
|
|
4460
|
-
|
|
4472
|
+
# Interactive menu mutates _bp.cif_tick_series; session/menu paths may use a
|
|
4473
|
+
# different list than this closure. fig._batplot_cif_tick_series stays synced
|
|
4474
|
+
# from interactive_menu so redraw sees renames (r→t), reorder, colors, etc.
|
|
4475
|
+
cif_series_draw = getattr(fig, '_batplot_cif_tick_series', None)
|
|
4476
|
+
if cif_series_draw is None:
|
|
4477
|
+
cif_series_draw = cif_tick_series
|
|
4478
|
+
if not cif_series_draw:
|
|
4461
4479
|
return
|
|
4462
4480
|
# Preserve current limits before drawing - use actual current limits
|
|
4463
4481
|
# to prevent any movement when toggling
|
|
@@ -4492,13 +4510,13 @@ def batplot_main() -> int: # type: ignore
|
|
|
4492
4510
|
_bp_module = sys.modules.get('__main__')
|
|
4493
4511
|
if _bp_module is not None and hasattr(_bp_module, 'cif_set_visible'):
|
|
4494
4512
|
vis = list(getattr(_bp_module, 'cif_set_visible') or [])
|
|
4495
|
-
if len(vis) == len(
|
|
4513
|
+
if len(vis) == len(cif_series_draw):
|
|
4496
4514
|
set_visible = [bool(v) for v in vis]
|
|
4497
4515
|
except Exception:
|
|
4498
4516
|
pass
|
|
4499
4517
|
# Effective number of visible CIF rows (for spacing and y-limit expansion)
|
|
4500
4518
|
if set_visible is None:
|
|
4501
|
-
n_rows = len(
|
|
4519
|
+
n_rows = len(cif_series_draw)
|
|
4502
4520
|
else:
|
|
4503
4521
|
n_rows = max(1, sum(1 for v in set_visible if v))
|
|
4504
4522
|
|
|
@@ -4571,7 +4589,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
4571
4589
|
except Exception:
|
|
4572
4590
|
pass
|
|
4573
4591
|
visible_idx = 0
|
|
4574
|
-
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(
|
|
4592
|
+
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(cif_series_draw):
|
|
4575
4593
|
if set_visible is not None and i < len(set_visible) and not set_visible[i]:
|
|
4576
4594
|
continue
|
|
4577
4595
|
y_line = base - visible_idx * spacing + xy_cif_stack_y_offset(fig, i)
|
|
@@ -4651,7 +4669,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
4651
4669
|
hover_meta = []
|
|
4652
4670
|
show_hkl = globals().get('show_cif_hkl', False)
|
|
4653
4671
|
# Build mapping from Q to label text if available
|
|
4654
|
-
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(
|
|
4672
|
+
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(cif_series_draw):
|
|
4655
4673
|
if use_2th and wl is None:
|
|
4656
4674
|
wl = getattr(ax, '_cif_hover_wl', None)
|
|
4657
4675
|
# Recreate domain peaks consistent with those drawn (limit to view)
|
|
@@ -4738,6 +4756,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
4738
4756
|
ax._cif_hover_cid = cid
|
|
4739
4757
|
|
|
4740
4758
|
if cif_tick_series:
|
|
4759
|
+
try:
|
|
4760
|
+
fig._batplot_cif_tick_series = cif_tick_series
|
|
4761
|
+
except Exception:
|
|
4762
|
+
pass
|
|
4741
4763
|
# Auto-assign distinct colors for CIF tick series.
|
|
4742
4764
|
# For multiple CIF series:
|
|
4743
4765
|
# - If <= 10 files, use 'tab10' but in a re-ordered sequence to
|
|
@@ -149,21 +149,17 @@ def _legend_no_frame(ax, *args, **kwargs):
|
|
|
149
149
|
kwargs.setdefault('columnspacing', 0.6)
|
|
150
150
|
# Don't use labelcolor='linecolor' by default as it causes issues
|
|
151
151
|
# with scatter plots that have facecolor='none' (hollow markers)
|
|
152
|
-
|
|
152
|
+
legend_host_ax = kwargs.pop('legend_host_ax', None)
|
|
153
|
+
target_ax = legend_host_ax if legend_host_ax is not None else ax
|
|
154
|
+
leg = target_ax.legend(*args, **kwargs)
|
|
153
155
|
if leg is not None:
|
|
154
156
|
try:
|
|
155
157
|
leg.set_frame_on(False)
|
|
158
|
+
# Keep legend above plot artists on the hosting axis.
|
|
159
|
+
leg.set_zorder(1_000_000)
|
|
160
|
+
leg.set_clip_on(False)
|
|
156
161
|
for t in leg.get_texts():
|
|
157
162
|
t.set_verticalalignment('center')
|
|
158
|
-
# Nudge text up so it aligns with the symbol handle (patches sit higher than text baseline)
|
|
159
|
-
try:
|
|
160
|
-
sizes = [t.get_fontsize() for t in leg.get_texts() if t.get_text().strip()]
|
|
161
|
-
fs = float(np.mean(sizes)) if sizes else 10.0
|
|
162
|
-
shift_pts = fs * 0.5 # Points to move text up (was 0.15, increased for proper alignment)
|
|
163
|
-
for t in leg.get_texts():
|
|
164
|
-
t.set_position((0, shift_pts))
|
|
165
|
-
except Exception:
|
|
166
|
-
pass
|
|
167
163
|
except Exception:
|
|
168
164
|
pass
|
|
169
165
|
return leg
|
|
@@ -540,13 +536,16 @@ def _rebuild_legend(ax, ax2, file_data, preserve_position=True):
|
|
|
540
536
|
_legend_no_frame(ax, h_all, l_all, loc='center',
|
|
541
537
|
bbox_to_anchor=(fx, fy),
|
|
542
538
|
bbox_transform=fig.transFigure,
|
|
543
|
-
borderaxespad=1.0, title=leg_title
|
|
539
|
+
borderaxespad=1.0, title=leg_title,
|
|
540
|
+
legend_host_ax=ax2)
|
|
544
541
|
except Exception:
|
|
545
542
|
_legend_no_frame(ax, h_all, l_all, loc='best',
|
|
546
|
-
borderaxespad=1.0, title=leg_title
|
|
543
|
+
borderaxespad=1.0, title=leg_title,
|
|
544
|
+
legend_host_ax=ax2)
|
|
547
545
|
else:
|
|
548
546
|
_legend_no_frame(ax, h_all, l_all, loc='best',
|
|
549
|
-
borderaxespad=1.0, title=leg_title
|
|
547
|
+
borderaxespad=1.0, title=leg_title,
|
|
548
|
+
legend_host_ax=ax2)
|
|
550
549
|
else:
|
|
551
550
|
# Single-file: standard handles from axes
|
|
552
551
|
try:
|
|
@@ -570,13 +569,16 @@ def _rebuild_legend(ax, ax2, file_data, preserve_position=True):
|
|
|
570
569
|
_legend_no_frame(ax, h_all, l_all, loc='center',
|
|
571
570
|
bbox_to_anchor=(fx, fy),
|
|
572
571
|
bbox_transform=fig.transFigure,
|
|
573
|
-
borderaxespad=1.0, title=leg_title
|
|
572
|
+
borderaxespad=1.0, title=leg_title,
|
|
573
|
+
legend_host_ax=ax2)
|
|
574
574
|
except Exception:
|
|
575
575
|
_legend_no_frame(ax, h_all, l_all, loc='best',
|
|
576
|
-
borderaxespad=1.0, title=leg_title
|
|
576
|
+
borderaxespad=1.0, title=leg_title,
|
|
577
|
+
legend_host_ax=ax2)
|
|
577
578
|
else:
|
|
578
579
|
_legend_no_frame(ax, h_all, l_all, loc='best',
|
|
579
|
-
borderaxespad=1.0, title=leg_title
|
|
580
|
+
borderaxespad=1.0, title=leg_title,
|
|
581
|
+
legend_host_ax=ax2)
|
|
580
582
|
else:
|
|
581
583
|
leg = ax.get_legend()
|
|
582
584
|
if leg:
|
|
@@ -654,13 +656,16 @@ def _build_compact_cpc_legend(ax, ax2, file_data, xy_in=None, leg_title=None):
|
|
|
654
656
|
pass
|
|
655
657
|
|
|
656
658
|
# --- Build proxy handles and labels ---
|
|
657
|
-
|
|
658
|
-
#
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
659
|
+
# Use scatter proxy handles so legend uses scatter-specific vertical alignment
|
|
660
|
+
# controls (scatterpoints/scatteryoffsets), which is more stable here.
|
|
661
|
+
ms2 = 28.0 # marker area in points^2
|
|
662
|
+
chg_handle = ax.scatter([], [], marker='s', s=ms2,
|
|
663
|
+
facecolors='#444444', edgecolors='#444444',
|
|
664
|
+
linewidths=1.0, label='Charge')
|
|
665
|
+
dch_handle = ax.scatter([], [], marker='s', s=ms2,
|
|
666
|
+
facecolors='none', edgecolors='#444444',
|
|
667
|
+
linewidths=1.2, label='Discharge')
|
|
668
|
+
handles = [chg_handle, dch_handle]
|
|
664
669
|
labels = ['Charge', 'Discharge']
|
|
665
670
|
|
|
666
671
|
# Header: efficiency triangle if visible
|
|
@@ -680,33 +685,34 @@ def _build_compact_cpc_legend(ax, ax2, file_data, xy_in=None, leg_title=None):
|
|
|
680
685
|
pass
|
|
681
686
|
except Exception:
|
|
682
687
|
eff_color = '#888888'
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
markersize=4, label='Efficiency')
|
|
688
|
+
eff_handle = ax.scatter([], [], marker='^', s=ms2,
|
|
689
|
+
facecolors=eff_color, edgecolors=eff_color,
|
|
690
|
+
linewidths=1.0, label='Efficiency')
|
|
687
691
|
handles.append(eff_handle)
|
|
688
692
|
labels.append('Efficiency')
|
|
689
693
|
|
|
690
|
-
# Separator: invisible
|
|
691
|
-
sep =
|
|
694
|
+
# Separator: invisible marker handle with empty label to create visual gap
|
|
695
|
+
sep = ax.scatter([], [], s=0.0, alpha=0.0, label='')
|
|
692
696
|
handles.append(sep)
|
|
693
697
|
labels.append('')
|
|
694
698
|
|
|
695
699
|
# Per-file rows
|
|
696
700
|
for color, fname in file_rows:
|
|
697
|
-
|
|
698
|
-
|
|
701
|
+
file_handle = ax.scatter([], [], marker='s', s=ms2,
|
|
702
|
+
facecolors=color, edgecolors=color,
|
|
703
|
+
linewidths=1.0, label=fname)
|
|
704
|
+
handles.append(file_handle)
|
|
699
705
|
labels.append(fname)
|
|
700
706
|
|
|
701
707
|
if not file_rows:
|
|
702
708
|
return # Nothing to show
|
|
703
709
|
|
|
704
710
|
fig = ax.figure
|
|
705
|
-
# Multi-file
|
|
711
|
+
# Multi-file compact legend with scatter-specific alignment controls.
|
|
706
712
|
_hl = 0.35
|
|
707
713
|
_legend_kw = dict(
|
|
708
714
|
handlelength=_hl, handleheight=_hl, borderaxespad=1.0, title=leg_title,
|
|
709
|
-
|
|
715
|
+
scatterpoints=1, scatteryoffsets=[0.5],
|
|
710
716
|
)
|
|
711
717
|
if xy_in is not None:
|
|
712
718
|
try:
|
|
@@ -717,11 +723,14 @@ def _build_compact_cpc_legend(ax, ax2, file_data, xy_in=None, leg_title=None):
|
|
|
717
723
|
loc='center',
|
|
718
724
|
bbox_to_anchor=(fx, fy),
|
|
719
725
|
bbox_transform=fig.transFigure,
|
|
726
|
+
legend_host_ax=ax2,
|
|
720
727
|
**_legend_kw)
|
|
721
728
|
except Exception:
|
|
722
|
-
_legend_no_frame(ax, handles, labels, loc='best',
|
|
729
|
+
_legend_no_frame(ax, handles, labels, loc='best',
|
|
730
|
+
legend_host_ax=ax2, **_legend_kw)
|
|
723
731
|
else:
|
|
724
|
-
_legend_no_frame(ax, handles, labels, loc='best',
|
|
732
|
+
_legend_no_frame(ax, handles, labels, loc='best',
|
|
733
|
+
legend_host_ax=ax2, **_legend_kw)
|
|
725
734
|
|
|
726
735
|
|
|
727
736
|
def _get_geometry_snapshot(ax, ax2) -> Dict:
|
|
@@ -91,10 +91,12 @@ class _FilterIMKWarning:
|
|
|
91
91
|
self.original_stderr.flush()
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def _safe_input(prompt: str = "") -> str:
|
|
94
|
+
def _safe_input(prompt: str = "", *, cancel_on_interrupt: bool = True) -> str:
|
|
95
95
|
"""Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
|
|
97
|
+
On **Ctrl+C** (or EOF on stdin), returns ``""`` by default so prompts behave like cancel
|
|
98
|
+
and the interactive menu keeps running instead of exiting with a traceback.
|
|
99
|
+
Set ``cancel_on_interrupt=False`` to re-raise (e.g. tests).
|
|
98
100
|
"""
|
|
99
101
|
# Filter stderr to hide macOS IMK warnings while preserving other errors
|
|
100
102
|
original_stderr = sys.stderr
|
|
@@ -102,7 +104,21 @@ def _safe_input(prompt: str = "") -> str:
|
|
|
102
104
|
try:
|
|
103
105
|
result = input(prompt)
|
|
104
106
|
return result
|
|
105
|
-
except
|
|
107
|
+
except KeyboardInterrupt:
|
|
108
|
+
if cancel_on_interrupt:
|
|
109
|
+
try:
|
|
110
|
+
print()
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
return ""
|
|
114
|
+
raise
|
|
115
|
+
except EOFError:
|
|
116
|
+
if cancel_on_interrupt:
|
|
117
|
+
try:
|
|
118
|
+
print()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
return ""
|
|
106
122
|
raise
|
|
107
123
|
finally:
|
|
108
124
|
sys.stderr = original_stderr
|
|
@@ -161,6 +177,70 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
161
177
|
# Provide a consistent interface for accessing CIF state
|
|
162
178
|
_bp = type('CIFState', (), cif_globals)() if cif_globals else None
|
|
163
179
|
|
|
180
|
+
def _sync_fig_cif_tick_series():
|
|
181
|
+
"""Keep fig._batplot_cif_tick_series aligned with menu state for CIF redraw."""
|
|
182
|
+
if _bp is None:
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
_cts = getattr(_bp, 'cif_tick_series', None)
|
|
186
|
+
if _cts is not None:
|
|
187
|
+
fig._batplot_cif_tick_series = _cts
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
_sync_fig_cif_tick_series()
|
|
192
|
+
|
|
193
|
+
def _cif_series_for_session():
|
|
194
|
+
"""CIF list for save (s), export (p), and undo snapshot: same as redraw (fig-backed)."""
|
|
195
|
+
try:
|
|
196
|
+
c = getattr(fig, '_batplot_cif_tick_series', None)
|
|
197
|
+
if c is not None:
|
|
198
|
+
return c
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
if _bp is not None:
|
|
202
|
+
return getattr(_bp, 'cif_tick_series', None)
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
def _print_cif_phase_list(cts):
|
|
206
|
+
for i, (lab, fname, *_rest) in enumerate(cts):
|
|
207
|
+
print(f" {i+1}: {lab} ({os.path.basename(fname)})")
|
|
208
|
+
|
|
209
|
+
def _apply_cif_phase_label_rename(idx: int, new_label: str) -> None:
|
|
210
|
+
"""Update one CIF phase row label and redraw (shared by main r→t and cif→r)."""
|
|
211
|
+
cts = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
|
|
212
|
+
if not cts or not (0 <= idx < len(cts)):
|
|
213
|
+
return
|
|
214
|
+
try:
|
|
215
|
+
push_state("cif-rename")
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
_, fname, peaksQ, wl_e, qmax, col = cts[idx]
|
|
219
|
+
if _bp is not None:
|
|
220
|
+
setattr(_bp, 'cif_extend_suspended', True)
|
|
221
|
+
if hasattr(ax, '_cif_tick_art'):
|
|
222
|
+
try:
|
|
223
|
+
for art in list(getattr(ax, '_cif_tick_art', [])):
|
|
224
|
+
try:
|
|
225
|
+
art.remove()
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
ax._cif_tick_art = []
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
cts[idx] = (new_label, fname, peaksQ, wl_e, qmax, col)
|
|
232
|
+
if _bp is not None:
|
|
233
|
+
setattr(_bp, 'cif_tick_series', cts)
|
|
234
|
+
_sync_fig_cif_tick_series()
|
|
235
|
+
if hasattr(ax, '_cif_draw_func'):
|
|
236
|
+
ax._cif_draw_func()
|
|
237
|
+
try:
|
|
238
|
+
fig.canvas.draw()
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
if _bp is not None:
|
|
242
|
+
setattr(_bp, 'cif_extend_suspended', False)
|
|
243
|
+
|
|
164
244
|
try:
|
|
165
245
|
raw_source_paths = list(getattr(args, 'files', []) or [])
|
|
166
246
|
except Exception:
|
|
@@ -985,7 +1065,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
985
1065
|
|
|
986
1066
|
# NEW: export current style to .bpcfg
|
|
987
1067
|
def export_style_config(filename, base_path=None, overwrite_path=None, force_kind=None):
|
|
988
|
-
cts =
|
|
1068
|
+
cts = _cif_series_for_session()
|
|
989
1069
|
show_titles = bool(getattr(_bp, 'show_cif_titles', True)) if _bp is not None else True
|
|
990
1070
|
return _export_style_config(
|
|
991
1071
|
filename,
|
|
@@ -1007,7 +1087,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1007
1087
|
|
|
1008
1088
|
# NEW: apply imported style config (restricted application)
|
|
1009
1089
|
def apply_style_config(filename):
|
|
1010
|
-
cts =
|
|
1090
|
+
cts = _cif_series_for_session()
|
|
1011
1091
|
hkl_map = getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None
|
|
1012
1092
|
res = _bp_apply_style_config(
|
|
1013
1093
|
filename,
|
|
@@ -1026,6 +1106,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1026
1106
|
hkl_map,
|
|
1027
1107
|
adjust_margins,
|
|
1028
1108
|
)
|
|
1109
|
+
_sync_fig_cif_tick_series()
|
|
1029
1110
|
# Sync top/right tick label2 fonts with current rcParams after style import
|
|
1030
1111
|
try:
|
|
1031
1112
|
fam_chain = plt.rcParams.get('font.sans-serif')
|
|
@@ -1586,6 +1667,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1586
1667
|
except Exception:
|
|
1587
1668
|
return None
|
|
1588
1669
|
return None
|
|
1670
|
+
_cts_for_snap = _cif_series_for_session()
|
|
1589
1671
|
snap = {
|
|
1590
1672
|
"note": note,
|
|
1591
1673
|
"xlim": ax.get_xlim(),
|
|
@@ -1622,7 +1704,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1622
1704
|
"tick_direction": getattr(fig, '_tick_direction', 'out'),
|
|
1623
1705
|
"tick_spacing": _capture_tick_spacing(ax),
|
|
1624
1706
|
"tick_minor_count": _capture_tick_minor_count(ax),
|
|
1625
|
-
"cif_tick_series": (list(
|
|
1707
|
+
"cif_tick_series": (list(_cts_for_snap) if _cts_for_snap is not None else None),
|
|
1626
1708
|
"show_cif_hkl": (bool(getattr(_bp, 'show_cif_hkl')) if _bp is not None and hasattr(_bp, 'show_cif_hkl') else False),
|
|
1627
1709
|
"show_cif_titles": (bool(getattr(_bp, 'show_cif_titles')) if _bp is not None and hasattr(_bp, 'show_cif_titles') else True),
|
|
1628
1710
|
"rotation_angle": getattr(ax, '_rotation_angle', 0),
|
|
@@ -2009,6 +2091,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2009
2091
|
_bp.cif_tick_series[:] = [tuple(t) for t in snap["cif_tick_series"]]
|
|
2010
2092
|
except Exception:
|
|
2011
2093
|
pass
|
|
2094
|
+
_sync_fig_cif_tick_series()
|
|
2012
2095
|
if _bp is not None and 'show_cif_hkl' in snap:
|
|
2013
2096
|
try:
|
|
2014
2097
|
new_state = bool(snap['show_cif_hkl'])
|
|
@@ -2144,7 +2227,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2144
2227
|
print(" " + colorize_menu("p: shift all CIF ticks (w/s or type a value)"))
|
|
2145
2228
|
print(" " + colorize_menu("c: CIF color (per set)"))
|
|
2146
2229
|
print(" " + colorize_menu("x: show/hide CIF set"))
|
|
2147
|
-
print(" " + colorize_menu("r: rename CIF
|
|
2230
|
+
print(" " + colorize_menu("r: rename CIF phase label (same as main menu r→t)"))
|
|
2148
2231
|
print(" " + colorize_menu("q: back to main menu"))
|
|
2149
2232
|
sub = _safe_input(colorize_prompt("CIF (z/t/v/p/c/x/r/q): ")).strip().lower()
|
|
2150
2233
|
if not sub or sub == 'q':
|
|
@@ -2249,6 +2332,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2249
2332
|
new_cts = [cts[i - 1] for i in parts]
|
|
2250
2333
|
if _bp is not None:
|
|
2251
2334
|
setattr(_bp, 'cif_tick_series', new_cts)
|
|
2335
|
+
_sync_fig_cif_tick_series()
|
|
2252
2336
|
prev_offs = getattr(fig, '_bp_cif_stack_y_offsets', None)
|
|
2253
2337
|
if prev_offs is not None and len(prev_offs) == len(cts):
|
|
2254
2338
|
fig._bp_cif_stack_y_offsets = [prev_offs[i - 1] for i in parts]
|
|
@@ -2390,6 +2474,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2390
2474
|
cts[idx] = (lab, fname, peaksQ, wl_e, qmax, resolved)
|
|
2391
2475
|
if _bp is not None:
|
|
2392
2476
|
setattr(_bp, 'cif_tick_series', cts)
|
|
2477
|
+
_sync_fig_cif_tick_series()
|
|
2393
2478
|
if hasattr(ax, '_cif_draw_func'):
|
|
2394
2479
|
ax._cif_draw_func()
|
|
2395
2480
|
else:
|
|
@@ -2495,6 +2580,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2495
2580
|
cts[idx] = (lab, fname, peaksQ, wl_e, qmax, col_val)
|
|
2496
2581
|
if _bp is not None:
|
|
2497
2582
|
setattr(_bp, 'cif_tick_series', cts)
|
|
2583
|
+
_sync_fig_cif_tick_series()
|
|
2498
2584
|
if hasattr(ax, '_cif_draw_func'):
|
|
2499
2585
|
ax._cif_draw_func()
|
|
2500
2586
|
except Exception as e:
|
|
@@ -2542,39 +2628,40 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2542
2628
|
except Exception as e:
|
|
2543
2629
|
print(f"Error toggling CIF visibility: {e}")
|
|
2544
2630
|
elif sub == 'r':
|
|
2545
|
-
# Rename CIF
|
|
2631
|
+
# Rename CIF phase labels — same behavior as main menu r→t.
|
|
2546
2632
|
try:
|
|
2547
2633
|
cts = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
|
|
2548
2634
|
if not cts:
|
|
2549
|
-
print("No CIF
|
|
2635
|
+
print("No CIF phases to rename.")
|
|
2550
2636
|
else:
|
|
2551
2637
|
while True:
|
|
2552
|
-
print("CIF
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2638
|
+
print("CIF phases (q=back to CIF menu)")
|
|
2639
|
+
_print_cif_phase_list(cts)
|
|
2640
|
+
idx_s = _safe_input(
|
|
2641
|
+
"Phase number to rename (q=back): "
|
|
2642
|
+
).strip().lower()
|
|
2556
2643
|
if not idx_s or idx_s == 'q':
|
|
2557
2644
|
break
|
|
2558
2645
|
try:
|
|
2559
2646
|
idx = int(idx_s) - 1
|
|
2560
|
-
if 0 <= idx < len(cts):
|
|
2561
|
-
lab, fname, peaksQ, wl_e, qmax, col = cts[idx]
|
|
2562
|
-
new_lab = _safe_input(f"New label for set {idx+1} (current: {lab}, blank=cancel): ").strip()
|
|
2563
|
-
if not new_lab:
|
|
2564
|
-
continue
|
|
2565
|
-
push_state("cif-rename")
|
|
2566
|
-
cts[idx] = (new_lab, fname, peaksQ, wl_e, qmax, col)
|
|
2567
|
-
if _bp is not None:
|
|
2568
|
-
setattr(_bp, 'cif_tick_series', cts)
|
|
2569
|
-
if hasattr(ax, '_cif_draw_func'):
|
|
2570
|
-
ax._cif_draw_func()
|
|
2571
|
-
print(f"Set {idx+1} renamed to: {new_lab}")
|
|
2572
|
-
else:
|
|
2647
|
+
if not (0 <= idx < len(cts)):
|
|
2573
2648
|
print("Invalid index.")
|
|
2649
|
+
continue
|
|
2574
2650
|
except ValueError:
|
|
2575
2651
|
print("Invalid index.")
|
|
2652
|
+
continue
|
|
2653
|
+
print_label_latex_tips()
|
|
2654
|
+
new_lab = _safe_input(
|
|
2655
|
+
"New CIF phase label (q=cancel): "
|
|
2656
|
+
).strip()
|
|
2657
|
+
if not new_lab or new_lab.lower() == 'q':
|
|
2658
|
+
print("Canceled.")
|
|
2659
|
+
continue
|
|
2660
|
+
new_lab = convert_label_shortcuts(new_lab)
|
|
2661
|
+
_apply_cif_phase_label_rename(idx, new_lab)
|
|
2662
|
+
print(f"Phase {idx + 1} label updated.")
|
|
2576
2663
|
except Exception as e:
|
|
2577
|
-
print(f"Error renaming CIF
|
|
2664
|
+
print(f"Error renaming CIF phase labels: {e}")
|
|
2578
2665
|
else:
|
|
2579
2666
|
print("Unknown option.")
|
|
2580
2667
|
continue
|
|
@@ -2708,7 +2795,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2708
2795
|
delta=delta,
|
|
2709
2796
|
args=args,
|
|
2710
2797
|
tick_state=tick_state,
|
|
2711
|
-
cif_tick_series=(
|
|
2798
|
+
cif_tick_series=_cif_series_for_session(),
|
|
2712
2799
|
cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
|
|
2713
2800
|
cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
|
|
2714
2801
|
show_cif_hkl=(bool(getattr(_bp, 'show_cif_hkl', False)) if _bp is not None else False),
|
|
@@ -2870,7 +2957,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2870
2957
|
delta=delta,
|
|
2871
2958
|
args=args,
|
|
2872
2959
|
tick_state=tick_state,
|
|
2873
|
-
cif_tick_series=(
|
|
2960
|
+
cif_tick_series=_cif_series_for_session(),
|
|
2874
2961
|
cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
|
|
2875
2962
|
cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
|
|
2876
2963
|
show_cif_hkl=(bool(getattr(_bp,'show_cif_hkl', False)) if _bp is not None else False),
|
|
@@ -2903,7 +2990,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2903
2990
|
delta=delta,
|
|
2904
2991
|
args=args,
|
|
2905
2992
|
tick_state=tick_state,
|
|
2906
|
-
cif_tick_series=(
|
|
2993
|
+
cif_tick_series=_cif_series_for_session(),
|
|
2907
2994
|
cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
|
|
2908
2995
|
cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
|
|
2909
2996
|
show_cif_hkl=(bool(getattr(_bp,'show_cif_hkl', False)) if _bp is not None else False),
|
|
@@ -2943,7 +3030,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2943
3030
|
delta=delta,
|
|
2944
3031
|
args=args,
|
|
2945
3032
|
tick_state=tick_state,
|
|
2946
|
-
cif_tick_series=(
|
|
3033
|
+
cif_tick_series=_cif_series_for_session(),
|
|
2947
3034
|
cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
|
|
2948
3035
|
cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
|
|
2949
3036
|
show_cif_hkl=(bool(getattr(_bp,'show_cif_hkl', False)) if _bp is not None else False),
|
|
@@ -3157,6 +3244,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3157
3244
|
cts[idx] = (lab, fname, peaksQ, wl_e, qmax, resolved)
|
|
3158
3245
|
if _bp is not None:
|
|
3159
3246
|
setattr(_bp, 'cif_tick_series', cts)
|
|
3247
|
+
_sync_fig_cif_tick_series()
|
|
3160
3248
|
if hasattr(ax, '_cif_draw_func'):
|
|
3161
3249
|
ax._cif_draw_func()
|
|
3162
3250
|
else:
|
|
@@ -3225,6 +3313,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3225
3313
|
cts[idx] = (lab, fname, peaksQ, wl_e, qmax, col_val)
|
|
3226
3314
|
if _bp is not None:
|
|
3227
3315
|
setattr(_bp, 'cif_tick_series', cts)
|
|
3316
|
+
_sync_fig_cif_tick_series()
|
|
3228
3317
|
if hasattr(ax, '_cif_draw_func'):
|
|
3229
3318
|
ax._cif_draw_func()
|
|
3230
3319
|
else:
|
|
@@ -3317,7 +3406,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3317
3406
|
while True:
|
|
3318
3407
|
rename_opts = "c=curve"
|
|
3319
3408
|
if has_cif:
|
|
3320
|
-
rename_opts += ", t=
|
|
3409
|
+
rename_opts += ", t=CIF phase label (same as cif→r)"
|
|
3321
3410
|
rename_opts += ", x=x-axis, y=y-axis, q=return"
|
|
3322
3411
|
mode = _safe_input(f"Rename ({rename_opts}): ").strip().lower()
|
|
3323
3412
|
if mode == 'q':
|
|
@@ -3350,44 +3439,34 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3350
3439
|
elif mode == 't':
|
|
3351
3440
|
cts = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
|
|
3352
3441
|
if not cts:
|
|
3353
|
-
print("No CIF
|
|
3442
|
+
print("No CIF phases to rename.")
|
|
3443
|
+
continue
|
|
3444
|
+
print("CIF phases (then pick one; same list as cif→r)")
|
|
3445
|
+
_print_cif_phase_list(cts)
|
|
3446
|
+
s = _safe_input(
|
|
3447
|
+
"Phase number to rename (q=cancel): "
|
|
3448
|
+
).strip()
|
|
3449
|
+
if not s or s.lower() == 'q':
|
|
3450
|
+
print("Canceled.")
|
|
3354
3451
|
continue
|
|
3355
|
-
for i,(lab, fname, *_rest) in enumerate(cts):
|
|
3356
|
-
print(f" {i+1}: {lab} ({os.path.basename(fname)})")
|
|
3357
|
-
s = _safe_input("CIF tick number to rename (q=cancel): ").strip()
|
|
3358
|
-
if not s or s.lower()=='q':
|
|
3359
|
-
print("Canceled."); continue
|
|
3360
3452
|
try:
|
|
3361
|
-
idx = int(s)-1
|
|
3453
|
+
idx = int(s) - 1
|
|
3362
3454
|
if not (0 <= idx < len(cts)):
|
|
3363
|
-
print("Index out of range.")
|
|
3455
|
+
print("Index out of range.")
|
|
3456
|
+
continue
|
|
3364
3457
|
except ValueError:
|
|
3365
|
-
print("Bad index.")
|
|
3458
|
+
print("Bad index.")
|
|
3459
|
+
continue
|
|
3366
3460
|
print_label_latex_tips()
|
|
3367
|
-
new_name = _safe_input(
|
|
3368
|
-
|
|
3369
|
-
|
|
3461
|
+
new_name = _safe_input(
|
|
3462
|
+
"New CIF phase label (q=cancel): "
|
|
3463
|
+
).strip()
|
|
3464
|
+
if not new_name or new_name.lower() == 'q':
|
|
3465
|
+
print("Canceled.")
|
|
3466
|
+
continue
|
|
3370
3467
|
new_name = convert_label_shortcuts(new_name)
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
if _bp is not None:
|
|
3374
|
-
setattr(_bp, 'cif_extend_suspended', True)
|
|
3375
|
-
if hasattr(ax, '_cif_tick_art'):
|
|
3376
|
-
try:
|
|
3377
|
-
for art in list(getattr(ax, '_cif_tick_art', [])):
|
|
3378
|
-
try:
|
|
3379
|
-
art.remove()
|
|
3380
|
-
except Exception:
|
|
3381
|
-
pass
|
|
3382
|
-
ax._cif_tick_art = []
|
|
3383
|
-
except Exception:
|
|
3384
|
-
pass
|
|
3385
|
-
cts[idx] = (new_name, fname, peaksQ, wl, qmax_sim, color)
|
|
3386
|
-
setattr(_bp, 'cif_tick_series', cts)
|
|
3387
|
-
if hasattr(ax,'_cif_draw_func'): ax._cif_draw_func()
|
|
3388
|
-
fig.canvas.draw()
|
|
3389
|
-
if _bp is not None:
|
|
3390
|
-
setattr(_bp, 'cif_extend_suspended', False)
|
|
3468
|
+
_apply_cif_phase_label_rename(idx, new_name)
|
|
3469
|
+
print(f"Phase {idx + 1} label updated.")
|
|
3391
3470
|
elif mode in ('x','y'):
|
|
3392
3471
|
print("Enter new axis label (q=cancel).")
|
|
3393
3472
|
print_label_latex_tips()
|
|
@@ -607,66 +607,73 @@ def plot_operando_folder(folder: str, args, cif_files=None) -> Tuple[plt.Figure,
|
|
|
607
607
|
|
|
608
608
|
for ec_path in ec_files:
|
|
609
609
|
is_datalogger = ec_path.suffix.lower() == ".csv" and is_biologic_datalogger_csv(str(ec_path))
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
610
|
+
try:
|
|
611
|
+
if is_datalogger:
|
|
612
|
+
time_h, voltage_v = read_biologic_datalogger_time_voltage(str(ec_path))
|
|
613
|
+
time_h = np.asarray(time_h, float)
|
|
614
|
+
voltage_v = np.asarray(voltage_v, float)
|
|
615
|
+
if len(time_h) == 0 or len(voltage_v) == 0:
|
|
616
|
+
raise ValueError(f"DataLogger file {ec_path.name} has no valid data rows")
|
|
617
|
+
if time_offset != 0:
|
|
618
|
+
time_h = time_h + time_offset
|
|
618
619
|
time_offset = float(np.nanmax(time_h))
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
else:
|
|
622
|
-
# .mpt file
|
|
623
|
-
readcol_mpt = None
|
|
624
|
-
if hasattr(args, 'readcols') and args.readcols is not None:
|
|
625
|
-
readcol_mpt = tuple(args.readcols)
|
|
626
|
-
if readcol_mpt is None and hasattr(args, 'readcol_by_ext') and '.mpt' in getattr(args, 'readcol_by_ext', {}):
|
|
627
|
-
readcol_mpt = args.readcol_by_ext['.mpt']
|
|
628
|
-
|
|
629
|
-
if readcol_mpt:
|
|
630
|
-
data = robust_loadtxt_skipheader(str(ec_path))
|
|
631
|
-
if data.ndim == 1:
|
|
632
|
-
data = data.reshape(1, -1)
|
|
633
|
-
if data.shape[1] < 2:
|
|
634
|
-
raise ValueError(f"MPT file {ec_path.name} has insufficient columns")
|
|
635
|
-
x_col, y_col = readcol_mpt
|
|
636
|
-
x_col_idx, y_col_idx = x_col - 1, y_col - 1
|
|
637
|
-
# EC panel: voltage on X, time on Y; assume x_col=time, y_col=voltage (EC-Lab order)
|
|
638
|
-
v_raw = np.asarray(data[:, y_col_idx], float)
|
|
639
|
-
t_raw = np.asarray(data[:, x_col_idx], float)
|
|
620
|
+
x_parts.append(voltage_v)
|
|
621
|
+
y_parts.append(time_h)
|
|
640
622
|
else:
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
623
|
+
# .mpt file
|
|
624
|
+
readcol_mpt = None
|
|
625
|
+
if hasattr(args, 'readcols') and args.readcols is not None:
|
|
626
|
+
readcol_mpt = tuple(args.readcols)
|
|
627
|
+
if readcol_mpt is None and hasattr(args, 'readcol_by_ext') and '.mpt' in getattr(args, 'readcol_by_ext', {}):
|
|
628
|
+
readcol_mpt = args.readcol_by_ext['.mpt']
|
|
629
|
+
|
|
630
|
+
if readcol_mpt:
|
|
631
|
+
data = robust_loadtxt_skipheader(str(ec_path))
|
|
632
|
+
if data.ndim == 1:
|
|
633
|
+
data = data.reshape(1, -1)
|
|
634
|
+
if data.shape[1] < 2:
|
|
635
|
+
raise ValueError(f"MPT file {ec_path.name} has insufficient columns")
|
|
636
|
+
x_col, y_col = readcol_mpt
|
|
637
|
+
x_col_idx, y_col_idx = x_col - 1, y_col - 1
|
|
638
|
+
# EC panel: voltage on X, time on Y; assume x_col=time, y_col=voltage (EC-Lab order)
|
|
639
|
+
v_raw = np.asarray(data[:, y_col_idx], float)
|
|
640
|
+
t_raw = np.asarray(data[:, x_col_idx], float)
|
|
641
|
+
else:
|
|
642
|
+
result = read_mpt_file(str(ec_path), mode='time')
|
|
643
|
+
if len(result) == 5:
|
|
644
|
+
x_data, y_data, current_mA, x_lbl, y_lbl = result
|
|
645
|
+
x_lower = x_lbl.lower().replace(' ', '').replace('_', '')
|
|
646
|
+
y_lower = y_lbl.lower().replace(' ', '').replace('_', '')
|
|
647
|
+
has_time_in_x = 'time' in x_lower
|
|
648
|
+
has_voltage_in_y = 'voltage' in y_lower or 'potential' in y_lower or 'ewe' in y_lower
|
|
649
|
+
if x_lbl == 'Time (h)' and y_lbl == 'Potential (V)':
|
|
650
|
+
t_raw = np.asarray(x_data, float)
|
|
651
|
+
v_raw = np.asarray(y_data, float)
|
|
652
|
+
elif has_time_in_x and has_voltage_in_y:
|
|
653
|
+
t_raw = np.asarray(x_data, float)
|
|
654
|
+
v_raw = np.asarray(y_data, float)
|
|
655
|
+
elif 'voltage' in x_lower or 'potential' in x_lower:
|
|
656
|
+
v_raw = np.asarray(x_data, float)
|
|
657
|
+
t_raw = np.asarray(y_data, float)
|
|
658
|
+
else:
|
|
659
|
+
v_raw = np.asarray(x_data, float)
|
|
660
|
+
t_raw = np.asarray(y_data, float)
|
|
657
661
|
else:
|
|
662
|
+
x_data, y_data, current_mA, *_ = result
|
|
658
663
|
v_raw = np.asarray(x_data, float)
|
|
659
|
-
t_raw = np.asarray(y_data, float)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
t_raw =
|
|
664
|
-
if time_offset != 0:
|
|
665
|
-
t_raw = t_raw + time_offset
|
|
666
|
-
if len(t_raw) > 0:
|
|
664
|
+
t_raw = np.asarray(y_data, float) / 3600.0
|
|
665
|
+
if len(t_raw) == 0 or len(v_raw) == 0:
|
|
666
|
+
raise ValueError(f"MPT file {ec_path.name} has no valid data rows")
|
|
667
|
+
if time_offset != 0:
|
|
668
|
+
t_raw = t_raw + time_offset
|
|
667
669
|
time_offset = float(np.nanmax(t_raw))
|
|
668
|
-
|
|
669
|
-
|
|
670
|
+
x_parts.append(v_raw)
|
|
671
|
+
y_parts.append(t_raw)
|
|
672
|
+
except Exception as ec_file_err:
|
|
673
|
+
print(f"[operando] Skip EC file {ec_path.name}: {ec_file_err}")
|
|
674
|
+
|
|
675
|
+
if not x_parts or not y_parts:
|
|
676
|
+
raise ValueError("No valid electrochem data points in detected EC files")
|
|
670
677
|
|
|
671
678
|
x_data = np.concatenate(x_parts) if len(x_parts) > 1 else x_parts[0]
|
|
672
679
|
y_data = np.concatenate(y_parts) if len(y_parts) > 1 else y_parts[0]
|
|
@@ -534,6 +534,17 @@ def print_style_info(
|
|
|
534
534
|
if cif_tick_series:
|
|
535
535
|
print(f"\n--- CIF (cif key) ---")
|
|
536
536
|
try:
|
|
537
|
+
print("CIF phase labels (r→t / cif→r) & per-set colors (c); stored in p / i / s / b:")
|
|
538
|
+
for i, ent in enumerate(cif_tick_series):
|
|
539
|
+
if len(ent) < 6:
|
|
540
|
+
continue
|
|
541
|
+
lab, fname, _pq, _wl, _qm, col = ent[0], ent[1], ent[2], ent[3], ent[4], ent[5]
|
|
542
|
+
try:
|
|
543
|
+
ch = mcolors.to_hex(mcolors.to_rgba(col))
|
|
544
|
+
except Exception:
|
|
545
|
+
ch = str(col)
|
|
546
|
+
hb = color_block(ch) if ch else ""
|
|
547
|
+
print(f" {i + 1}: {lab} ({os.path.basename(fname)}) {hb} {ch}")
|
|
537
548
|
hkl_state = None
|
|
538
549
|
_bp_module = sys.modules.get('__main__')
|
|
539
550
|
if _bp_module is not None and hasattr(_bp_module, 'show_cif_hkl'):
|
|
@@ -786,8 +797,9 @@ def export_style_config(
|
|
|
786
797
|
except Exception:
|
|
787
798
|
pass
|
|
788
799
|
if cif_tick_series:
|
|
800
|
+
# label + color so export (p) / import (i) match session (s) / undo (b) for renamed phases
|
|
789
801
|
cfg["cif_ticks"] = [
|
|
790
|
-
{"index": i, "color": color}
|
|
802
|
+
{"index": i, "label": str(lab), "color": color}
|
|
791
803
|
for i, (lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(cif_tick_series)
|
|
792
804
|
]
|
|
793
805
|
# Always save one offset per CIF set so import (i) matches session/undo (s/b).
|
|
@@ -101,10 +101,10 @@ def _wrap_line(text: str, width: int) -> List[str]:
|
|
|
101
101
|
UPDATE_INFO = {
|
|
102
102
|
# Custom message to include in update notification
|
|
103
103
|
# (Auto-filled from RELEASE_NOTES.txt when using batplot --dev-upgrade)
|
|
104
|
-
'custom_message': '- Bug fixes',
|
|
104
|
+
'custom_message': '- Bug fixes for BatX operando plot',
|
|
105
105
|
# Additional notes (auto-filled from RELEASE_NOTES.txt)
|
|
106
106
|
'update_notes': [
|
|
107
|
-
'- Bug fixes'
|
|
107
|
+
'- Bug fixes for BatX operando plot'
|
|
108
108
|
],
|
|
109
109
|
'show_update_notes': True,
|
|
110
110
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.39
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "batplot"
|
|
7
|
-
version = "1.8.
|
|
7
|
+
version = "1.8.39"
|
|
8
8
|
description = "Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Tian Dai", email = "tianda@uio.no" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|