batplot 1.1.7__tar.gz → 1.2.0__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 (32) hide show
  1. {batplot-1.1.7 → batplot-1.2.0}/PKG-INFO +1 -1
  2. batplot-1.2.0/batplot/cli.py +0 -0
  3. {batplot-1.1.7 → batplot-1.2.0}/batplot/cpc_interactive.py +1 -2
  4. {batplot-1.1.7 → batplot-1.2.0}/batplot/electrochem_interactive.py +72 -10
  5. {batplot-1.1.7 → batplot-1.2.0}/batplot/operando_ec_interactive.py +60 -47
  6. {batplot-1.1.7 → batplot-1.2.0}/batplot/style.py +121 -10
  7. {batplot-1.1.7 → batplot-1.2.0}/batplot/ui.py +2 -2
  8. {batplot-1.1.7 → batplot-1.2.0}/batplot.egg-info/PKG-INFO +1 -1
  9. {batplot-1.1.7 → batplot-1.2.0}/pyproject.toml +1 -1
  10. batplot-1.1.7/batplot/cli.py +0 -34
  11. {batplot-1.1.7 → batplot-1.2.0}/LICENSE +0 -0
  12. {batplot-1.1.7 → batplot-1.2.0}/README.md +0 -0
  13. {batplot-1.1.7 → batplot-1.2.0}/batplot/__init__.py +0 -0
  14. {batplot-1.1.7 → batplot-1.2.0}/batplot/args.py +0 -0
  15. {batplot-1.1.7 → batplot-1.2.0}/batplot/batch.py +0 -0
  16. {batplot-1.1.7 → batplot-1.2.0}/batplot/batplot.py +0 -0
  17. {batplot-1.1.7 → batplot-1.2.0}/batplot/batplot_new.py +0 -0
  18. {batplot-1.1.7 → batplot-1.2.0}/batplot/cif.py +0 -0
  19. {batplot-1.1.7 → batplot-1.2.0}/batplot/converters.py +0 -0
  20. {batplot-1.1.7 → batplot-1.2.0}/batplot/interactive.py +0 -0
  21. {batplot-1.1.7 → batplot-1.2.0}/batplot/modes.py +0 -0
  22. {batplot-1.1.7 → batplot-1.2.0}/batplot/operando.py +0 -0
  23. {batplot-1.1.7 → batplot-1.2.0}/batplot/plotting.py +0 -0
  24. {batplot-1.1.7 → batplot-1.2.0}/batplot/readers.py +0 -0
  25. {batplot-1.1.7 → batplot-1.2.0}/batplot/session.py +0 -0
  26. {batplot-1.1.7 → batplot-1.2.0}/batplot/utils.py +0 -0
  27. {batplot-1.1.7 → batplot-1.2.0}/batplot.egg-info/SOURCES.txt +0 -0
  28. {batplot-1.1.7 → batplot-1.2.0}/batplot.egg-info/dependency_links.txt +0 -0
  29. {batplot-1.1.7 → batplot-1.2.0}/batplot.egg-info/entry_points.txt +0 -0
  30. {batplot-1.1.7 → batplot-1.2.0}/batplot.egg-info/requires.txt +0 -0
  31. {batplot-1.1.7 → batplot-1.2.0}/batplot.egg-info/top_level.txt +0 -0
  32. {batplot-1.1.7 → batplot-1.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.1.7
3
+ Version: 1.2.0
4
4
  Summary: Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License: MIT License
File without changes
@@ -259,7 +259,6 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
259
259
  'position_inches': legend_xy_in # [x, y] offset from canvas center in inches
260
260
  },
