batplot 1.1.2__tar.gz → 1.1.4__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 (31) hide show
  1. {batplot-1.1.2 → batplot-1.1.4}/PKG-INFO +1 -1
  2. {batplot-1.1.2 → batplot-1.1.4}/batplot/batplot.py +71 -23
  3. {batplot-1.1.2 → batplot-1.1.4}/batplot/electrochem_interactive.py +47 -30
  4. {batplot-1.1.2 → batplot-1.1.4}/batplot/interactive.py +4 -31
  5. {batplot-1.1.2 → batplot-1.1.4}/batplot/modes.py +82 -11
  6. {batplot-1.1.2 → batplot-1.1.4}/batplot/session.py +10 -0
  7. {batplot-1.1.2 → batplot-1.1.4}/batplot.egg-info/PKG-INFO +1 -1
  8. {batplot-1.1.2 → batplot-1.1.4}/pyproject.toml +1 -1
  9. {batplot-1.1.2 → batplot-1.1.4}/LICENSE +0 -0
  10. {batplot-1.1.2 → batplot-1.1.4}/README.md +0 -0
  11. {batplot-1.1.2 → batplot-1.1.4}/batplot/__init__.py +0 -0
  12. {batplot-1.1.2 → batplot-1.1.4}/batplot/args.py +0 -0
  13. {batplot-1.1.2 → batplot-1.1.4}/batplot/batch.py +0 -0
  14. {batplot-1.1.2 → batplot-1.1.4}/batplot/batplot_new.py +0 -0
  15. {batplot-1.1.2 → batplot-1.1.4}/batplot/cif.py +0 -0
  16. {batplot-1.1.2 → batplot-1.1.4}/batplot/cli.py +0 -0
  17. {batplot-1.1.2 → batplot-1.1.4}/batplot/converters.py +0 -0
  18. {batplot-1.1.2 → batplot-1.1.4}/batplot/cpc_interactive.py +0 -0
  19. {batplot-1.1.2 → batplot-1.1.4}/batplot/operando.py +0 -0
  20. {batplot-1.1.2 → batplot-1.1.4}/batplot/operando_ec_interactive.py +0 -0
  21. {batplot-1.1.2 → batplot-1.1.4}/batplot/plotting.py +0 -0
  22. {batplot-1.1.2 → batplot-1.1.4}/batplot/readers.py +0 -0
  23. {batplot-1.1.2 → batplot-1.1.4}/batplot/style.py +0 -0
  24. {batplot-1.1.2 → batplot-1.1.4}/batplot/ui.py +0 -0
  25. {batplot-1.1.2 → batplot-1.1.4}/batplot/utils.py +0 -0
  26. {batplot-1.1.2 → batplot-1.1.4}/batplot.egg-info/SOURCES.txt +0 -0
  27. {batplot-1.1.2 → batplot-1.1.4}/batplot.egg-info/dependency_links.txt +0 -0
  28. {batplot-1.1.2 → batplot-1.1.4}/batplot.egg-info/entry_points.txt +0 -0
  29. {batplot-1.1.2 → batplot-1.1.4}/batplot.egg-info/requires.txt +0 -0
  30. {batplot-1.1.2 → batplot-1.1.4}/batplot.egg-info/top_level.txt +0 -0
  31. {batplot-1.1.2 → batplot-1.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.1.2
3
+ Version: 1.1.4
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
@@ -536,7 +536,9 @@ def batplot_main() -> int:
536
536
  _backend = _plt.get_backend()
537
537
  except Exception:
538
538
  _backend = "unknown"
539
- _is_noninteractive = isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
539
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
540
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
541
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
540
542
  if _is_noninteractive:
541
543
  print(f"Matplotlib backend '{_backend}' is non-interactive; a window cannot be shown.")
542
544
  print("Tips: unset MPLBACKEND or set a GUI backend, e.g. on macOS:")
@@ -564,7 +566,10 @@ def batplot_main() -> int:
564
566
  _backend = _plt.get_backend()
565
567
  except Exception:
566
568
  _backend = "unknown"
567
- if not (isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})):
569
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
570
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
571
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
572
+ if not _is_noninteractive:
568
573
  _plt.show()
569
574
  else:
570
575
  print(f"Matplotlib backend '{_backend}' is non-interactive; use --out to save the figure.")
