batplot 1.7.23__py3-none-any.whl → 1.7.25__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.
@@ -468,6 +468,8 @@ def _get_legend_title(fig, default: str = "Cycle") -> str:
468
468
  def _rebuild_legend(ax):
469
469
  """Rebuild legend using only visible lines, anchoring to absolute inches from canvas center if available."""
470
470
  fig = ax.figure
471
+ # Capture existing title before any rebuild so it isn't lost
472
+ _store_legend_title(fig, ax)
471
473
  if not _get_legend_user_pref(fig):
472
474
  leg = ax.get_legend()
473
475
  if leg is not None:
@@ -1271,6 +1273,10 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1271
1273
  snap['legend']['visible'] = bool(leg_obj.get_visible()) if leg_obj is not None else False
1272
1274
  except Exception:
1273
1275
  pass
1276
+ try:
1277
+ snap['legend']['title'] = _get_legend_title(fig)
1278
+ except Exception:
1279
+ snap['legend']['title'] = None
1274
1280
  try:
1275
1281
  legend_xy = getattr(fig, '_ec_legend_xy_in', None)
1276
1282
  if legend_xy is not None:
@@ -1299,7 +1305,33 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1299
1305
  if len(state_history) > 40:
1300
1306
  state_history.pop(0)
1301
1307
  except Exception:
1302
- pass
1308
+ # Minimal fallback so undo still works if full snapshot fails
1309
+ try:
1310
+ fallback = {
1311
+ 'note': f"{note}-fallback",
1312
+ 'xlim': ax.get_xlim(),
1313
+ 'ylim': ax.get_ylim(),
1314
+ 'legend': {
1315
+ 'visible': bool(ax.get_legend().get_visible()) if ax.get_legend() else False,
1316
+ 'position_inches': getattr(fig, '_ec_legend_xy_in', None),
1317
+ 'title': _get_legend_title(fig),
1318
+ },
1319
+ 'lines': []
1320
+ }
1321
+ for i, ln in enumerate(ax.lines):
1322
+ try:
1323
+ fallback['lines'].append({
1324
+ 'index': i,
1325
+ 'color': ln.get_color(),
1326
+ 'visible': ln.get_visible(),
1327
+ })
1328
+ except Exception:
1329
+ fallback['lines'].append({'index': i})
1330
+ state_history.append(fallback)
1331
+ if len(state_history) > 40:
1332
+ state_history.pop(0)
1333
+ except Exception:
1334
+ pass
1303
1335
 
1304
1336
  def restore_state():
1305
1337
  if not state_history:
@@ -1529,6 +1561,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1529
1561
  legend_snap = snap.get('legend', {})
1530
1562
  if legend_snap:
1531
1563
  try:
1564
+ if 'title' in legend_snap:
1565
+ fig._ec_legend_title = legend_snap.get('title') or _get_legend_title(fig)
1532
1566
  xy = legend_snap.get('position_inches')
1533
1567
  fig._ec_legend_xy_in = _sanitize_legend_offset(fig, xy) if xy is not None else None
1534
1568
  except Exception:
@@ -1594,14 +1628,35 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1594
1628
  else:
1595
1629
  print(f" {i}: {fname}")
1596
1630
 
1597
- fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1631
+ last_figure_path = getattr(fig, '_last_figure_export_path', None)
1632
+ if last_figure_path:
1633
+ fname = input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
1634
+ else:
1635
+ fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1598
1636
  if not fname or fname.lower() == 'q':
1599
1637
  _print_menu(len(all_cycles), is_dqdv)
1600
1638
  continue
1601
1639
 
1640
+ already_confirmed = False # Initialize for new filename case
1641
+ # Check for 'o' option
1642
+ if fname.lower() == 'o':
1643
+ if not last_figure_path:
1644
+ print("No previous export found.")
1645
+ _print_menu(len(all_cycles), is_dqdv)
1646
+ continue
1647
+ if not os.path.exists(last_figure_path):
1648
+ print(f"Previous export file not found: {last_figure_path}")
1649
+ _print_menu(len(all_cycles), is_dqdv)
1650
+ continue
1651
+ yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
1652
+ if yn != 'y':
1653
+ _print_menu(len(all_cycles), is_dqdv)
1654
+ continue
1655
+ target = last_figure_path
1656
+ already_confirmed = True
1602
1657
  # Check if user selected a number