261
261
  'ticks': {
262
- 'visibility': tick_vis,
263
262
  'widths': {
264
263
  'x_major': _tick_width(ax.xaxis, 'major'),
265
264
  'x_minor': _tick_width(ax.xaxis, 'minor'),
@@ -1316,7 +1315,6 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1316
1315
  _print_menu(); continue
1317
1316
  elif key == 'i':
1318
1317
  try:
1319
- push_state("import-style")
1320
1318
  try:
1321
1319
  files = sorted([f for f in os.listdir(os.getcwd()) if f.lower().endswith('.bpcfg')])
1322
1320
  except Exception:
@@ -1328,6 +1326,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1328
1326
  inp = input("Enter number to open or filename (.bpcfg; q=cancel): ").strip()
1329
1327
  if not inp or inp.lower() == 'q':
1330
1328
  _print_menu(); continue
1329
+ push_state("import-style")
1331
1330
  if inp.isdigit() and files:
1332
1331
  idx = int(inp)
1333
1332
  if 1 <= idx <= len(files):
@@ -570,10 +570,10 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
570
570
  except Exception:
571
571
  pass
572
572
 
573
- # Convert to points and add gap (match matplotlib's labelpad = 14pt)
573
+ # Convert to points and add gap (match matplotlib's labelpad = 8pt)
574
574
  if max_w_px > 0:
575
575
  tick_width_pts = max_w_px * 72.0 / dpi
576
- dx_pts = tick_width_pts + 14.0 # 14pt gap to match left labelpad
576
+ dx_pts = tick_width_pts + 8.0 # 8pt gap to match left labelpad
577
577
  else:
578
578
  dx_pts = 6.0 # Minimal spacing when no tick labels (match small labelpad)
579
579
 
@@ -981,7 +981,6 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
981
981
  elif key == 'i':
982
982
  # Import style from .bpcfg (with numbered list)
983
983
  try:
984
- push_state("import-style")
985
984
  try:
986
985
  _bpcfg_files = sorted([f for f in os.listdir(os.getcwd()) if f.lower().endswith('.bpcfg')])
987
986
  except Exception:
@@ -993,6 +992,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
993
992
  inp = input("Enter number to open or filename (.bpcfg, q=cancel): ").strip()
994
993
  if not inp or inp.lower() == 'q':
995
994
  _print_menu(len(all_cycles), is_dqdv); continue
995
+ push_state("import-style")
996
996
  if inp.isdigit() and _bpcfg_files:
997
997
  _idx = int(inp)
998
998
  if 1 <= _idx <= len(_bpcfg_files):
@@ -1031,17 +1031,79 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1031
1031
  _apply_font_family(ax, font_cfg['family'])
1032
1032
  if font_cfg.get('size') is not None:
1033
1033
  _apply_font_size(ax, float(font_cfg['size']))
1034
- except Exception: pass
1034
+ except Exception as e:
1035
+ print(f"Warning: Could not apply figure/font settings: {e}")
1035
1036
 
1036
1037
  # WASD state and dependent components
1037
1038
  try:
1038
1039
  wasd_state = cfg.get('wasd_state')
1039
1040
  if wasd_state and isinstance(wasd_state, dict):
1040
- # Store on fig and apply
1041
- setattr(fig, '_ec_wasd_state', wasd_state)
1042
- _sync_tick_state()
1043
- _apply_wasd()
1044
- except Exception: pass
1041
+ # Apply spines
1042
+ for name in ('top','bottom','left','right'):
1043
+ side = wasd_state.get(name, {})
1044
+ if name in ax.spines and 'spine' in side:
1045
+ ax.spines[name].set_visible(bool(side['spine']))
1046
+
1047
+ # Apply major ticks & labels
1048
+ top_s = wasd_state.get('top', {})
1049
+ bot_s = wasd_state.get('bottom', {})
1050
+ left_s = wasd_state.get('left', {})
1051
+ right_s = wasd_state.get('right', {})
1052
+
1053
+ ax.tick_params(axis='x',
1054
+ top=bool(top_s.get('ticks', False)),
1055
+ bottom=bool(bot_s.get('ticks', True)),
1056
+ labeltop=bool(top_s.get('labels', False)),
1057
+ labelbottom=bool(bot_s.get('labels', True)))
1058
+ ax.tick_params(axis='y',
1059
+ left=bool(left_s.get('ticks', True)),
1060
+ right=bool(right_s.get('ticks', False)),
1061
+ labelleft=bool(left_s.get('labels', True)),
1062
+ labelright=bool(right_s.get('labels', False)))
1063
+
1064
+ # Apply minor ticks
1065
+ if top_s.get('minor') or bot_s.get('minor'):
1066
+ ax.xaxis.set_minor_locator(AutoMinorLocator())
1067
+ ax.xaxis.set_minor_formatter(NullFormatter())
1068
+ ax.tick_params(axis='x', which='minor',
1069
+ top=bool(top_s.get('minor', False)),
1070
+ bottom=bool(bot_s.get('minor', False)),
1071
+ labeltop=False, labelbottom=False)
1072
+
1073
+ if left_s.get('minor') or right_s.get('minor'):
1074
+ ax.yaxis.set_minor_locator(AutoMinorLocator())
1075
+ ax.yaxis.set_minor_formatter(NullFormatter())
1076
+ ax.tick_params(axis='y', which='minor',
1077
+ left=bool(left_s.get('minor', False)),
1078
+ right=bool(right_s.get('minor', False)),
1079
+ labelleft=False, labelright=False)
1080
+
1081
+ # Apply axis titles
1082
+ ax._top_xlabel_on = bool(top_s.get('title', False))
1083
+ ax._right_ylabel_on = bool(right_s.get('title', False))
1084
+
1085
+ # Update tick_state for consistency
1086
+ tick_state['t_ticks'] = bool(top_s.get('ticks', False))
1087
+ tick_state['t_labels'] = bool(top_s.get('labels', False))
1088
+ tick_state['b_ticks'] = bool(bot_s.get('ticks', True))
1089
+ tick_state['b_labels'] = bool(bot_s.get('labels', True))
1090
+ tick_state['l_ticks'] = bool(left_s.get('ticks', True))
1091
+ tick_state['l_labels'] = bool(left_s.get('labels', True))
1092
+ tick_state['r_ticks'] = bool(right_s.get('ticks', False))
1093
+ tick_state['r_labels'] = bool(right_s.get('labels', False))
1094
+ tick_state['mtx'] = bool(top_s.get('minor', False))
1095
+ tick_state['mbx'] = bool(bot_s.get('minor', False))
1096
+ tick_state['mly'] = bool(left_s.get('minor', False))
1097
+ tick_state['mry'] = bool(right_s.get('minor', False))
1098
+
1099
+ # Reposition titles
1100
+ _ui_position_top_xlabel(ax, fig, tick_state)
1101
+ _ui_position_bottom_xlabel(ax, fig, tick_state)
1102
+ _ui_position_left_ylabel(ax, fig, tick_state)
1103
+ _ui_position_right_ylabel(ax, fig, tick_state)
1104
+
1105
+ except Exception as e:
1106
+ print(f"Warning: Could not apply tick visibility: {e}")
1045
1107
 
1046
1108
  # Spines and Ticks (widths)
1047
1109
  try:
@@ -1903,7 +1965,7 @@ def _get_style_snapshot(fig, ax, cycle_lines: Dict, tick_state: Dict) -> Dict:
1903
1965
  },
1904
1966
  'font': {'family': font_fam0, 'size': font_size},
1905
1967
  'spines': spines,
1906
- 'ticks': {'widths': tick_widths, 'state': dict(tick_state)},
1968
+ 'ticks': {'widths': tick_widths},
1907
1969
  'wasd_state': wasd_state,
1908
1970
  'curve_linewidth': curve_linewidth,
1909
1971
  'curve_markers': curve_marker_props,
@@ -534,6 +534,15 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
534
534
  # Crosshair state for both axes
535
535
  # Undo history
536
536
  state_history = []
537
+
538
+ def _get_spine_visible(axis, which: str) -> bool:
539
+ """Helper to get spine visibility status"""
540
+ sp = axis.spines.get(which)
541
+ try:
542
+ return bool(sp.get_visible()) if sp is not None else False
543
+ except Exception:
544
+ return False
545
+
537
546
  def _snapshot(note: str = ""):
538
547
  try:
539
548
  fig_w, fig_h = _get_fig_size(fig)
@@ -561,37 +570,37 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
561
570
  # WASD state for both panes
562
571
  op_wasd = {
563
572
  'top': {'spine': _get_spine_visible(ax, 'top'), 'ticks': ax.xaxis._major_tick_kw.get('tick1On', True),
564
- 'minor': bool(ax.xaxis._minortickkw.get('tick1On', False)),
573
+ 'minor': bool(ax.xaxis._minor_tick_kw.get('tick1On', False)),
565
574
  'labels': ax.xaxis._major_tick_kw.get('label1On', True),
566
575
  'title': bool(getattr(ax, '_top_xlabel_on', False))},
567
576
  'bottom': {'spine': _get_spine_visible(ax, 'bottom'), 'ticks': ax.xaxis._major_tick_kw.get('tick2On', True),
568
- 'minor': bool(ax.xaxis._minortickkw.get('tick2On', False)),
577
+ 'minor': bool(ax.xaxis._minor_tick_kw.get('tick2On', False)),
569
578
  'labels': ax.xaxis._major_tick_kw.get('label2On', True),
570
579
  'title': bool(ax.get_xlabel())},
571
580
  'left': {'spine': _get_spine_visible(ax, 'left'), 'ticks': ax.yaxis._major_tick_kw.get('tick1On', True),
572
- 'minor': bool(ax.yaxis._minortickkw.get('tick1On', False)),
581
+ 'minor': bool(ax.yaxis._minor_tick_kw.get('tick1On', False)),
573
582
  'labels': ax.yaxis._major_tick_kw.get('label1On', True),
574
583
  'title': bool(ax.get_ylabel())},
575
584
  'right': {'spine': _get_spine_visible(ax, 'right'), 'ticks': ax.yaxis._major_tick_kw.get('tick2On', False),
576
- 'minor': bool(ax.yaxis._minortickkw.get('tick2On', False)),
585
+ 'minor': bool(ax.yaxis._minor_tick_kw.get('tick2On', False)),
577
586
  'labels': ax.yaxis._major_tick_kw.get('label2On', False),
578
587
  'title': bool(getattr(ax, '_right_ylabel_on', False))},
579
588
  }
580
589
  ec_wasd = {
581
590
  'top': {'spine': _get_spine_visible(ec_ax, 'top'), 'ticks': ec_ax.xaxis._major_tick_kw.get('tick1On', True),
582
- 'minor': bool(ec_ax.xaxis._minortickkw.get('tick1On', False)),
591
+ 'minor': bool(ec_ax.xaxis._minor_tick_kw.get('tick1On', False)),
583
592
  'labels': ec_ax.xaxis._major_tick_kw.get('label1On', True),
584
593
  'title': bool(getattr(ec_ax, '_top_xlabel_on', False))},
585
594
  'bottom': {'spine': _get_spine_visible(ec_ax, 'bottom'), 'ticks': ec_ax.xaxis._major_tick_kw.get('tick2On', True),
586
- 'minor': bool(ec_ax.xaxis._minortickkw.get('tick2On', False)),
595
+ 'minor': bool(ec_ax.xaxis._minor_tick_kw.get('tick2On', False)),
587
596
  'labels': ec_ax.xaxis._major_tick_kw.get('label2On', True),
588
597
  'title': bool(ec_ax.get_xlabel())},
589
598
  'left': {'spine': _get_spine_visible(ec_ax, 'left'), 'ticks': ec_ax.yaxis._major_tick_kw.get('tick1On', False),
590
- 'minor': bool(ec_ax.yaxis._minortickkw.get('tick1On', False)),
599
+ 'minor': bool(ec_ax.yaxis._minor_tick_kw.get('tick1On', False)),
591
600
  'labels': ec_ax.yaxis._major_tick_kw.get('label1On', False),
592
601
  'title': bool(ec_ax.get_ylabel())},
593
602
  'right': {'spine': _get_spine_visible(ec_ax, 'right'), 'ticks': ec_ax.yaxis._major_tick_kw.get('tick2On', True),
594
- 'minor': bool(ec_ax.yaxis._minortickkw.get('tick2On', False)),
603
+ 'minor': bool(ec_ax.yaxis._minor_tick_kw.get('tick2On', False)),
595
604
  'labels': ec_ax.yaxis._major_tick_kw.get('label2On', True),
596
605
  'title': bool(ec_ax.get_ylabel())},
597
606
  }
@@ -615,8 +624,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
615
624
  })
