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.
- batplot/__init__.py +1 -1
- batplot/batplot.py +2 -2
- batplot/cpc_interactive.py +938 -325
- batplot/electrochem_interactive.py +153 -15
- batplot/interactive.py +157 -31
- batplot/operando_ec_interactive.py +163 -12
- batplot/session.py +205 -17
- batplot/style.py +109 -47
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/METADATA +23 -2
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/RECORD +14 -14
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/WHEEL +0 -0
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.23.dist-info → batplot-1.7.25.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
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
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3553
|
-
|
|
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()
|