1603
- already_confirmed = False
1604
- if fname.isdigit() and files:
1658
+ elif fname.isdigit() and files:
1659
+ already_confirmed = False
1605
1660
  idx = int(fname)
1606
1661
  if 1 <= idx <= len(files):
1607
1662
  name = files[idx-1]
@@ -1671,6 +1726,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1671
1726
  else:
1672
1727
  fig.savefig(target, bbox_inches='tight')
1673
1728
  print(f"Exported figure to {target}")
1729
+ fig._last_figure_export_path = target
1674
1730
  except Exception as e:
1675
1731
  print(f"Export failed: {e}")
1676
1732
  except Exception as e:
@@ -1854,11 +1910,45 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1854
1910
  else:
1855
1911
  print(f" {_i}: {fname}")
1856
1912
 
1857
- sub = input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
1913
+ last_style_path = getattr(fig, '_last_style_export_path', None)
1914
+ if last_style_path:
1915
+ sub = input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
1916
+ else:
1917
+ sub = input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
1858
1918
  if sub == 'q':
1859
1919
  break
1860
1920
  if sub == 'r' or sub == '':
1861
1921
  continue
1922
+ if sub == 'o':
1923
+ # Overwrite last exported style file
1924
+ if not last_style_path:
1925
+ print("No previous export found.")
1926
+ continue
1927
+ if not os.path.exists(last_style_path):
1928
+ print(f"Previous export file not found: {last_style_path}")
1929
+ continue
1930
+ yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
1931
+ if yn != 'y':
1932
+ continue
1933
+ # Rebuild config based on current state
1934
+ cfg = _get_style_snapshot(fig, ax, cycle_lines, tick_state)
1935
+ # Determine if last export was style-only or style+geometry
1936
+ try:
1937
+ with open(last_style_path, 'r', encoding='utf-8') as f:
1938
+ old_cfg = json.load(f)
1939
+ if old_cfg.get('kind') == 'ec_style_geom':
1940
+ geom = _get_geometry_snapshot(fig, ax)
1941
+ cfg['kind'] = 'ec_style_geom'
1942
+ cfg['geometry'] = geom
1943
+ else:
1944
+ cfg['kind'] = 'ec_style'
1945
+ except Exception:
1946
+ cfg['kind'] = 'ec_style'
1947
+ with open(last_style_path, 'w', encoding='utf-8') as f:
1948
+ json.dump(cfg, f, indent=2)
1949
+ print(f"Overwritten style to {last_style_path}")
1950
+ style_menu_active = False
1951
+ break
1862
1952
  if sub == 'e':
1863
1953
  # Ask for ps or psg
1864
1954
  print("Export options:")
@@ -1895,7 +1985,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1895
1985
  print("Style export canceled.")
1896
1986
  continue
1897
1987
  print(f"\nChosen path: {save_base}")
1898
- _export_style_dialog(cfg, default_ext=default_ext, base_path=save_base)
1988
+ exported_path = _export_style_dialog(cfg, default_ext=default_ext, base_path=save_base)
1989
+ if exported_path:
1990
+ fig._last_style_export_path = exported_path
1899
1991
  style_menu_active = False # Exit style submenu and return to main menu
1900
1992
  break
1901
1993
  else:
@@ -2197,6 +2289,19 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2197
2289
  except Exception as e:
2198
2290
  print(f"Warning: Could not apply geometry: {e}")
2199
2291
 
