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.
Files changed (42) hide show
  1. {batplot-1.8.37/batplot.egg-info → batplot-1.8.39}/PKG-INFO +1 -1
  2. {batplot-1.8.37 → batplot-1.8.39}/batplot/__init__.py +1 -1
  3. {batplot-1.8.37 → batplot-1.8.39}/batplot/args.py +2 -0
  4. {batplot-1.8.37 → batplot-1.8.39}/batplot/batplot.py +33 -11
  5. {batplot-1.8.37 → batplot-1.8.39}/batplot/cpc_interactive.py +44 -35
  6. {batplot-1.8.37 → batplot-1.8.39}/batplot/data/CHANGELOG.md +8 -0
  7. {batplot-1.8.37 → batplot-1.8.39}/batplot/interactive.py +144 -65
  8. {batplot-1.8.37 → batplot-1.8.39}/batplot/operando.py +62 -55
  9. {batplot-1.8.37 → batplot-1.8.39}/batplot/style.py +13 -1
  10. {batplot-1.8.37 → batplot-1.8.39}/batplot/version_check.py +2 -2
  11. {batplot-1.8.37 → batplot-1.8.39/batplot.egg-info}/PKG-INFO +1 -1
  12. {batplot-1.8.37 → batplot-1.8.39}/pyproject.toml +1 -1
  13. {batplot-1.8.37 → batplot-1.8.39}/LICENSE +0 -0
  14. {batplot-1.8.37 → batplot-1.8.39}/MANIFEST.in +0 -0
  15. {batplot-1.8.37 → batplot-1.8.39}/NOTICE +0 -0
  16. {batplot-1.8.37 → batplot-1.8.39}/README.md +0 -0
  17. {batplot-1.8.37 → batplot-1.8.39}/batplot/batch.py +0 -0
  18. {batplot-1.8.37 → batplot-1.8.39}/batplot/canvas_interactive.py +0 -0
  19. {batplot-1.8.37 → batplot-1.8.39}/batplot/cif.py +0 -0
  20. {batplot-1.8.37 → batplot-1.8.39}/batplot/cli.py +0 -0
  21. {batplot-1.8.37 → batplot-1.8.39}/batplot/color_utils.py +0 -0
  22. {batplot-1.8.37 → batplot-1.8.39}/batplot/config.py +0 -0
  23. {batplot-1.8.37 → batplot-1.8.39}/batplot/converters.py +0 -0
  24. {batplot-1.8.37 → batplot-1.8.39}/batplot/data/USER_MANUAL.md +0 -0
  25. {batplot-1.8.37 → batplot-1.8.39}/batplot/dev_upgrade.py +0 -0
  26. {batplot-1.8.37 → batplot-1.8.39}/batplot/electrochem_interactive.py +0 -0
  27. {batplot-1.8.37 → batplot-1.8.39}/batplot/manual.py +0 -0
  28. {batplot-1.8.37 → batplot-1.8.39}/batplot/modes.py +0 -0
  29. {batplot-1.8.37 → batplot-1.8.39}/batplot/operando_ec_interactive.py +0 -0
  30. {batplot-1.8.37 → batplot-1.8.39}/batplot/plotting.py +0 -0
  31. {batplot-1.8.37 → batplot-1.8.39}/batplot/readers.py +0 -0
  32. {batplot-1.8.37 → batplot-1.8.39}/batplot/session.py +0 -0
  33. {batplot-1.8.37 → batplot-1.8.39}/batplot/showcol.py +0 -0
  34. {batplot-1.8.37 → batplot-1.8.39}/batplot/ui.py +0 -0
  35. {batplot-1.8.37 → batplot-1.8.39}/batplot/utils.py +0 -0
  36. {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/SOURCES.txt +0 -0
  37. {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/dependency_links.txt +0 -0
  38. {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/entry_points.txt +0 -0
  39. {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/requires.txt +0 -0
  40. {batplot-1.8.37 → batplot-1.8.39}/batplot.egg-info/top_level.txt +0 -0
  41. {batplot-1.8.37 → batplot-1.8.39}/setup.cfg +0 -0
  42. {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.37
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
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.8.37"
3
+ __version__ = "1.8.39"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -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
- expanded.append(p)
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
- for _lab,_fname,_peaks,_wl,_qmax,_color in cif_tick_series:
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
- if not cif_tick_series:
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(cif_tick_series) - 1) * spacing - _cif_bottom_m
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(cif_tick_series):
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': list(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
- if not cif_tick_series:
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(cif_tick_series):
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(cif_tick_series)
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(cif_tick_series):
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(cif_tick_series):
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
- leg = ax.legend(*args, **kwargs)
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
- _marker_kw = dict(linewidth=0)
658
- # Header: filled square = Charge
659
- chg_patch = mpatches.Patch(facecolor='#444444', edgecolor='#444444', label='Charge')
660
- # Header: hollow square = Discharge
661
- dch_patch = mpatches.Patch(facecolor='none', edgecolor='#444444',
662
- linewidth=1.2, label='Discharge')
663
- handles = [chg_patch, dch_patch]
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
- from matplotlib.lines import Line2D # type: ignore
684
- eff_handle = Line2D([0], [0], marker='^', color='none',
685
- markerfacecolor=eff_color, markeredgecolor=eff_color,
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 patch with empty label to create visual gap
691
- sep = mpatches.Patch(visible=False, label='')
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
- patch = mpatches.Patch(facecolor=color, edgecolor=color, label=fname)
698
- handles.append(patch)
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: smaller symbols; handler_map forces squares (never cuboids)
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
- handler_map={mpatches.Patch: _HandlerSquarePatch()},
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', **_legend_kw)
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', **_legend_kw)
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:
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.39] - 2026-05-01
4
+ - Bug fixes for BatX operando plot
5
+
6
+
7
+ ## [1.8.38] - 2026-04-03
8
+ - Bug fixes
9
+
10
+
3
11
  ## [1.8.37] - 2026-03-31
4
12
  - Bug fixes
5
13
 
@@ -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
- This is a harmless macOS system message that appears when using input() in terminals.
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 (KeyboardInterrupt, EOFError):
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 = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
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 = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
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(getattr(_bp, 'cif_tick_series')) if (_bp is not None and hasattr(_bp, 'cif_tick_series')) else None),
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 set label"))
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 set labels — updates label field in cif_tick_series and redraws.
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 tick sets to rename.")
2635
+ print("No CIF phases to rename.")
2550
2636
  else:
2551
2637
  while True:
2552
- print("CIF sets (q=back)")
2553
- for i, (lab, fname, *_rest) in enumerate(cts):
2554
- print(f" {i+1}: {lab}")
2555
- idx_s = _safe_input("Set index to rename (q=back): ").strip().lower()
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 sets: {e}")
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=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
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=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
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=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
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=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
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=cif tick label"
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 tick sets to rename.")
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."); continue
3455
+ print("Index out of range.")
3456
+ continue
3364
3457
  except ValueError:
3365
- print("Bad index."); continue
3458
+ print("Bad index.")
3459
+ continue
3366
3460
  print_label_latex_tips()
3367
- new_name = _safe_input("New CIF tick label (q=cancel): ")
3368
- if not new_name or new_name.lower()=='q':
3369
- print("Canceled."); continue
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
- lab,fname,peaksQ,wl,qmax_sim,color = cts[idx]
3372
- # Suspend extension while updating label
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
- 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 time_offset != 0:
616
- time_h = time_h + time_offset
617
- if len(time_h) > 0:
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
- x_parts.append(voltage_v)
620
- y_parts.append(time_h)
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
- result = read_mpt_file(str(ec_path), mode='time')
642
- if len(result) == 5:
643
- x_data, y_data, current_mA, x_lbl, y_lbl = result
644
- x_lower = x_lbl.lower().replace(' ', '').replace('_', '')
645
- y_lower = y_lbl.lower().replace(' ', '').replace('_', '')
646
- has_time_in_x = 'time' in x_lower
647
- has_voltage_in_y = 'voltage' in y_lower or 'potential' in y_lower or 'ewe' in y_lower
648
- if x_lbl == 'Time (h)' and y_lbl == 'Potential (V)':
649
- t_raw = np.asarray(x_data, float)
650
- v_raw = np.asarray(y_data, float)
651
- elif has_time_in_x and has_voltage_in_y:
652
- t_raw = np.asarray(x_data, float)
653
- v_raw = np.asarray(y_data, float)
654
- elif 'voltage' in x_lower or 'potential' in x_lower:
655
- v_raw = np.asarray(x_data, float)
656
- t_raw = np.asarray(y_data, float)
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
- else:
661
- x_data, y_data, current_mA, *_ = result
662
- v_raw = np.asarray(x_data, float)
663
- t_raw = np.asarray(y_data, float) / 3600.0
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
- x_parts.append(v_raw)
669
- y_parts.append(t_raw)
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.37
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.37"
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