batplot 1.7.23__py3-none-any.whl → 1.7.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of batplot might be problematic. Click here for more details.

batplot/interactive.py CHANGED
@@ -866,10 +866,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
866
866
  )
867
867
 
868
868
  # NEW: export current style to .bpcfg
869
- def export_style_config(filename, base_path=None):
869
+ def export_style_config(filename, base_path=None, overwrite_path=None):
870
870
  cts = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
871
871
  show_titles = bool(getattr(_bp, 'show_cif_titles', True)) if _bp is not None else True
872
- return _bp_export_style_config(
872
+ from .style import export_style_config as _export_style_config
873
+ return _export_style_config(
873
874
  filename,
874
875
  fig,
875
876
  ax,
@@ -883,6 +884,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
883
884
  label_text_objects,
884
885
  base_path,
885
886
  show_cif_titles=show_titles,
887
+ overwrite_path=overwrite_path,
886
888
  )
887
889
 
888
890
  # NEW: apply imported style config (restricted application)
@@ -1467,6 +1469,17 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1467
1469
  else:
1468
1470
  continue
1469
1471
  elif key == 'z': # toggle hkl labels on CIF ticks (non-blocking)
1472
+ # Check if CIF files exist before allowing this command
1473
+ has_cif = False
1474
+ try:
1475
+ has_cif = any(f.split(':')[0].lower().endswith('.cif') for f in args.files)
1476
+ if not has_cif and _bp is not None:
1477
+ has_cif = bool(getattr(_bp, 'cif_tick_series', None))
1478
+ except Exception:
1479
+ pass
1480
+ if not has_cif:
1481
+ print("Unknown option.")
1482
+ continue
1470
1483
  try:
1471
1484
  # Flip visibility flag in batplot module
1472
1485
  cur = bool(getattr(_bp, 'show_cif_hkl', False)) if _bp is not None else False
@@ -1546,6 +1559,17 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1546
1559
  print(f"Error in legend submenu: {e}")
1547
1560
  continue
1548
1561
  elif key == 'j': # toggle CIF title labels (filename labels)
1562
+ # Check if CIF files exist before allowing this command
1563
+ has_cif = False
1564
+ try:
1565
+ has_cif = any(f.split(':')[0].lower().endswith('.cif') for f in args.files)
1566
+ if not has_cif and _bp is not None:
1567
+ has_cif = bool(getattr(_bp, 'cif_tick_series', None))
1568
+ except Exception:
1569
+ pass
1570
+ if not has_cif:
1571
+ print("Unknown option.")
1572
+ continue
1549
1573
  try:
1550
1574
  # Preserve both x and y-axis limits to prevent movement
1551
1575
  prev_xlim = ax.get_xlim()
@@ -1609,11 +1633,47 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1609
1633
  print(f" {i}: {f} ({timestamp})")
1610
1634
  else:
1611
1635
  print(f" {i}: {f}")
1612
- prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
1636
+ last_session_path = getattr(fig, '_last_session_save_path', None)
1637
+ if last_session_path:
1638
+ prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
1639
+ else:
1640
+ prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
1613
1641
  choice = input(prompt).strip()
1614
1642
  if not choice or choice.lower() == 'q':
1615
1643
  print("Canceled.")
1616
1644
  continue
1645
+ if choice.lower() == 'o':
1646
+ # Overwrite last saved session
1647
+ if not last_session_path:
1648
+ print("No previous save found.")
1649
+ continue
1650
+ if not os.path.exists(last_session_path):
1651
+ print(f"Previous save file not found: {last_session_path}")
1652
+ continue
1653
+ yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
1654
+ if yn != 'y':
1655
+ continue
1656
+ _bp_dump_session(
1657
+ last_session_path,
1658
+ fig=fig,
1659
+ ax=ax,
1660
+ x_data_list=x_data_list,
1661
+ y_data_list=y_data_list,
1662
+ orig_y=orig_y,
1663
+ offsets_list=offsets_list,
1664
+ labels=labels,
1665
+ delta=delta,
1666
+ args=args,
1667
+ tick_state=tick_state,
1668
+ cif_tick_series=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
1669
+ cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
1670
+ cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
1671
+ show_cif_hkl=(bool(getattr(_bp,'show_cif_hkl', False)) if _bp is not None else False),
1672
+ show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1673
+ skip_confirm=True,
1674
+ )
1675
+ print(f"Overwritten session to {last_session_path}")
1676
+ continue
1617
1677
  target_path = None
1618
1678
  # Overwrite by number
1619
1679
  if choice.isdigit() and files:
@@ -1626,10 +1686,32 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1626
1686
  continue
1627
1687
  target_path = os.path.join(folder, name)
1628
1688
  skip_confirm = True # Already confirmed above
1689
+ _bp_dump_session(
1690
+ target_path,
1691
+ fig=fig,
1692
+ ax=ax,
1693
+ x_data_list=x_data_list,
1694
+ y_data_list=y_data_list,
1695
+ orig_y=orig_y,
1696
+ offsets_list=offsets_list,
1697
+ labels=labels,
1698
+ delta=delta,
1699
+ args=args,
1700
+ tick_state=tick_state,
1701
+ cif_tick_series=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
1702
+ cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
1703
+ cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
1704
+ show_cif_hkl=(bool(getattr(_bp,'show_cif_hkl', False)) if _bp is not None else False),
1705
+ show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1706
+ skip_confirm=skip_confirm,
1707
+ )
1708
+ print(f"Saved session to {target_path}")
1709
+ fig._last_session_save_path = target_path
1710
+ continue
1629
1711
  else:
1630
1712
  print("Invalid number.")
1631
1713
  continue
1632
- else:
1714
+ if choice.lower() != 'o':
1633
1715
  # New name, allow relative or absolute
1634
1716
  name = choice
1635
1717
  root, ext = os.path.splitext(name)
@@ -1643,27 +1725,28 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1643
1725
  print("Canceled.")
1644
1726
  continue
1645
1727
  skip_confirm = True # Already confirmed
1646
- # Delegate to session dumper
1647
- _bp_dump_session(
1648
- target_path,
1649
- fig=fig,
1650
- ax=ax,
1651
- x_data_list=x_data_list,
1652
- y_data_list=y_data_list,
1653
- orig_y=orig_y,
1654
- offsets_list=offsets_list,
1655
- labels=labels,
1656
- delta=delta,
1657
- args=args,
1658
- tick_state=tick_state,
1659
- cif_tick_series=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
1660
- cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
1661
- cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
1662
- show_cif_hkl=(bool(getattr(_bp,'show_cif_hkl', False)) if _bp is not None else False),
1663
- show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1664
- skip_confirm=skip_confirm,
1665
- )
1666
- print(f"Saved session to {target_path}")
1728
+ # Delegate to session dumper
1729
+ _bp_dump_session(
1730
+ target_path,
1731
+ fig=fig,
1732
+ ax=ax,
1733
+ x_data_list=x_data_list,
1734
+ y_data_list=y_data_list,
1735
+ orig_y=orig_y,
1736
+ offsets_list=offsets_list,
1737
+ labels=labels,
1738
+ delta=delta,
1739
+ args=args,
1740
+ tick_state=tick_state,
1741
+ cif_tick_series=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
1742
+ cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
1743
+ cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
1744
+ show_cif_hkl=(bool(getattr(_bp,'show_cif_hkl', False)) if _bp is not None else False),
1745
+ show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1746
+ skip_confirm=skip_confirm,
1747
+ )
1748
+ print(f"Saved session to {target_path}")
1749
+ fig._last_session_save_path = target_path
1667
1750
  except Exception as e:
1668
1751
  print(f"Error saving session: {e}")
1669
1752
  continue
@@ -3493,11 +3576,32 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3493
3576
  print(f" {_i}: {fname} ({timestamp})")
3494
3577
  else:
3495
3578
  print(f" {_i}: {fname}")
3496
- sub = input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
3579
+ last_style_path = getattr(fig, '_last_style_export_path', None)
3580
+ if last_style_path:
3581
+ sub = input(colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
3582
+ else:
3583
+ sub = input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
3497
3584
  if sub == 'q':
3498
3585
  break
3499
3586
  if sub == 'r' or sub == '':
3500
3587
  continue
3588
+ if sub == 'o':
3589
+ # Overwrite last exported style file
3590
+ if not last_style_path:
3591
+ print("No previous export found.")
3592
+ continue
3593
+ if not os.path.exists(last_style_path):
3594
+ print(f"Previous export file not found: {last_style_path}")
3595
+ continue
3596
+ yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3597
+ if yn != 'y':
3598
+ continue
3599
+ # Call export_style_config with overwrite_path to skip dialog
3600
+ exported_path = export_style_config(None, base_path=None, overwrite_path=last_style_path)
3601
+ if exported_path:
3602
+ fig._last_style_export_path = exported_path
3603
+ style_menu_active = False
3604
+ break
3501
3605
  if sub == 'e':
3502
3606
  save_base = choose_save_path(source_file_paths, purpose="style export")
3503
3607
  if not save_base:
@@ -3505,7 +3609,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3505
3609
  continue
3506
3610
  print(f"\nChosen path: {save_base}")
3507
3611
  # Call export_style_config which handles the entire export dialog
3508
- export_style_config(None, base_path=save_base) # filename parameter ignored
3612
+ exported_path = export_style_config(None, base_path=save_base) # filename parameter ignored
3613
+ if exported_path:
3614
+ fig._last_style_export_path = exported_path
3509
3615
  style_menu_active = False # Exit style submenu and return to main menu
3510
3616
  break
3511
3617
  else:
@@ -3543,14 +3649,33 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3543
3649
  else:
3544
3650
  print(f" {i}: {fname}")
3545
3651
 
3546
- filename = input("Enter filename (default SVG if no extension) or number to overwrite (q=cancel): ").strip()
3652
+ last_figure_path = getattr(fig, '_last_figure_export_path', None)
3653
+ if last_figure_path:
3654
+ filename = input("Enter filename (default SVG if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
3655
+ else:
3656
+ filename = input("Enter filename (default SVG if no extension) or number to overwrite (q=cancel): ").strip()
3547
3657
  if not filename or filename.lower() == 'q':
3548
3658
  print("Canceled.")
3549
3659
  continue
3550
3660
 
3661
+ already_confirmed = False # Initialize for new filename case
3662
+ # Check for 'o' option
3663
+ if filename.lower() == 'o':
3664
+ if not last_figure_path:
3665
+ print("No previous export found.")
3666
+ continue
3667
+ if not os.path.exists(last_figure_path):
3668
+ print(f"Previous export file not found: {last_figure_path}")
3669
+ continue
3670
+ yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
3671
+ if yn != 'y':
3672
+ print("Canceled.")
3673
+ continue
3674
+ export_target = last_figure_path
3675
+ already_confirmed = True
3551
3676
  # Check if user selected a number
3552
- already_confirmed = False
3553
- if filename.isdigit() and files:
3677
+ elif filename.isdigit() and files:
3678
+ already_confirmed = False
3554
3679
  idx = int(filename)
3555
3680
  if 1 <= idx <= len(files):
3556
3681
  name = files[idx-1]
@@ -3617,6 +3742,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3617
3742
  else:
3618
3743
  fig.savefig(export_target, dpi=300)
3619
3744
  print(f"Figure saved to {export_target}")
3745
+ fig._last_figure_export_path = export_target
3620
3746
  for i, txt in enumerate(label_text_objects):
3621
3747
  txt.set_text(f"{i+1}: {labels[i]}")
3622
3748
  fig.canvas.draw()
@@ -1716,13 +1716,31 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1716
1716
  else:
1717
1717
  print(f" {i}: {fname}")
1718
1718
 
1719
- fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1719
+ last_figure_path = getattr(fig, '_last_figure_export_path', None)
1720
+ if last_figure_path:
1721
+ fname = input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
1722
+ else:
1723
+ fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1720
1724
  if not fname or fname.lower() == 'q':
1721
1725
  print_menu(); continue
1722
1726
 
1727
+ already_confirmed = False # Initialize for new filename case
1728
+ # Check for 'o' option
1729
+ if fname.lower() == 'o':
1730
+ if not last_figure_path:
1731
+ print("No previous export found.")
1732
+ print_menu(); continue
1733
+ if not os.path.exists(last_figure_path):
1734
+ print(f"Previous export file not found: {last_figure_path}")
1735
+ print_menu(); continue
1736
+ yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
1737
+ if yn != 'y':
1738
+ print_menu(); continue
1739
+ target = last_figure_path
1740
+ already_confirmed = True
1723
1741
  # Check if user selected a number
1724
- already_confirmed = False
1725
- if fname.isdigit() and files:
1742
+ elif fname.isdigit() and files:
1743
+ already_confirmed = False
1726
1744
  idx = int(fname)
1727
1745
  if 1 <= idx <= len(files):
1728
1746
  name = files[idx-1]
@@ -1782,6 +1800,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1782
1800
  else:
1783
1801
  fig.savefig(target, dpi=300)
1784
1802
  print(f"Exported figure to {target}")
1803
+ fig._last_figure_export_path = target
1785
1804
  except Exception as e:
1786
1805
  print(f"Export failed: {e}")
1787
1806
  print_menu(); continue
@@ -1985,10 +2004,28 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1985
2004
  print(f" {i}: {f} ({timestamp})")
1986
2005
  else:
1987
2006
  print(f" {i}: {f}")
1988
- prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
2007
+ last_session_path = getattr(fig, '_last_session_save_path', None)
2008
+ if last_session_path:
2009
+ prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
2010
+ else:
2011
+ prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
1989
2012
  choice = input(prompt).strip()
1990
2013
  if not choice or choice.lower() == 'q':
1991
2014
  print_menu(); continue
2015
+ if choice.lower() == 'o':
2016
+ # Overwrite last saved session
2017
+ if not last_session_path:
2018
+ print("No previous save found.")
2019
+ print_menu(); continue
2020
+ if not os.path.exists(last_session_path):
2021
+ print(f"Previous save file not found: {last_session_path}")
2022
+ print_menu(); continue
2023
+ yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
2024
+ if yn != 'y':
2025
+ print_menu(); continue
2026
+ dump_operando_session(last_session_path, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
2027
+ print(f"Overwritten session to {last_session_path}")
2028
+ print_menu(); continue
1992
2029
  if choice.isdigit() and files:
1993
2030
  idx = int(choice)
1994
2031
  if 1 <= idx <= len(files):
@@ -1997,10 +2034,13 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1997
2034
  if yn != 'y':
1998
2035
  print_menu(); continue
1999
2036
  target = os.path.join(folder, name)
2037
+ dump_operando_session(target, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
2038
+ fig._last_session_save_path = target
2039
+ print_menu(); continue
2000
2040
  else:
2001
2041
  print("Invalid number.")
2002
2042
  print_menu(); continue
2003
- else:
2043
+ if choice.lower() != 'o':
2004
2044
  name = choice
2005
2045
  root, ext = os.path.splitext(name)
2006
2046
  if ext == '':
@@ -2010,7 +2050,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2010
2050
  yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
2011
2051
  if yn != 'y':
2012
2052
  print_menu(); continue
2013
- dump_operando_session(target, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
2053
+ dump_operando_session(target, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
2054
+ fig._last_session_save_path = target
2014
2055
  except Exception as e:
2015
2056
  print(f"Save failed: {e}")
2016
2057
  print_menu(); continue
@@ -3463,6 +3504,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3463
3504
  else:
3464
3505
  print(f" {_i}: {fname}")
3465
3506
 
3507
+ last_style_path = getattr(fig, '_last_style_export_path', None)
3466
3508
  if ec_ax is None:
3467
3509
  print("\nNote: Style export (.bps/.bpsg) is only available in dual-pane mode (with EC file).")
3468
3510
  sub = input("Style submenu: (q=return, r=refresh): ").strip().lower()
@@ -3474,11 +3516,38 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3474
3516
  print("Unknown choice.")
3475
3517
  continue
3476
3518
  else:
3477
- sub = input("Style submenu: (e=export, q=return, r=refresh): ").strip().lower()
3519
+ if last_style_path:
3520
+ sub = input("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ").strip().lower()
3521
+ else:
3522
+ sub = input("Style submenu: (e=export, q=return, r=refresh): ").strip().lower()
3478
3523
  if sub == 'q':
3479
3524
  break
3480
3525
  if sub == 'r' or sub == '':
3481
3526
  continue
3527
+ if sub == 'o':
3528
+ # Overwrite last exported style file
3529
+ if not last_style_path:
3530
+ print("No previous export found.")
3531
+ continue
3532
+ if not os.path.exists(last_style_path):
3533
+ print(f"Previous export file not found: {last_style_path}")
3534
+ continue
3535
+ yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3536
+ if yn != 'y':
3537
+ continue
3538
+ # Determine export type from existing file and rebuild config
3539
+ try:
3540
+ with open(last_style_path, 'r', encoding='utf-8') as f:
3541
+ old_cfg = json.load(f)
3542
+ old_kind = old_cfg.get('kind', '')
3543
+ # Need to rebuild the full config - this requires the same logic as 'e' command
3544
+ # For simplicity, redirect user to use 'e' for now, or we could duplicate the config building code
3545
+ print("To overwrite with current style, please use 'e' to export fresh.")
3546
+ print("The 'o' option will be enhanced in a future update to rebuild config automatically.")
3547
+ continue
3548
+ except Exception as e:
3549
+ print(f"Error reading previous export: {e}")
3550
+ continue
3482
3551
  if sub == 'e':
3483
3552
  # Ask for ps or psg
3484
3553
  print("Export options:")
@@ -3598,6 +3667,23 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3598
3667
  'x': getattr(ec_ax.xaxis, 'labelpad', None),
3599
3668
  'y': getattr(ec_ax.yaxis, 'labelpad', None),
3600
3669
  }
3670
+ # Capture title offsets
3671
+ op_title_offsets = {
3672
+ 'top_y': float(getattr(ax, '_top_xlabel_manual_offset_y_pts', 0.0) or 0.0),
3673
+ 'top_x': float(getattr(ax, '_top_xlabel_manual_offset_x_pts', 0.0) or 0.0),
3674
+ 'bottom_y': float(getattr(ax, '_bottom_xlabel_manual_offset_y_pts', 0.0) or 0.0),
3675
+ 'left_x': float(getattr(ax, '_left_ylabel_manual_offset_x_pts', 0.0) or 0.0),
3676
+ 'right_x': float(getattr(ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0),
3677
+ 'right_y': float(getattr(ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0),
3678
+ }
3679
+ ec_title_offsets = {
3680
+ 'top_y': float(getattr(ec_ax, '_top_xlabel_manual_offset_y_pts', 0.0) or 0.0),
3681
+ 'top_x': float(getattr(ec_ax, '_top_xlabel_manual_offset_x_pts', 0.0) or 0.0),
3682
+ 'bottom_y': float(getattr(ec_ax, '_bottom_xlabel_manual_offset_y_pts', 0.0) or 0.0),
3683
+ 'left_x': float(getattr(ec_ax, '_left_ylabel_manual_offset_x_pts', 0.0) or 0.0),
3684
+ 'right_x': float(getattr(ec_ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0),
3685
+ 'right_y': float(getattr(ec_ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0),
3686
+ }
3601
3687
 
3602
3688
  if exp_choice == 'ps':
3603
3689
  cb_h_offset = getattr(cbar.ax, '_cb_h_offset_in', 0.0)
@@ -3607,8 +3693,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3607
3693
  'version': 2,
3608
3694
  'figure': {'canvas_size': [fig_w, fig_h], 'cb_visible': cb_visible, 'cb_label_mode': cb_label_mode},
3609
3695
  'geometry': {'op_w_in': ax_w_in, 'op_h_in': ax_h_in, 'ec_w_in': ec_w_in, 'cb_h_offset': float(cb_h_offset), 'ec_h_offset': float(ec_h_offset) if ec_h_offset is not None else None},
3610
- 'operando': {'cmap': cmap_name, 'wasd_state': op_wasd_state, 'spines': op_spines, 'ticks': {'widths': op_ticks}, 'y_reversed': op_reversed, 'intensity_range': intensity_range, 'labelpads': op_labelpads},
3611
- 'ec': {'wasd_state': ec_wasd_state, 'spines': ec_spines, 'ticks': {'widths': ec_ticks}, 'curve': ec_curve, 'y_reversed': ec_reversed, 'y_mode': ec_y_mode, 'ion_params': ion_params, 'visible': ec_visible, 'labelpads': ec_labelpads},
3696
+ 'operando': {'cmap': cmap_name, 'wasd_state': op_wasd_state, 'spines': op_spines, 'ticks': {'widths': op_ticks}, 'y_reversed': op_reversed, 'intensity_range': intensity_range, 'labelpads': op_labelpads, 'title_offsets': op_title_offsets},
3697
+ 'ec': {'wasd_state': ec_wasd_state, 'spines': ec_spines, 'ticks': {'widths': ec_ticks}, 'curve': ec_curve, 'y_reversed': ec_reversed, 'y_mode': ec_y_mode, 'ion_params': ion_params, 'visible': ec_visible, 'labelpads': ec_labelpads, 'title_offsets': ec_title_offsets},
3612
3698
  'font': {'family': fam, 'size': fsize},
3613
3699
  'colorbar': {'label': cb_label_text, 'mode': cb_label_mode, 'visible': cb_visible},
3614
3700
  }
@@ -3621,8 +3707,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3621
3707
  'version': 2,
3622
3708
  'figure': {'canvas_size': [fig_w, fig_h], 'cb_visible': cb_visible, 'cb_label_mode': cb_label_mode},
3623
3709
  'geometry': {'op_w_in': ax_w_in, 'op_h_in': ax_h_in, 'ec_w_in': ec_w_in, 'cb_h_offset': float(cb_h_offset), 'ec_h_offset': float(ec_h_offset) if ec_h_offset is not None else None},
3624
- 'operando': {'cmap': cmap_name, 'wasd_state': op_wasd_state, 'spines': op_spines, 'ticks': {'widths': op_ticks}, 'y_reversed': op_reversed, 'intensity_range': intensity_range, 'labelpads': op_labelpads},
3625
- 'ec': {'wasd_state': ec_wasd_state, 'spines': ec_spines, 'ticks': {'widths': ec_ticks}, 'curve': ec_curve, 'y_reversed': ec_reversed, 'y_mode': ec_y_mode, 'ion_params': ion_params, 'visible': ec_visible, 'labelpads': ec_labelpads},
3710
+ 'operando': {'cmap': cmap_name, 'wasd_state': op_wasd_state, 'spines': op_spines, 'ticks': {'widths': op_ticks}, 'y_reversed': op_reversed, 'intensity_range': intensity_range, 'labelpads': op_labelpads, 'title_offsets': op_title_offsets},
3711
+ 'ec': {'wasd_state': ec_wasd_state, 'spines': ec_spines, 'ticks': {'widths': ec_ticks}, 'curve': ec_curve, 'y_reversed': ec_reversed, 'y_mode': ec_y_mode, 'ion_params': ion_params, 'visible': ec_visible, 'labelpads': ec_labelpads, 'title_offsets': ec_title_offsets},
3626
3712
  'font': {'family': fam, 'size': fsize},
3627
3713
  'axes_geometry': _get_geometry_snapshot(ax, ec_ax),
3628
3714
  'colorbar': {'label': cb_label_text, 'mode': cb_label_mode, 'visible': cb_visible},
@@ -4223,6 +4309,33 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4223
4309
  except Exception:
4224
4310
  pass
4225
4311
 
4312
+ # Restore title offsets BEFORE applying labelpads
4313
+ if version >= 2:
4314
+ try:
4315
+ op_offsets = op.get('title_offsets', {})
4316
+ if op_offsets:
4317
+ ax._top_xlabel_manual_offset_y_pts = float(op_offsets.get('top_y', 0.0) or 0.0)
4318
+ ax._top_xlabel_manual_offset_x_pts = float(op_offsets.get('top_x', 0.0) or 0.0)
4319
+ ax._bottom_xlabel_manual_offset_y_pts = float(op_offsets.get('bottom_y', 0.0) or 0.0)
4320
+ ax._left_ylabel_manual_offset_x_pts = float(op_offsets.get('left_x', 0.0) or 0.0)
4321
+ ax._right_ylabel_manual_offset_x_pts = float(op_offsets.get('right_x', 0.0) or 0.0)
4322
+ ax._right_ylabel_manual_offset_y_pts = float(op_offsets.get('right_y', 0.0) or 0.0)
4323
+ except Exception as e:
4324
+ print(f"Warning: Could not apply operando title offsets: {e}")
4325
+
4326
+ try:
4327
+ ec_cfg = cfg.get('ec', {})
4328
+ ec_offsets = ec_cfg.get('title_offsets', {})
4329
+ if ec_offsets and ec_ax is not None:
4330
+ ec_ax._top_xlabel_manual_offset_y_pts = float(ec_offsets.get('top_y', 0.0) or 0.0)
4331
+ ec_ax._top_xlabel_manual_offset_x_pts = float(ec_offsets.get('top_x', 0.0) or 0.0)
4332
+ ec_ax._bottom_xlabel_manual_offset_y_pts = float(ec_offsets.get('bottom_y', 0.0) or 0.0)
4333
+ ec_ax._left_ylabel_manual_offset_x_pts = float(ec_offsets.get('left_x', 0.0) or 0.0)
4334
+ ec_ax._right_ylabel_manual_offset_x_pts = float(ec_offsets.get('right_x', 0.0) or 0.0)
4335
+ ec_ax._right_ylabel_manual_offset_y_pts = float(ec_offsets.get('right_y', 0.0) or 0.0)
4336
+ except Exception as e:
4337
+ print(f"Warning: Could not apply EC title offsets: {e}")
4338
+
4226
4339
  # Apply labelpads (title positioning) - preserve current if not in config
4227
4340
  if version >= 2:
4228
4341
  try:
@@ -4266,6 +4379,44 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4266
4379
  except Exception as e:
4267
4380
  print(f"Warning: Could not apply EC labelpads: {e}")
4268
4381
 
4382
+ # Reposition titles to apply offsets (after labelpads are set)
4383
+ try:
4384
+ from .ui import position_top_xlabel as _ui_position_top_xlabel, position_bottom_xlabel as _ui_position_bottom_xlabel, position_left_ylabel as _ui_position_left_ylabel, position_right_ylabel as _ui_position_right_ylabel
4385
+ # Build tick_state for operando pane
4386
+ op_ts = getattr(ax, '_saved_tick_state', {})
4387
+ op_tick_state = {
4388
+ 't_ticks': bool(op_ts.get('t_ticks', op_ts.get('tx', False))),
4389
+ 't_labels': bool(op_ts.get('t_labels', op_ts.get('tx', False))),
4390
+ 'b_ticks': bool(op_ts.get('b_ticks', op_ts.get('bx', True))),
4391
+ 'b_labels': bool(op_ts.get('b_labels', op_ts.get('bx', True))),
4392
+ 'l_ticks': bool(op_ts.get('l_ticks', op_ts.get('ly', True))),
4393
+ 'l_labels': bool(op_ts.get('l_labels', op_ts.get('ly', True))),
4394
+ 'r_ticks': bool(op_ts.get('r_ticks', op_ts.get('ry', False))),
4395
+ 'r_labels': bool(op_ts.get('r_labels', op_ts.get('ry', False))),
4396
+ }
4397
+ _ui_position_top_xlabel(ax, fig, op_tick_state)
4398
+ _ui_position_bottom_xlabel(ax, fig, op_tick_state)
4399
+ _ui_position_left_ylabel(ax, fig, op_tick_state)
4400
+ _ui_position_right_ylabel(ax, fig, op_tick_state)
4401
+ if ec_ax is not None:
4402
+ ec_ts = getattr(ec_ax, '_saved_tick_state', {})
4403
+ ec_tick_state = {
4404
+ 't_ticks': bool(ec_ts.get('t_ticks', ec_ts.get('tx', False))),
4405
+ 't_labels': bool(ec_ts.get('t_labels', ec_ts.get('tx', False))),
4406
+ 'b_ticks': bool(ec_ts.get('b_ticks', ec_ts.get('bx', True))),
4407
+ 'b_labels': bool(ec_ts.get('b_labels', ec_ts.get('bx', True))),
4408
+ 'l_ticks': bool(ec_ts.get('l_ticks', ec_ts.get('ly', True))),
4409
+ 'l_labels': bool(ec_ts.get('l_labels', ec_ts.get('ly', True))),
4410
+ 'r_ticks': bool(ec_ts.get('r_ticks', ec_ts.get('ry', False))),
4411
+ 'r_labels': bool(ec_ts.get('r_labels', ec_ts.get('ry', False))),
4412
+ }
4413
+ _ui_position_top_xlabel(ec_ax, fig, ec_tick_state)
4414
+ _ui_position_bottom_xlabel(ec_ax, fig, ec_tick_state)
4415
+ _ui_position_left_ylabel(ec_ax, fig, ec_tick_state)
4416
+ _ui_position_right_ylabel(ec_ax, fig, ec_tick_state)
4417
+ except Exception as e:
4418
+ print(f"Warning: Could not reposition titles: {e}")
4419
+
4269
4420
  # Final redraw
4270
4421
  try:
4271
4422
  fig.canvas.draw()