2292
+ # Restore title offsets
2293
+ try:
2294
+ offsets = cfg.get('title_offsets', {})
2295
+ if offsets:
2296
+ ax._top_xlabel_manual_offset_y_pts = float(offsets.get('top_y', 0.0) or 0.0)
2297
+ ax._top_xlabel_manual_offset_x_pts = float(offsets.get('top_x', 0.0) or 0.0)
2298
+ ax._bottom_xlabel_manual_offset_y_pts = float(offsets.get('bottom_y', 0.0) or 0.0)
2299
+ ax._left_ylabel_manual_offset_x_pts = float(offsets.get('left_x', 0.0) or 0.0)
2300
+ ax._right_ylabel_manual_offset_x_pts = float(offsets.get('right_x', 0.0) or 0.0)
2301
+ ax._right_ylabel_manual_offset_y_pts = float(offsets.get('right_y', 0.0) or 0.0)
2302
+ except Exception:
2303
+ pass
2304
+
2200
2305
  # Final label positioning - do this AFTER all style changes to prevent drift
2201
2306
  # Set pending labelpad before repositioning to preserve original values
2202
2307
  try:
@@ -2210,12 +2315,11 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2210
2315
  font_cfg = cfg.get('font', {})
2211
2316
  font_changed = (font_cfg.get('family') is not None or font_cfg.get('size') is not None)
2212
2317
 
2213
- if axes_position_changed or font_changed:
2214
- # Reposition titles (will use _pending_xlabelpad if set, preserving original labelpad)
2215
- _ui_position_top_xlabel(ax, fig, tick_state)
2216
- _ui_position_bottom_xlabel(ax, fig, tick_state)
2217
- _ui_position_left_ylabel(ax, fig, tick_state)
2218
- _ui_position_right_ylabel(ax, fig, tick_state)
2318
+ # Always reposition titles to apply offsets (even if nothing else changed)
2319
+ _ui_position_top_xlabel(ax, fig, tick_state)
2320
+ _ui_position_bottom_xlabel(ax, fig, tick_state)
2321
+ _ui_position_left_ylabel(ax, fig, tick_state)
2322
+ _ui_position_right_ylabel(ax, fig, tick_state)
2219
2323
 
2220
2324
  # Always ensure labelpad is exactly as it was before style import
2221
2325
  # This is a final safeguard against any drift
@@ -2537,7 +2641,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2537
2641
  try:
2538
2642
  print("Tip: Use LaTeX/mathtext for special characters:")
2539
2643
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2540
- print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2644
+ print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2541
2645
  while True:
2542
2646
  print("Rename axis: x, y, both, q=back")
2543
2647
  sub = input("Rename> ").strip().lower()
@@ -2798,6 +2902,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2798
2902
  elif key == 's':
2799
2903
  try:
2800
2904
  from .session import dump_ec_session
2905
+ last_session_path = getattr(fig, '_last_session_save_path', None)
2801
2906
  folder = choose_save_path(source_paths, purpose="EC session save")
2802
2907
  if not folder:
2803
2908
  _print_menu(len(all_cycles), is_dqdv); continue
@@ -2815,10 +2920,27 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2815
2920
  print(f" {i}: {f} ({timestamp})")
2816
2921
  else:
2817
2922
  print(f" {i}: {f}")
2818
- prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
2923
+ if last_session_path:
2924
+ prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
2925
+ else:
2926
+ prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
2819
2927
  choice = input(prompt).strip()
2820
2928
  if not choice or choice.lower() == 'q':
2821
2929
  _print_menu(len(all_cycles), is_dqdv); continue