@@ -731,7 +736,9 @@ def batplot_main() -> int:
731
736
  _backend = plt.get_backend()
732
737
  except Exception:
733
738
  _backend = "unknown"
734
- _is_noninteractive = isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
739
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
740
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
741
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
735
742
  if _is_noninteractive:
736
743
  print(f"Matplotlib backend '{_backend}' is non-interactive; a window cannot be shown.")
737
744
  print("Tips: unset MPLBACKEND or set a GUI backend, e.g. on macOS:")
@@ -765,7 +772,18 @@ def batplot_main() -> int:
765
772
  # Keep window open after menu
766
773
  plt.show()
767
774
  else:
768
- plt.show()
775
+ if not (args.savefig or args.out):
776
+ try:
777
+ _backend = plt.get_backend()
778
+ except Exception:
779
+ _backend = "unknown"
780
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
781
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
782
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
783
+ if not _is_noninteractive:
784
+ plt.show()
785
+ else:
786
+ print(f"Matplotlib backend '{_backend}' is non-interactive; use --out to save the figure.")
769
787
  exit(0)
770
788
 
771
789
  # dQ/dV plotting mode for supported .csv electrochemistry exports
@@ -936,7 +954,9 @@ def batplot_main() -> int:
936
954
  _backend = _plt.get_backend()
937
955
  except Exception:
938
956
  _backend = "unknown"
939
- _is_noninteractive = isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
957
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
958
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
959
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
940
960
  if _is_noninteractive:
941
961
  print(f"Matplotlib backend '{_backend}' is non-interactive; a window cannot be shown.")
942
962
  print("Tips: unset MPLBACKEND or set a GUI backend, e.g. on macOS:")
@@ -961,7 +981,10 @@ def batplot_main() -> int:
961
981
  _backend = _plt.get_backend()
962
982
  except Exception:
963
983
  _backend = "unknown"
964
- if not (isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})):
984
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
985
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
986
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
987
+ if not _is_noninteractive:
965
988
  _plt.show()
966
989
  else:
967
990
  print(f"Matplotlib backend '{_backend}' is non-interactive; use --out to save the figure.")
@@ -1039,29 +1062,44 @@ def batplot_main() -> int:
1039
1062
  # Interactive or show
1040
1063
  if args.interactive:
1041
1064
  try:
1042
- _plt.ion()
1043
- except Exception:
1044
- pass
1045
- try:
1046
- _plt.show(block=False)
1065
+ _backend = _plt.get_backend()
1047
1066
  except Exception:
1048
- pass
1049
- try:
1050
- if has_ec and (operando_ec_interactive_menu is not None) and (ec_ax is not None):
1051
- operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax)
1052
- else:
1053
- # Operando-only interactive menu has been removed; fall back to non-interactive view
1054
- print("Operando-only interactive menu is no longer available; showing figure without interactive controls.\nTip: include EC data to use the combined operando+EC interactive menu.")
1055
- except Exception as _ie:
1056
- print(f"Interactive menu failed: {_ie}")
1057
- _plt.show()
1067
+ _backend = "unknown"
1068
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
1069
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
1070
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
1071
+ if _is_noninteractive:
1072
+ print(f"Matplotlib backend '{_backend}' is non-interactive; a window cannot be shown.")
1073
+ print("Tips: unset MPLBACKEND or set a GUI backend")
1074
+ print("Or run without --interactive and use --out to save the figure.")
1075
+ else:
1076
+ try:
1077
+ _plt.ion()
1078
+ except Exception:
1079
+ pass
1080
+ try:
1081
+ _plt.show(block=False)
1082
+ except Exception:
1083
+ pass
1084
+ try:
1085
+ if has_ec and (operando_ec_interactive_menu is not None) and (ec_ax is not None):
1086
+ operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax)
1087
+ else:
1088
+ # Operando-only interactive menu has been removed; fall back to non-interactive view
1089
+ print("Operando-only interactive menu is no longer available; showing figure without interactive controls.\nTip: include EC data to use the combined operando+EC interactive menu.")
1090
+ except Exception as _ie:
1091
+ print(f"Interactive menu failed: {_ie}")
1092
+ _plt.show()
1058
1093
  else:
1059
1094
  if not (args.savefig or args.out):
1060
1095
  try:
1061
1096
  _backend = _plt.get_backend()
1062
1097
  except Exception:
1063
1098
  _backend = "unknown"