616
625
  if len(state_history) > 40:
617
626
  state_history.pop(0)
618
- except Exception:
619
- pass
627
+ except Exception as e:
628
+ print(f"Warning: snapshot failed: {e}")
620
629
  def _restore():
621
630
  if not state_history:
622
631
  print("No undo history."); return
@@ -1055,15 +1064,16 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1055
1064
  print(f"Save failed: {e}")
1056
1065
  print_menu(); continue
1057
1066
  if cmd == 'h':
1058
- _snapshot("height")
1059
1067
  print(f"Current height: {ax_h_in:.2f} in")
1060
1068
  val = input("New height (inches): ").strip()
1061
- try:
1062
- new_h = max(0.25, float(val))
1063
- ax_h_in = new_h
1064
- _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1065
- except Exception as e:
1066
- print(f"Invalid height: {e}")
1069
+ if val:
1070
+ _snapshot("height")
1071
+ try:
1072
+ new_h = max(0.25, float(val))
1073
+ ax_h_in = new_h
1074
+ _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1075
+ except Exception as e:
1076
+ print(f"Invalid height: {e}")
1067
1077
  print_menu()
1068
1078
  elif cmd == 'r':
1069
1079
  _snapshot("reverse")
@@ -1100,7 +1110,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1100
1110
  if sub == 'q':