2930
+ if choice.lower() == 'o':
2931
+ # Overwrite last saved session
2932
+ if not last_session_path:
2933
+ print("No previous save found.")
2934
+ _print_menu(len(all_cycles), is_dqdv); continue
2935
+ if not os.path.exists(last_session_path):
2936
+ print(f"Previous save file not found: {last_session_path}")
2937
+ _print_menu(len(all_cycles), is_dqdv); continue
2938
+ yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
2939
+ if yn != 'y':
2940
+ _print_menu(len(all_cycles), is_dqdv); continue
2941
+ dump_ec_session(last_session_path, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
2942
+ print(f"Overwritten session to {last_session_path}")
2943
+ _print_menu(len(all_cycles), is_dqdv); continue
2822
2944
  if choice.isdigit() and files:
2823
2945
  idx = int(choice)
2824
2946
  if 1 <= idx <= len(files):
@@ -2827,10 +2949,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2827
2949
  if yn != 'y':
2828
2950
  _print_menu(len(all_cycles), is_dqdv); continue
2829
2951
  target = os.path.join(folder, name)
2952
+ dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
2953
+ fig._last_session_save_path = target
2954
+ _print_menu(len(all_cycles), is_dqdv); continue
2830
2955
  else:
2831
2956
  print("Invalid number.")
2832
2957
  _print_menu(len(all_cycles), is_dqdv); continue
2833
- else:
2958
+ if choice.lower() != 'o':
2834
2959
  name = choice
2835
2960
  root, ext = os.path.splitext(name)
2836
2961
  if ext == '':
@@ -2841,6 +2966,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2841
2966
  if yn != 'y':
2842
2967
  _print_menu(len(all_cycles), is_dqdv); continue
2843
2968
  dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
2969
+ fig._last_session_save_path = target
2844
2970
  except Exception as e:
2845
2971
  print(f"Save failed: {e}")
2846
2972
  _print_menu(len(all_cycles), is_dqdv)
@@ -4008,6 +4134,14 @@ def _get_style_snapshot(fig, ax, cycle_lines: Dict, tick_state: Dict) -> Dict:
4008
4134
  'ticks': {'widths': tick_widths, 'direction': tick_direction},
4009
4135
  'grid': grid_enabled,
4010
4136
  'wasd_state': wasd_state,
4137
+ 'title_offsets': {
4138
+ 'top_y': float(getattr(ax, '_top_xlabel_manual_offset_y_pts', 0.0) or 0.0),
4139
+ 'top_x': float(getattr(ax, '_top_xlabel_manual_offset_x_pts', 0.0) or 0.0),
4140
+ 'bottom_y': float(getattr(ax, '_bottom_xlabel_manual_offset_y_pts', 0.0) or 0.0),
4141
+ 'left_x': float(getattr(ax, '_left_ylabel_manual_offset_x_pts', 0.0) or 0.0),
4142
+ 'right_x': float(getattr(ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0),
4143
+ 'right_y': float(getattr(ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0),
4144
+ },
4011
4145
  'curve_linewidth': curve_linewidth,
4012
4146
  'curve_markers': curve_marker_props,
4013
4147
  'rotation_angle': getattr(fig, '_ec_rotation_angle', 0),
@@ -4236,9 +4370,11 @@ def _export_style_dialog(cfg: Dict, default_ext: str = '.bpcfg', base_path: Opti
4236
4370
  with open(target_path, 'w', encoding='utf-8') as f:
4237
4371
  json.dump(cfg, f, indent=2)
4238
4372
  print(f"Style exported to {target_path}")
4373
+ return target_path
4239
4374
 
4240
4375
  except Exception as e:
4241
4376
  print(f"Export failed: {e}")
4377
+ return None
4242
4378
  def _legend_no_frame(ax, *args, title: Optional[str] = None, **kwargs):
4243
4379
  leg = ax.legend(*args, **kwargs)
4244
4380
  if leg is not None:
@@ -4258,6 +4394,8 @@ def _apply_legend_position(fig, ax):
4258
4394
  xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', None))
4259
4395
  if xy_in is None:
4260
4396
  return False
4397
+ # Preserve current title before rebuilding the legend
4398
+ _store_legend_title(fig, ax)
4261
4399
  handles, labels = _visible_legend_entries(ax)
4262
4400
  if not handles:
4263
4401
  return False
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
@@ -2096,7 +2179,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2096
2179
  if mode == 'c':
2097
2180
  print("Tip: Use LaTeX/mathtext for special characters:")
2098
2181
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2099
- print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2182
+ print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2100
2183
  idx_in = input("Curve number to rename (q=cancel): ").strip()
2101
2184
  if not idx_in or idx_in.lower() == 'q':
2102
2185
  print("Canceled.")
@@ -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()