1064
- if not (isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})):
1099
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
1100
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
1101
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
1102
+ if not _is_noninteractive:
1065
1103
  _plt.show()
1066
1104
  else:
1067
1105
  print(f"Matplotlib backend '{_backend}' is non-interactive; use --out to save the figure.")
@@ -2324,7 +2362,17 @@ def batplot_main() -> int:
2324
2362
  print(f"Saved plot to {export_target}")
2325
2363
  else:
2326
2364
  # Default: show the plot in non-interactive, non-save mode
2327
- plt.show()
2365
+ try:
2366
+ _backend = plt.get_backend()
2367
+ except Exception:
2368
+ _backend = "unknown"
2369
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
2370
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
2371
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
2372
+ if not _is_noninteractive:
2373
+ plt.show()
2374
+ else:
2375
+ print(f"Matplotlib backend '{_backend}' is non-interactive; use --out to save the figure.")
2328
2376
 
2329
2377
  # Success
2330
2378
  return 0
@@ -25,7 +25,7 @@ from .plotting import update_labels as _update_labels
25
25
  from .utils import _confirm_overwrite
26
26
 
27
27
 
28
- def _print_menu(n_cycles: int):
28
+ def _print_menu(n_cycles: int, is_dqdv: bool = False):
29
29
  # Three-column menu similar to operando: Styles | Geometries | Options
30
30
  # Use dynamic column widths for clean alignment.
31
31
  col1 = [
@@ -38,11 +38,14 @@ def _print_menu(n_cycles: int):
38
38
  ]
39
39
  col2 = [
40
40
  "c: cycles/colors",
41
- "a: capacity/ion",
42
41
  "r: rename axes",
43
42
  "x: x-scale",
44
43
  "y: y-scale",
45
44
  ]