1101
1111
  break
1102
1112
  if sub == 'f':
1103
- _snapshot("font-family")
1104
1113
  # Common font families with numbered options
1105
1114
  fonts = ['Arial', 'DejaVu Sans', 'Helvetica', 'Liberation Sans',
1106
1115
  'Times New Roman', 'Courier New', 'Verdana', 'Tahoma']
@@ -1111,6 +1120,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1111
1120
  choice = input("Font family (number or name): ").strip()
1112
1121
  if not choice:
1113
1122
  continue
1123
+ _snapshot("font-family")
1114
1124
  # Check if it's a number
1115
1125
  if choice.isdigit():
1116
1126
  idx = int(choice)
@@ -1125,12 +1135,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1125
1135
  set_fonts(family=choice)
1126
1136
  print(f"Applied font family: {choice}")
1127
1137
  elif sub == 's':
1128
- _snapshot("font-size")
1129
1138
  # Show current size and accept direct input
1130
1139
  cur_size = plt.rcParams.get('font.size', None)
1131
1140
  choice = input(f"Font size (current: {cur_size}): ").strip()
1132
1141
  if not choice:
1133
1142
  continue
1143
+ _snapshot("font-size")
1134
1144
  try:
1135
1145
  sz = float(choice)
1136
1146
  if sz > 0:
@@ -1143,7 +1153,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1143
1153
  print_menu()
1144
1154
  elif cmd == 'l':
1145
1155
  # Line widths submenu for both operando and EC panes
1146
- _snapshot("line-widths")
1147
1156
  print("Line widths: set frame (spines) and tick widths for both operando and EC")
1148
1157
  print("Enter frame/tick width (e.g., '1.5' or 'f t' for frame/tick separately)")
1149
1158
  print("Format examples:")
@@ -1156,6 +1165,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1156
1165
  print_menu()
1157
1166
  continue
1158
1167
 
1168
+ _snapshot("line-widths")
1159
1169
  try:
1160
1170
  parts = inp.split()
1161
1171
  if len(parts) == 1:
@@ -1283,7 +1293,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1283
1293
  axis.yaxis.set_major_locator(MaxNLocator(nbins='auto', steps=[1, 2, 5], min_n_ticks=4))
1284
1294
  except Exception:
1285
1295
  pass
1286
- _snapshot("toggle-ticks")
1287
1296
  while True:
1288
1297
  print("Choose pane: o=operando, e=ec, q=back")
1289
1298
  pane = input("ot> ").strip().lower()
@@ -1543,6 +1552,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1543
1552
  if side == 'right' and key == 'minor':
1544
1553
  ts['mry'] = bool(wasd['right']['minor'])
1545
1554
  if changed:
1555
+ _snapshot("toggle-ticks")
1546
1556
  _apply_wasd_axis(target, wasd)
1547
1557
  try:
1548
1558
  target._saved_tick_state = dict(ts)
@@ -1554,10 +1564,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1554
1564
  fig.canvas.draw_idle()