45
+ # Only show capacity/ion option when NOT in dQdV mode
46
+ if not is_dqdv:
47
+ col2.insert(1, "a: capacity/ion")
48
+
46
49
  col3 = [
47
50
  "p: print(export) style",
48
51
  "i: import style",
@@ -364,6 +367,15 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
364
367
 
365
368
  base_xlabel = ax.get_xlabel() or ''
366
369
  base_ylabel = ax.get_ylabel() or ''
370
+
371
+ # Detect dQdV mode: check stored flag first, then fall back to y-label detection
372
+ # This handles cases where the user renamed the y-axis and saved/reloaded the session
373
+ is_dqdv = getattr(ax, '_is_dqdv_mode', None)
374
+ if is_dqdv is None:
375
+ # Initial detection: check if y-label contains "dQ"
376
+ is_dqdv = 'dQ' in base_ylabel
377
+ # Store the mode on the axes for persistence
378
+ ax._is_dqdv_mode = is_dqdv
367
379
 
368
380
  def _set_spine_visible(which: str, visible: bool):
369
381
  sp = ax.spines.get(which)
@@ -765,7 +777,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
765
777
  print("Undo: restored previous state.")
766
778
  except Exception as e:
767
779
  print(f"Undo failed: {e}")
768
- _print_menu(len(all_cycles))
780
+ _print_menu(len(all_cycles), is_dqdv)
769
781
  while True:
770
782
  key = input("Press a key: ").strip().lower()
771
783
  if not key:
@@ -778,18 +790,18 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
778
790
  if confirm == 'y':
779
791
  break
780
792
  else:
781
- _print_menu(len(all_cycles))
793
+ _print_menu(len(all_cycles), is_dqdv)
782
794
  continue
783
795
  elif key == 'b':
784
796
  restore_state()
785
- _print_menu(len(all_cycles))
797
+ _print_menu(len(all_cycles), is_dqdv)
786
798
  continue
787
799
  elif key == 'e':
788
800
  # Export current figure to a file; default extension .svg if missing
789
801
  try:
790
802
  fname = input("Export filename (default .svg if no extension, q=cancel): ").strip()
791
803
  if not fname or fname.lower() == 'q':
792
- _print_menu(len(all_cycles))
804
+ _print_menu(len(all_cycles), is_dqdv)
793
805
  continue
794
806
  root, ext = os.path.splitext(fname)
795
807
  if ext == '':
@@ -843,7 +855,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
843
855
  print(f"Export failed: {e}")
844
856
  except Exception as e:
845
857
  print(f"Error exporting figure: {e}")
846
- _print_menu(len(all_cycles))
858
+ _print_menu(len(all_cycles), is_dqdv)
847
859
  continue
848
860
  elif key == 'h':
849
861
  # Legend submenu: toggle visibility and move legend in inches relative to canvas center
@@ -948,7 +960,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
948
960
  print("Unknown option.")
949
961
  except Exception:
950
962
  pass
951
- _print_menu(len(all_cycles))
963
+ _print_menu(len(all_cycles), is_dqdv)
952
964
  continue
953
965
  elif key == 'p':
954
966
  # Print current style and optionally export to .bpcfg
@@ -964,7 +976,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
964
976
 
965
977
  except Exception as e:
966
978
  print(f"Error in style menu: {e}")
967
- _print_menu(len(all_cycles))
979
+ _print_menu(len(all_cycles), is_dqdv)
968
980
  continue
969
981
  elif key == 'i':
970
982
  # Import style from .bpcfg (with numbered list)
@@ -980,13 +992,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
980
992
  print(f" {_i}: {_f}")
981
993
  inp = input("Enter number to open or filename (.bpcfg, q=cancel): ").strip()
982
994
  if not inp or inp.lower() == 'q':
983
- _print_menu(len(all_cycles)); continue
995
+ _print_menu(len(all_cycles), is_dqdv); continue
984
996
  if inp.isdigit() and _bpcfg_files:
985
997
  _idx = int(inp)
986
998
  if 1 <= _idx <= len(_bpcfg_files):
987
999
  path = os.path.join(os.getcwd(), _bpcfg_files[_idx-1])
988
1000
  else:
989
- print("Invalid number."); _print_menu(len(all_cycles)); continue
1001
+ print("Invalid number."); _print_menu(len(all_cycles), is_dqdv); continue
990
1002
  else:
991
1003
  path = inp
992
1004
  if not os.path.isfile(path):
@@ -996,14 +1008,14 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
996
1008
  if os.path.isfile(alt):
997
1009
  path = alt
998
1010
  else:
999
- print("File not found."); _print_menu(len(all_cycles)); continue
1011
+ print("File not found."); _print_menu(len(all_cycles), is_dqdv); continue
1000
1012
  else:
1001
- print("File not found."); _print_menu(len(all_cycles)); continue
1013
+ print("File not found."); _print_menu(len(all_cycles), is_dqdv); continue
1002
1014
  with open(path, 'r', encoding='utf-8') as f:
1003
1015
  cfg = json.load(f)
1004
1016
  if not isinstance(cfg, dict) or cfg.get('kind') != 'ec_style':
1005
1017
  print("Not an EC style file.")
1006
- _print_menu(len(all_cycles))
1018
+ _print_menu(len(all_cycles), is_dqdv)
1007
1019
  continue
1008
1020
 
1009
1021
  # --- Apply comprehensive style (no curve data) ---
@@ -1087,7 +1099,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1087
1099
 
1088
1100
  except Exception as e:
1089
1101
  print(f"Error importing style: {e}")
1090
- _print_menu(len(all_cycles))
1102
+ _print_menu(len(all_cycles), is_dqdv)
1091
1103
  continue
1092
1104
  elif key == 'l':
1093
1105
  # Line widths submenu: curves vs frame/ticks
@@ -1262,7 +1274,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1262
1274
  print("Unknown option.")
1263
1275
  except Exception as e:
1264
1276
  print(f"Error in line submenu: {e}")
1265
- _print_menu(len(all_cycles))
1277
+ _print_menu(len(all_cycles), is_dqdv)
1266
1278
  continue
1267
1279
  elif key == 'r':
1268
1280
  # Rename axis labels
@@ -1323,7 +1335,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1323
1335
  fig.canvas.draw_idle()
1324
1336
  except Exception as e:
1325
1337
  print(f"Error renaming axes: {e}")
1326
- _print_menu(len(all_cycles))
1338
+ _print_menu(len(all_cycles), is_dqdv)
1327
1339
  continue
1328
1340
  elif key == 't':
1329
1341
  # Unified WASD: w/a/s/d x 1..5 => spine, ticks, minor, labels, title
@@ -1442,7 +1454,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1442
1454
  fig.canvas.draw_idle()
1443
1455
  except Exception as e:
1444
1456
  print(f"Error in WASD tick visibility menu: {e}")
1445
- _print_menu(len(all_cycles))
1457
+ _print_menu(len(all_cycles), is_dqdv)
1446
1458
  continue
1447
1459
  elif key == 's':
1448
1460
  try:
@@ -1458,18 +1470,18 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1458
1470
  print(f" {i}: {f}")
1459
1471
  choice = input("Enter new filename or number to overwrite (q=cancel): ").strip()
1460
1472
  if not choice or choice.lower() == 'q':
1461
- _print_menu(len(all_cycles)); continue
1473
+ _print_menu(len(all_cycles), is_dqdv); continue
1462
1474
  if choice.isdigit() and files:
1463
1475
  idx = int(choice)
1464
1476
  if 1 <= idx <= len(files):
1465
1477
  name = files[idx-1]
1466
1478
  yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1467
1479
  if yn != 'y':
1468
- _print_menu(len(all_cycles)); continue
1480
+ _print_menu(len(all_cycles), is_dqdv); continue
1469
1481
  target = os.path.join(folder, name)
1470
1482
  else:
1471
1483
  print("Invalid number.")
1472
- _print_menu(len(all_cycles)); continue
1484
+ _print_menu(len(all_cycles), is_dqdv); continue
1473
1485
  else:
1474
1486
  name = choice
1475
1487
  root, ext = os.path.splitext(name)
@@ -1479,11 +1491,11 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1479
1491
  if os.path.exists(target):
1480
1492
  yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
1481
1493
  if yn != 'y':
1482
- _print_menu(len(all_cycles)); continue
1494
+ _print_menu(len(all_cycles), is_dqdv); continue
1483
1495
  dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
1484
1496
  except Exception as e:
1485
1497
  print(f"Save failed: {e}")
1486
- _print_menu(len(all_cycles))
1498
+ _print_menu(len(all_cycles), is_dqdv)
1487
1499
  continue
1488
1500
  elif key == 'c':
1489
1501
  print(f"Cycles present ({len(all_cycles)} total):", ", ".join(str(c) for c in all_cycles))
@@ -1559,9 +1571,14 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1559
1571
  if ignored:
1560
1572
  print("Ignored cycles:", ", ".join(str(c) for c in ignored))
1561
1573
  # Show the menu again after completing the command
1562
- _print_menu(len(all_cycles))
1574
+ _print_menu(len(all_cycles), is_dqdv)
1563
1575
  continue
1564
1576
  elif key == 'a':
1577
+ # X-axis submenu: number-of-ions vs capacity (not available in dQdV mode)
1578
+ if is_dqdv:
1579
+ print("Capacity/ion conversion is not available in dQ/dV mode.")
1580
+ _print_menu(len(all_cycles), is_dqdv)
1581
+ continue
1565
1582
  # X-axis submenu: number-of-ions vs capacity
1566
1583
  while True:
1567
1584
  print("X-axis menu: n=number of ions, c=capacity, q=back")
@@ -1625,7 +1642,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1625
1642
  fig.canvas.draw()
1626
1643
  except Exception:
1627
1644
  fig.canvas.draw_idle()
1628
- _print_menu(len(all_cycles))
1645
+ _print_menu(len(all_cycles), is_dqdv)
1629
1646
  continue
1630
1647
  elif key == 'f':
1631
1648
  # Font submenu with numbered options
@@ -1694,7 +1711,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1694
1711
  print("Size must be positive.")
1695
1712
  except Exception:
1696
1713
  print("Invalid size.")
1697
- _print_menu(len(all_cycles))
1714
+ _print_menu(len(all_cycles), is_dqdv)
1698
1715
  continue
1699
1716
  elif key == 'x':
1700
1717
  # X-axis: set limits only
@@ -1708,7 +1725,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1708
1725
  fig.canvas.draw()
1709
1726
  except Exception:
1710
1727
  print("Invalid limits, ignored.")
1711
- _print_menu(len(all_cycles))
1728
+ _print_menu(len(all_cycles), is_dqdv)
1712
1729
  continue
1713
1730
  elif key == 'y':
1714
1731
  # Y-axis: set limits only
@@ -1722,7 +1739,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1722
1739
  fig.canvas.draw()
1723
1740
  except Exception:
1724
1741
  print("Invalid limits, ignored.")
1725
- _print_menu(len(all_cycles))
1742
+ _print_menu(len(all_cycles), is_dqdv)
1726
1743
  continue
1727
1744
  elif key == 'g':
1728
1745
  # Geometry submenu: plot frame vs canvas (scales moved to separate keys)
@@ -1751,11 +1768,11 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1751
1768
  fig.canvas.draw()
1752
1769
  except Exception:
1753
1770
  fig.canvas.draw_idle()
1754
- _print_menu(len(all_cycles))
1771
+ _print_menu(len(all_cycles), is_dqdv)
1755
1772
  continue
1756
1773
  else:
1757
1774
  print("Unknown command.")
1758
- _print_menu(len(all_cycles))
1775
+ _print_menu(len(all_cycles), is_dqdv)
1759
1776
 
1760
1777
 
1761
1778
  def _get_style_snapshot(fig, ax, cycle_lines: Dict, tick_state: Dict) -> Dict:
@@ -1969,37 +1969,10 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1969
1969
  if sub == 'r' or sub == '':
1970
1970
  continue
1971
1971
  if sub == 'e':
1972
- choice = input("Enter new filename or number to overwrite (q=cancel): ").strip()
1973
- if not choice or choice.lower() == 'q':
1974
- print("Canceled.")
1975
- else:
1976
- target = None
1977
- if choice.isdigit() and _bpcfg_files:
1978
- _idx = int(choice)
1979
- if 1 <= _idx <= len(_bpcfg_files):
1980
- name = _bpcfg_files[_idx-1]
1981
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1982
- if yn == 'y':
1983
- target = os.path.join(os.getcwd(), name)
1984
- else:
1985
- print("Invalid number.")
1986
- else:
1987
- name = choice
1988
- root, ext = os.path.splitext(name)
1989
- if ext == '':
1990
- name = name + '.bpcfg'
1991
- target = name if os.path.isabs(name) else os.path.join(os.getcwd(), name)
1992
- if os.path.exists(target):
1993
- yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
1994
- if yn != 'y':
1995
- target = None
1996
- if target:
1997
- export_style_config(target)
1998
- print(f"Exported style to {target}")
1999
- style_menu_active = False # Exit style submenu and return to main menu
2000
- break
2001
- else:
2002
- print("Export canceled.")
1972
+ # Call export_style_config which handles the entire export dialog
1973
+ export_style_config(None) # The filename parameter is ignored by the function
1974
+ style_menu_active = False # Exit style submenu and return to main menu
1975
+ break
2003
1976
  else:
2004
1977
  print("Unknown choice.")
2005
1978
  except Exception as e:
@@ -111,20 +111,86 @@ def handle_cv_mode(args) -> int:
111
111
  ax.legend()
112
112
  fig.subplots_adjust(left=0.12, right=0.95, top=0.88, bottom=0.15)
113
113
 
114
+ # Save if requested
115
+ outname = args.savefig or args.out
116
+ if outname:
117
+ if not os.path.splitext(outname)[1]:
118
+ outname += '.svg'
119
+ _, _ext = os.path.splitext(outname)
120
+ if _ext.lower() == '.svg':
121
+ try:
122
+ _fig_fc = fig.get_facecolor()
123
+ except Exception:
124
+ _fig_fc = None
125
+ try:
126
+ _ax_fc = ax.get_facecolor()
127
+ except Exception:
128
+ _ax_fc = None
129
+ try:
130
+ if getattr(fig, 'patch', None) is not None:
131
+ fig.patch.set_alpha(0.0)
132
+ fig.patch.set_facecolor('none')
133
+ if getattr(ax, 'patch', None) is not None:
134
+ ax.patch.set_alpha(0.0)
135
+ ax.patch.set_facecolor('none')
136
+ except Exception:
137
+ pass
138
+ try:
139
+ fig.savefig(outname, dpi=300, transparent=True, facecolor='none', edgecolor='none')
140
+ finally:
141
+ try:
142
+ if _fig_fc is not None and getattr(fig, 'patch', None) is not None:
143
+ fig.patch.set_alpha(1.0)
144
+ fig.patch.set_facecolor(_fig_fc)
145
+ except Exception:
146
+ pass
147
+ try:
148
+ if _ax_fc is not None and getattr(ax, 'patch', None) is not None:
149
+ ax.patch.set_alpha(1.0)
150
+ ax.patch.set_facecolor(_ax_fc)
151
+ except Exception:
152
+ pass
153
+ else:
154
+ fig.savefig(outname, dpi=300)
155
+ print(f"CV plot saved to {outname}")
156
+
114
157
  # Interactive menu
115
158
  if args.interactive:
116
159
  try:
117
- plt.ion()
160
+ _backend = plt.get_backend()
118
161
  except Exception:
119
- pass
120
- plt.show(block=False)
121
- try:
122
- electrochem_interactive_menu(fig, ax, cycle_lines)
123
- except Exception as _ie:
124
- print(f"Interactive menu failed: {_ie}")
125
- plt.show()
162
+ _backend = "unknown"
163
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
164
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
165
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
166
+ if _is_noninteractive:
167
+ print(f"Matplotlib backend '{_backend}' is non-interactive; a window cannot be shown.")
168
+ print("Tips: unset MPLBACKEND or set a GUI backend")
169
+ print("Or run without --interactive and use --out to save the figure.")
170
+ else:
171
+ try:
172
+ plt.ion()
173
+ except Exception:
174
+ pass
175
+ plt.show(block=False)
176
+ try:
177
+ electrochem_interactive_menu(fig, ax, cycle_lines)
178
+ except Exception as _ie:
179
+ print(f"Interactive menu failed: {_ie}")
180
+ plt.show()
126
181
  else:
127
- plt.show()
182
+ if not (args.savefig or args.out):
183
+ try:
184
+ _backend = plt.get_backend()
185
+ except Exception:
186
+ _backend = "unknown"
187
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
188
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
189
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
190
+ if not _is_noninteractive:
191
+ plt.show()
192
+ else:
193
+ print(f"Matplotlib backend '{_backend}' is non-interactive; use --out to save the figure.")
128
194
  return 0
129
195
 
130
196
  except Exception as e:
@@ -333,7 +399,9 @@ def handle_gc_mode(args) -> int:
333
399
  _backend = plt.get_backend()
334
400
  except Exception:
335
401
  _backend = "unknown"
336
- _is_noninteractive = isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
402
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
403
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
404
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
337
405
  if _is_noninteractive:
338
406
  print(f"Matplotlib backend '{_backend}' is non-interactive; a window cannot be shown.")
339
407
  print("Tips: unset MPLBACKEND or set a GUI backend")
@@ -355,7 +423,10 @@ def handle_gc_mode(args) -> int:
355
423
  _backend = plt.get_backend()
356
424
  except Exception:
357
425
  _backend = "unknown"
358
- if not (isinstance(_backend, str) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})):
426
+ # TkAgg, QtAgg, Qt5Agg, WXAgg, MacOSX etc. are interactive
427
+ _interactive_backends = {"tkagg", "qt5agg", "qt4agg", "qtagg", "wxagg", "macosx", "gtk3agg", "gtk4agg", "wx", "qt", "gtk", "gtk3", "gtk4"}
428
+ _is_noninteractive = isinstance(_backend, str) and (_backend.lower() not in _interactive_backends) and ("agg" in _backend.lower() or _backend.lower() in {"pdf","ps","svg","template"})
429
+ if not _is_noninteractive:
359
430
  plt.show()
360
431
  else:
361
432
  print(f"Matplotlib backend '{_backend}' is non-interactive; use --out to save the figure.")
@@ -1011,6 +1011,7 @@ def dump_ec_session(
1011
1011
  'tick_widths': tick_widths,
1012
1012
  'spines': spines_state,
1013
1013
  'titles': titles,
1014
+ 'mode': getattr(ax, '_is_dqdv_mode', None), # Store dQdV mode flag
1014
1015
  }
1015
1016
  if skip_confirm:
1016
1017
  target = filename
@@ -1316,6 +1317,15 @@ def load_ec_session(filename: str):
1316
1317
  ax._right_ylabel_on = False
1317
1318
  except Exception:
1318
1319
  pass
1320
+
1321
+ # Restore mode flag (e.g., dQdV mode)
1322
+ try:
1323
+ mode = sess.get('mode')
1324
+ if mode is not None:
1325
+ ax._is_dqdv_mode = bool(mode)
1326
+ except Exception:
1327
+ pass
1328
+
1319
1329
  try:
1320
1330
  fig.canvas.draw()
1321
1331
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.1.2
3
+ Version: 1.1.4
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.2"
7
+ version = "1.1.4"
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" }
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