1555
1565
  print_menu()
1556
1566
  elif cmd == 'ox':
1557
- _snapshot("operando-xrange")
1558
1567
  cur = ax.get_xlim(); print(f"Current operando X: {cur[0]:.4g} {cur[1]:.4g}")
1559
1568
  line = input("New X range (min max, blank=cancel): ").strip()
1560
1569
  if line:
1570
+ _snapshot("operando-xrange")
1561
1571
  try:
1562
1572
  lo, hi = map(float, line.split())
1563
1573
  ax.set_xlim(lo, hi)
@@ -1568,10 +1578,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1568
1578
  print(f"Invalid range: {e}")
1569
1579
  print_menu()
1570
1580
  elif cmd == 'oy':
1571
- _snapshot("operando-yrange")
1572
1581
  cur = ax.get_ylim(); print(f"Current operando Y: {cur[0]:.4g} {cur[1]:.4g}")
1573
1582
  line = input("New Y range (min max, blank=cancel): ").strip()
1574
1583
  if line:
1584
+ _snapshot("operando-yrange")
1575
1585
  try:
1576
1586
  lo, hi = map(float, line.split())
1577
1587
  ax.set_ylim(lo, hi)
@@ -1582,7 +1592,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1582
1592
  print(f"Invalid range: {e}")
1583
1593
  print_menu()
1584
1594
  elif cmd == 'oz':
1585
- _snapshot("operando-intensity-range")
1586
1595
  try:
1587
1596
  cur = im.get_clim()
1588
1597
  print(f"Current normalized intensity range: {cur[0]:.4g} {cur[1]:.4g}")
@@ -1590,6 +1599,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1590
1599
  print("Could not retrieve current intensity range")
1591
1600
  line = input("New intensity range (min max, blank=cancel): ").strip()
1592
1601
  if line:
1602
+ _snapshot("operando-intensity-range")
1593
1603
  try:
1594
1604
  lo, hi = map(float, line.split())
1595
1605
  im.set_clim(lo, hi)
@@ -1603,26 +1613,28 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1603
1613
  print(f"Invalid range: {e}")
1604
1614
  print_menu()
1605
1615
  elif cmd in ('ow'):
1606
- _snapshot("operando-width")
1607
1616
  print(f"Current operando width: {ax_w_in:.2f} in")
1608
1617
  val = input("New width (inches): ").strip()
1609
- try:
1610
- new_w = max(0.25, float(val))
1611
- ax_w_in = new_w
1612
- _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1613
- except Exception as e:
1614
- print(f"Invalid width: {e}")
1618
+ if val:
1619
+ _snapshot("operando-width")
1620
+ try:
1621
+ new_w = max(0.25, float(val))
1622
+ ax_w_in = new_w
1623
+ _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1624
+ except Exception as e:
1625
+ print(f"Invalid width: {e}")
1615
1626
  print_menu()
1616
1627
  elif cmd == 'ew':
1617
- _snapshot("ec-width")
1618
1628
  print(f"Current EC width: {ec_w_in:.2f} in")
1619
1629
  val = input("New EC width (inches): ").strip()
1620
- try:
1621
- new_w = max(0.25, float(val))
1622
- ec_w_in = new_w
1623
- _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1624
- except Exception as e:
1625
- print(f"Invalid EC width: {e}")
1630
+ if val:
1631
+ _snapshot("ec-width")
1632
+ try:
1633
+ new_w = max(0.25, float(val))
1634
+ ec_w_in = new_w
1635
+ _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1636
+ except Exception as e:
1637
+ print(f"Invalid EC width: {e}")
1626
1638
  print_menu()
1627
1639
  elif cmd == 'oc':
1628
1640
  # Change operando colormap (perceptually uniform suggestions)
@@ -1909,7 +1921,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1909
1921
  # Load a .bpcfg style and apply
1910
1922
  # Applies style properties from commands: oc, ow, ew, h, el, t, l, f, g, r
1911
1923
  try:
1912
- _snapshot("import-style")
1913
1924
  try:
1914
1925
  _bpcfg_files = sorted([f for f in os.listdir(os.getcwd()) if f.lower().endswith('.bpcfg')])
1915
1926
  except Exception:
@@ -1921,6 +1932,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
1921
1932
  inp = input("Enter number to open or filename (.bpcfg): ").strip()
1922
1933
  if not inp:
1923
1934
  print_menu(); continue
1935
+ _snapshot("import-style")
1924
1936
  if inp.isdigit() and _bpcfg_files:
1925
1937
  _idx = int(inp)
1926
1938
  if 1 <= _idx <= len(_bpcfg_files):
@@ -2233,10 +2245,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2233
2245
  if sub == 'q':
2234
2246
  break
2235
2247
  if sub == 'x':
2236
- _snapshot("rename-op-x")
2237
2248
  cur = ax.get_xlabel() or ''
2238
2249
  lab = input(f"New operando X label (blank=cancel, current='{cur}'): ").strip()
2239
2250
  if lab:
2251
+ _snapshot("rename-op-x")
2240
2252
  try:
2241
2253
  ax.set_xlabel(lab)
2242
2254
  ax._custom_labels['x'] = lab
@@ -2246,10 +2258,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2246
2258
  except Exception:
2247
2259
  pass
2248
2260
  elif sub == 'y':
2249
- _snapshot("rename-op-y")
2250
2261
  cur = ax.get_ylabel() or ''
2251
2262
  lab = input(f"New operando Y label (blank=cancel, current='{cur}'): ").strip()
2252
2263
  if lab:
2264
+ _snapshot("rename-op-y")
2253
2265
  try:
2254
2266
  ax.set_ylabel(lab)
2255
2267
  ax._custom_labels['y'] = lab
@@ -2278,10 +2290,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2278
2290
  if sub == 'q':
2279
2291
  break
2280
2292
  if sub == 'x':
2281
- _snapshot("rename-ec-x")
2282
2293
  cur = ec_ax.get_xlabel() or ''
2283
2294
  lab = input(f"New EC X label (blank=cancel, current='{cur}'): ").strip()
2284
2295
  if lab:
2296
+ _snapshot("rename-ec-x")
2285
2297
  try:
2286
2298
  ec_ax.set_xlabel(lab)
2287
2299
  ec_ax._custom_labels['x'] = lab
@@ -2290,10 +2302,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2290
2302
  except Exception:
2291
2303
  pass
2292
2304
  elif sub == 'y':
2293
- _snapshot("rename-ec-y")
2294
2305
  cur = ec_ax.get_ylabel() or ''
2295
2306
  lab = input(f"New EC Y label (blank=cancel, current='{cur}'): ").strip()
2296
2307
  if lab:
2308
+ _snapshot("rename-ec-y")
2297
2309
  try:
2298
2310
  ec_ax.set_ylabel(lab)
2299
2311
  # Store against current mode
@@ -2331,22 +2343,22 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2331
2343
  if sub == 'q':
2332
2344
  break
2333
2345
  if sub == 'c':
2334
- _snapshot("ec-line-color")
2335
2346
  cur = ln.get_color()
2336
2347
  val = input(f"Color (name or hex, current={cur}, blank=cancel): ").strip()
2337
2348
  if not val:
2338
2349
  continue
2350
+ _snapshot("ec-line-color")
2339
2351
  try:
2340
2352
  ln.set_color(val)
2341
2353
  fig.canvas.draw_idle()
2342
2354
  except Exception as e:
2343
2355
  print(f"Invalid color: {e}")
2344
2356
  elif sub == 'l':
2345
- _snapshot("ec-line-width")
2346
2357
  cur = ln.get_linewidth()
2347
2358
  val = input(f"Line width (current={cur}, blank=cancel): ").strip()
2348
2359
  if not val:
2349
2360
  continue
2361
+ _snapshot("ec-line-width")
2350
2362
  try:
2351
2363
  lw = float(val)
2352
2364
  if lw > 0:
@@ -2362,10 +2374,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2362
2374
  print(f"EC line styling failed: {e}")
2363
2375
  print_menu()
2364
2376
  elif cmd == 'et':
2365
- _snapshot("ec-time-range")
2366
2377
  cur = ec_ax.get_ylim(); print(f"Current EC time range (Y): {cur[0]:.4g} {cur[1]:.4g}")
2367
2378
  line = input("New time range (min max, blank=cancel): ").strip()
2368
2379
  if line:
2380
+ _snapshot("ec-time-range")
2369
2381
  try:
2370
2382
  lo, hi = map(float, line.split())
2371
2383
  ec_ax.set_ylim(lo, hi)
@@ -2438,7 +2450,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2438
2450
  if not sub or sub == 'q':
2439
2451
  print_menu(); continue
2440
2452
  if sub == 'n':
2441
- _snapshot("ey->ions")
2442
2453
  # Get or update parameters; allow reuse of previous values
2443
2454
  params = getattr(ec_ax, '_ion_params', {"mass_mg": None, "cap_per_ion_mAh_g": None, "start_ions": None, "material": "cathode"})
2444
2455
  mass_mg = params.get('mass_mg')
@@ -2469,6 +2480,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2469
2480
  if material is None:
2470
2481
  material = 'cathode'
2471
2482
  ec_ax._ion_params = {"mass_mg": mass_mg, "cap_per_ion_mAh_g": cap_per_ion, "start_ions": start_ions, "material": material}
2483
+ _snapshot("ey->ions")
2472
2484
  import numpy as np
2473
2485
  t = np.asarray(time_h, float)
2474
2486
  i_mA = np.asarray(current_mA, float)
@@ -2734,6 +2746,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax):
2734
2746
  print("Canvas: only figure size will change; panel widths/gaps are not altered.")
2735
2747
  line = input("New canvas size 'W H' (blank=cancel): ").strip()
2736
2748
  if line:
2749
+ _snapshot("canvas-size")
2737
2750
  try:
2738
2751
  parts = line.split()
2739
2752
  if len(parts) == 2:
@@ -166,6 +166,46 @@ def export_style_config(
166
166
  bbox = ax.get_position()
167
167
  frame_w_in = bbox.width * fw
168
168
  frame_h_in = bbox.height * fh
169
+
170
+ # Build WASD state (20 parameters: 4 sides × 5 properties each)
171
+ def _get_spine_visible(which: str) -> bool:
172
+ sp = ax.spines.get(which)
173
+ try:
174
+ return bool(sp.get_visible()) if sp is not None else False
175
+ except Exception:
176
+ return False
177
+
178
+ wasd_state = {
179
+ 'top': {
180
+ 'spine': _get_spine_visible('top'),
181
+ 'ticks': bool(tick_state.get('t_ticks', tick_state.get('tx', False))),
182
+ 'minor': bool(tick_state.get('mtx', False)),
183
+ 'labels': bool(tick_state.get('t_labels', tick_state.get('tx', False))),
184
+ 'title': bool(getattr(ax, '_top_xlabel_on', False))
185
+ },
186
+ 'bottom': {
187
+ 'spine': _get_spine_visible('bottom'),
188
+ 'ticks': bool(tick_state.get('b_ticks', tick_state.get('bx', True))),
189
+ 'minor': bool(tick_state.get('mbx', False)),
190
+ 'labels': bool(tick_state.get('b_labels', tick_state.get('bx', True))),
191
+ 'title': bool(ax.get_xlabel())
192
+ },
193
+ 'left': {
194
+ 'spine': _get_spine_visible('left'),
195
+ 'ticks': bool(tick_state.get('l_ticks', tick_state.get('ly', True))),
196
+ 'minor': bool(tick_state.get('mly', False)),
197
+ 'labels': bool(tick_state.get('l_labels', tick_state.get('ly', True))),
198
+ 'title': bool(ax.get_ylabel())
199
+ },
200
+ 'right': {
201
+ 'spine': _get_spine_visible('right'),
202
+ 'ticks': bool(tick_state.get('r_ticks', tick_state.get('ry', False))),
203
+ 'minor': bool(tick_state.get('mry', False)),
204
+ 'labels': bool(tick_state.get('r_labels', tick_state.get('ry', False))),
205
+ 'title': bool(getattr(ax, '_right_ylabel_on', False))
206
+ },
207
+ }
208
+
169
209
  cfg = {
170
210
  "figure": {
171
211
  "size": [fw, fh],
@@ -184,12 +224,12 @@ def export_style_config(
184
224
  "family_chain": plt.rcParams.get("font.sans-serif"),
185
225
  },
186
226
  "ticks": {
187
- "visibility": tick_state.copy(),
188
227
  "x_major_width": axis_tick_width(ax.xaxis, "major"),
189
228
  "x_minor_width": axis_tick_width(ax.xaxis, "minor"),
190
229
  "y_major_width": axis_tick_width(ax.yaxis, "major"),
191
230
  "y_minor_width": axis_tick_width(ax.yaxis, "minor"),
192
231
  },
232
+ "wasd_state": wasd_state,
193
233
  "spines": {
194
234
  name: {
195
235
  "linewidth": spn.get_linewidth(),
@@ -403,17 +443,88 @@ def apply_style_config(
403
443
 
404
444
  # Tick visibility + widths
405
445
  ticks_cfg = cfg.get("ticks", {})
406
- vis_cfg = ticks_cfg.get("visibility", {})
407
- changed_visibility = False
408
- for k, v in vis_cfg.items():
409
- if k in tick_state and isinstance(v, bool):
410
- tick_state[k] = v
411
- changed_visibility = True
412
- if changed_visibility:
446
+
447
+ # Try wasd_state first (version 2), fall back to visibility dict (version 1)
448
+ wasd = cfg.get("wasd_state", {})
449
+ if wasd:
450
+ # Apply WASD state (20 parameters)
413
451
  try:
414
- _ui_update_tick_visibility(ax, tick_state)
452
+ # Apply spines from wasd
453
+ for side in ('top', 'bottom', 'left', 'right'):
454
+ side_cfg = wasd.get(side, {})
455
+ if 'spine' in side_cfg and side in ax.spines:
456
+ ax.spines[side].set_visible(bool(side_cfg['spine']))
457
+
458
+ # Apply ticks and labels
459
+ top_cfg = wasd.get('top', {})
460
+ bot_cfg = wasd.get('bottom', {})
461
+ left_cfg = wasd.get('left', {})
462
+ right_cfg = wasd.get('right', {})
463
+
464
+ ax.tick_params(axis='x',
465
+ top=bool(top_cfg.get('ticks', False)),
466
+ bottom=bool(bot_cfg.get('ticks', True)),
467
+ labeltop=bool(top_cfg.get('labels', False)),
468
+ labelbottom=bool(bot_cfg.get('labels', True)))
469
+ ax.tick_params(axis='y',
470
+ left=bool(left_cfg.get('ticks', True)),
471
+ right=bool(right_cfg.get('ticks', False)),
472
+ labelleft=bool(left_cfg.get('labels', True)),
473
+ labelright=bool(right_cfg.get('labels', False)))
474
+
475
+ # Apply minor ticks
476
+ if top_cfg.get('minor') or bot_cfg.get('minor'):
477
+ from matplotlib.ticker import AutoMinorLocator, NullFormatter
478
+ ax.xaxis.set_minor_locator(AutoMinorLocator())
479
+ ax.xaxis.set_minor_formatter(NullFormatter())
480
+ ax.tick_params(axis='x', which='minor',
481
+ top=bool(top_cfg.get('minor', False)),
482
+ bottom=bool(bot_cfg.get('minor', False)),
483
+ labeltop=False, labelbottom=False)
484
+
485
+ if left_cfg.get('minor') or right_cfg.get('minor'):
486
+ from matplotlib.ticker import AutoMinorLocator, NullFormatter
487
+ ax.yaxis.set_minor_locator(AutoMinorLocator())
488
+ ax.yaxis.set_minor_formatter(NullFormatter())
489
+ ax.tick_params(axis='y', which='minor',
490
+ left=bool(left_cfg.get('minor', False)),
491
+ right=bool(right_cfg.get('minor', False)),
492
+ labelleft=False, labelright=False)
493
+
494
+ # Apply titles
495
+ ax._top_xlabel_on = bool(top_cfg.get('title', False))
496
+ ax._right_ylabel_on = bool(right_cfg.get('title', False))
497
+
498
+ # Update tick_state for consistency
499
+ tick_state['t_ticks'] = bool(top_cfg.get('ticks', False))
500
+ tick_state['t_labels'] = bool(top_cfg.get('labels', False))
501
+ tick_state['b_ticks'] = bool(bot_cfg.get('ticks', True))
502
+ tick_state['b_labels'] = bool(bot_cfg.get('labels', True))
503
+ tick_state['l_ticks'] = bool(left_cfg.get('ticks', True))
504
+ tick_state['l_labels'] = bool(left_cfg.get('labels', True))
505
+ tick_state['r_ticks'] = bool(right_cfg.get('ticks', False))
506
+ tick_state['r_labels'] = bool(right_cfg.get('labels', False))
507
+ tick_state['mtx'] = bool(top_cfg.get('minor', False))
508
+ tick_state['mbx'] = bool(bot_cfg.get('minor', False))
509
+ tick_state['mly'] = bool(left_cfg.get('minor', False))
510
+ tick_state['mry'] = bool(right_cfg.get('minor', False))
511
+
415
512
  except Exception as e:
416
- print(f"[DEBUG] Exception updating tick visibility: {e}")
513
+ print(f"Warning: Could not apply WASD tick visibility: {e}")
514
+ else:
515
+ # Fall back to old visibility dict
516
+ vis_cfg = ticks_cfg.get("visibility", {})
517
+ changed_visibility = False
518
+ for k, v in vis_cfg.items():
519
+ if k in tick_state and isinstance(v, bool):
520
+ tick_state[k] = v
521
+ changed_visibility = True
522
+ if changed_visibility:
523
+ try:
524
+ _ui_update_tick_visibility(ax, tick_state)
525
+ except Exception as e:
526
+ print(f"[DEBUG] Exception updating tick visibility: {e}")
527
+
417
528
 
418
529
  xmaj = ticks_cfg.get("x_major_width")
419
530
  xminr = ticks_cfg.get("x_minor_width")
@@ -250,7 +250,7 @@ def position_bottom_xlabel(ax, fig, tick_state: Dict[str, bool]):
250
250
  pass
251
251
  return
252
252
  # Otherwise choose pad based on current tick label visibility
253
- pad = 14 if bool(tick_state.get('b_labels', tick_state.get('bx', False))) else 6
253
+ pad = 8 if bool(tick_state.get('b_labels', tick_state.get('bx', False))) else 6
254
254
  try:
255
255
  ax.xaxis.labelpad = pad
256
256
  except Exception:
@@ -280,7 +280,7 @@ def position_left_ylabel(ax, fig, tick_state: Dict[str, bool]):
280
280
  except Exception:
281
281
  pass
282
282
  return
283
- pad = 14 if bool(tick_state.get('l_labels', tick_state.get('ly', False))) else 6
283
+ pad = 8 if bool(tick_state.get('l_labels', tick_state.get('ly', False))) else 6
284
284
  try:
285
285
  ax.yaxis.labelpad = pad
286
286
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.1.7
3
+ Version: 1.2.0
4
4
  Summary: Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)
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.1.7"
7
+ version = "1.2.0"
8
8
  description = "Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)"
9
9
  authors = [
10
10
  { name = "Tian Dai", email = "tianda@uio.no" }
@@ -1,34 +0,0 @@
1
- """CLI entry for batplot.
2
-
3
- Clean entry point that delegates to mode handlers without import-time side effects.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import sys
9
- from typing import Optional
10
-
11
- def main(argv: Optional[list] = None) -> int:
12
- """Main CLI entry point for batplot.
13
-
14
- Args:
15
- argv: Optional command line arguments (defaults to sys.argv)
16
-
17
- Returns:
18
- Exit code (0 for success, non-zero for error)
19
- """
20
- # Import here to avoid side effects at module import time
21
- if argv is not None:
22
- # Temporarily replace sys.argv for argument parsing
23
- old_argv = sys.argv
24
- sys.argv = ['batplot'] + list(argv)
25
-
26
- try:
27
- # Import the main batplot function (now refactored to be safe)
28
- from .batplot import batplot_main
29
- return batplot_main()
30
- finally:
31
- if argv is not None:
32
- sys.argv = old_argv
33
-
34
- __all__ = ["main"]
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