batplot 1.3.3__tar.gz → 1.3.5__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.3.3 → batplot-1.3.5}/PKG-INFO +10 -3
  2. {batplot-1.3.3 → batplot-1.3.5}/README.md +9 -2
  3. {batplot-1.3.3 → batplot-1.3.5}/batplot/args.py +17 -15
  4. {batplot-1.3.3 → batplot-1.3.5}/batplot/batch.py +5 -5
  5. {batplot-1.3.3 → batplot-1.3.5}/batplot/batplot.py +91 -56
  6. {batplot-1.3.3 → batplot-1.3.5}/batplot/cpc_interactive.py +52 -0
  7. {batplot-1.3.3 → batplot-1.3.5}/batplot/electrochem_interactive.py +138 -1
  8. {batplot-1.3.3 → batplot-1.3.5}/batplot/interactive.py +304 -36
  9. {batplot-1.3.3 → batplot-1.3.5}/batplot/operando.py +5 -14
  10. {batplot-1.3.3 → batplot-1.3.5}/batplot/operando_ec_interactive.py +503 -196
  11. {batplot-1.3.3 → batplot-1.3.5}/batplot/readers.py +187 -0
  12. {batplot-1.3.3 → batplot-1.3.5}/batplot/session.py +9 -1
  13. {batplot-1.3.3 → batplot-1.3.5}/batplot/style.py +80 -1
  14. {batplot-1.3.3 → batplot-1.3.5}/batplot.egg-info/PKG-INFO +10 -3
  15. {batplot-1.3.3 → batplot-1.3.5}/pyproject.toml +1 -1
  16. {batplot-1.3.3 → batplot-1.3.5}/LICENSE +0 -0
  17. {batplot-1.3.3 → batplot-1.3.5}/batplot/__init__.py +0 -0
  18. {batplot-1.3.3 → batplot-1.3.5}/batplot/batplot_new.py +0 -0
  19. {batplot-1.3.3 → batplot-1.3.5}/batplot/cif.py +0 -0
  20. {batplot-1.3.3 → batplot-1.3.5}/batplot/cli.py +0 -0
  21. {batplot-1.3.3 → batplot-1.3.5}/batplot/converters.py +0 -0
  22. {batplot-1.3.3 → batplot-1.3.5}/batplot/modes.py +0 -0
  23. {batplot-1.3.3 → batplot-1.3.5}/batplot/plotting.py +0 -0
  24. {batplot-1.3.3 → batplot-1.3.5}/batplot/ui.py +0 -0
  25. {batplot-1.3.3 → batplot-1.3.5}/batplot/utils.py +0 -0
  26. {batplot-1.3.3 → batplot-1.3.5}/batplot.egg-info/SOURCES.txt +0 -0
  27. {batplot-1.3.3 → batplot-1.3.5}/batplot.egg-info/dependency_links.txt +0 -0
  28. {batplot-1.3.3 → batplot-1.3.5}/batplot.egg-info/entry_points.txt +0 -0
  29. {batplot-1.3.3 → batplot-1.3.5}/batplot.egg-info/requires.txt +0 -0
  30. {batplot-1.3.3 → batplot-1.3.5}/batplot.egg-info/top_level.txt +0 -0
  31. {batplot-1.3.3 → batplot-1.3.5}/setup.cfg +0 -0
  32. {batplot-1.3.3 → batplot-1.3.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.3.3
3
+ Version: 1.3.5
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
@@ -91,8 +91,11 @@ batplot --all
91
91
  # Batch mode with options: custom axis and range
92
92
  batplot --all --xaxis 2theta --xrange 10 80
93
93
 
94
- # Batch mode: convert 2theta to Q and use raw intensity
95
- batplot --all --wl 1.5406 --raw
94
+ # Normalize data (--stack mode auto-normalizes by default)
95
+ batplot allfiles --norm
96
+
97
+ # Batch mode: convert 2theta to Q
98
+ batplot --all --wl 1.5406
96
99
  ```
97
100
 
98
101
  ### Electrochemistry
@@ -132,6 +135,10 @@ batplot --all geom.bpsg --cpc --mass 5.4 # Apply style+geometry to all CPC fi
132
135
  # Correlate in-situ XRD with electrochemistry
133
136
  # (Place both .xye and .mpt files in same directory)
134
137
  batplot --operando --interactive
138
+
139
+ # Operando mode without electrochemistry data
140
+ # (Only .xye files, no .mpt file)
141
+ batplot --operando --interactive
135
142
  ```
136
143
 
137
144
  ## Supported File Formats
@@ -42,8 +42,11 @@ batplot --all
42
42
  # Batch mode with options: custom axis and range
43
43
  batplot --all --xaxis 2theta --xrange 10 80
44
44
 
45
- # Batch mode: convert 2theta to Q and use raw intensity
46
- batplot --all --wl 1.5406 --raw
45
+ # Normalize data (--stack mode auto-normalizes by default)
46
+ batplot allfiles --norm
47
+
48
+ # Batch mode: convert 2theta to Q
49
+ batplot --all --wl 1.5406
47
50
  ```
48
51
 
49
52
  ### Electrochemistry
@@ -83,6 +86,10 @@ batplot --all geom.bpsg --cpc --mass 5.4 # Apply style+geometry to all CPC fi
83
86
  # Correlate in-situ XRD with electrochemistry
84
87
  # (Place both .xye and .mpt files in same directory)
85
88
  batplot --operando --interactive
89
+
90
+ # Operando mode without electrochemistry data
91
+ # (Only .xye files, no .mpt file)
92
+ batplot --operando --interactive
86
93
  ```
87
94
 
88
95
  ## Supported File Formats
@@ -12,7 +12,7 @@ def _print_general_help() -> None:
12
12
  "What it does:\n"
13
13
  " • XY: XRD/PDF/XAS/... curves\n"
14
14
  " • EC: GC/CPC/dQdV/CV (from .csv or .mpt)\n"
15
- " • Operando: contour maps from a folder of XY and .mpt files (use --raw for raw intensity)\n"
15
+ " • Operando: contour maps from a folder of XY and .mpt files\n"
16
16
  " • Batch: export SVG plots for all files in a directory\n\n"
17
17
  " • Interactive mode: --interactive flag opens a menu for styling, ranges, fonts, export, sessions\n\n"
18
18
  "How to run (basics):\n"
@@ -34,8 +34,7 @@ def _print_general_help() -> None:
34
34
  " batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
35
35
  " batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
36
36
  " [Operando]\n"
37
- " batplot --operando [FOLDER] # Operando contour from folder (normalized by default)\n"
38
- " batplot --operando --raw # Operando contour with raw intensity (no normalization)\n\n"
37
+ " batplot --operando [FOLDER] # Operando contour (with or without .mpt file)\n\n"
39
38
  "Features:\n"
40
39
  " • Quick plotting with sensible defaults, no config files needed\n"
41
40
  " • Supports many common file formats (see -h xy/ec/op)\n"
@@ -58,8 +57,9 @@ def _print_xy_help() -> None:
58
57
  msg = (
59
58
  "XY plots (diffraction/PDF/XAS)\n\n"
60
59
  "Supported files: .xye .xy .qye .dat .csv .gr .nor .chik .chir .txt (2-col). CIF overlays supported.\n\n"
61
- "Axis detection: .qye→Q, .gr→r, .nor→energy, .chik→k, .chir→r, else use --xaxis (Q, 2theta, r, k, energy, rft).\n"
62
- "If mixing 2θ data in Q, give wavelength per-file (file.xye:1.5406) or global --wl.\n\n"
60
+ "Axis detection: .qye→Q, .gr→r, .nor→energy, .chik→k, .chir→r, else use --xaxis (Q, 2theta, r, k, energy, rft, time).\n"
61
+ "If mixing 2θ data in Q, give wavelength per-file (file.xye:1.5406) or global --wl.\n"
62
+ "For electrochemistry CSV/MPT time-voltage plots, use --xaxis time.\n\n"
63
63
  "Examples:\n"
64
64
  " batplot a.xye:1.5406 b.qye --stack --interactive\n"
65
65
  " batplot a.dat b.xy --wl 1.54 --out fig.svg\n"
@@ -73,18 +73,19 @@ def _print_xy_help() -> None:
73
73
  " batplot --all # Export all XY files to SVG\n"
74
74
  " batplot --all --xaxis 2theta # Batch mode with custom axis type\n"
75
75
  " batplot --all --xrange 10 80 # Batch mode with X-axis range\n"
76
- " batplot --all --wl 1.5406 --raw # Batch mode with wavelength and raw intensity\n\n"
76
+ " batplot --all --wl 1.5406 # Batch mode with wavelength conversion\n\n"
77
77
  "Tips and options:\n"
78
78
  "[XY plot]\n"
79
79
  " --interactive : open interactive menu for styling, ranges, fonts, export, sessions\n"
80
80
  " --delta/-d <float> : spacing between curves, e.g. --delta 0.1\n"
81
- " --raw : plot raw intensity (normalized by default)\n"
81
+ " --norm : normalize intensity to 0-1 range. Stack mode (--stack) auto-normalizes\n"
82
82
  " --xrange/-r <min> <max> : set x-axis range, e.g. --xrange 0 10\n"
83
83
  " --out/-o <filename> : save figure to file, e.g. --out file.svg\n"
84
- " --xaxis <type> : set x-axis type (Q, 2theta, r, k, energy, rft, or user defined), e.g. --xaxis 2theta\n"
84
+ " --xaxis <type> : set x-axis type (Q, 2theta, r, k, energy, rft, time, or user defined)\n"
85
+ " e.g. --xaxis 2theta, or --xaxis time for electrochemistry CSV/MPT time-voltage plots\n"
85
86
  " --wl <float> : set wavelength for Q conversion for all files, e.g. --wl 1.5406\n"
86
87
  " --fullprof <args> : FullProf overlay options\n"
87
- " --stack : stack curves vertically\n"
88
+ " --stack : stack curves vertically (auto-enables normalization)\n"
88
89
  )
89
90
  print(msg)
90
91
 
@@ -130,14 +131,15 @@ def _print_op_help() -> None:
130
131
  "Operando contour plots\n\n"
131
132
  "Example usage:\n"
132
133
  " batplot --operando --interactive --wl 0.25995 # Interactive mode with Q conversion\n"
133
- " batplot --operando --raw --interactive # Raw intensity (no normalization)\n"
134
134
  " batplot --operando --xaxis 2theta # Using 2theta axis\n\n"
135
135
  " • Folder should contain XY files (.xy/.xye/.qye/.dat).\n"
136
- " • By default, data is normalized (0-1). Use --raw to plot raw intensity.\n"
136
+ " • Intensity scale is auto-adjusted between min/max values.\n"
137
137
  " • If no .qye present, provide --xaxis 2theta or set --wl for Q conversion.\n"
138
- " • If a BioLogic .mpt is present, an EC side panel will be added automatically.\n\n"
139
- "Interactive (--interactive): resize axes/canvas, change colormap, set intensity range (oi),\n"
140
- "EC y-axis options (time ions), geometry tweaks, print/export/import style, save session.\n"
138
+ " • If a .mpt file is present, an EC side panel is added for dual-panel mode.\n"
139
+ " Without a .mpt file, operando-only mode shows the contour plot alone.\n\n"
140
+ "Interactive (--interactive): resize axes/canvas, change colormap, set intensity range (oz),\n"
141
+ "EC y-axis options (time ↔ ions), geometry tweaks, toggle spines/ticks/labels,\n"
142
+ "print/export/import style, save session.\n"
141
143
  )
142
144
  print(msg)
143
145
 
@@ -158,7 +160,7 @@ def build_parser() -> argparse.ArgumentParser:
158
160
  parser.add_argument("--convert", "-c", nargs="+", help=argparse.SUPPRESS)
159
161
  parser.add_argument("--wl", type=float, help=argparse.SUPPRESS)
160
162
  parser.add_argument("--fullprof", nargs="+", type=float, help=argparse.SUPPRESS)
161
- parser.add_argument("--raw", action="store_true", help=argparse.SUPPRESS)
163
+ parser.add_argument("--norm", action="store_true", help=argparse.SUPPRESS)
162
164
  parser.add_argument("--interactive", action="store_true", help=argparse.SUPPRESS)
163
165
  parser.add_argument("--savefig", type=str, help=argparse.SUPPRESS)
164
166
  parser.add_argument("--stack", action="store_true", help=argparse.SUPPRESS)
@@ -278,16 +278,16 @@ def batch_process(directory: str, args):
278
278
  else:
279
279
  x_plot = x
280
280
 
281
- # Normalize or raw
282
- if args.raw:
283
- y_plot = y.copy()
284
- else:
281
+ # Normalize if --norm flag is set
282
+ if getattr(args, 'norm', False):
285
283
  if y.size:
286
284
  ymin = float(y.min()); ymax = float(y.max())
287
285
  span = ymax - ymin
288
286
  y_plot = (y - ymin)/span if span > 0 else np.zeros_like(y)
289
287
  else:
290
288
  y_plot = y
289
+ else:
290
+ y_plot = y.copy()
291
291
 
292
292
  # Plot and save
293
293
  fig_b, ax_b = plt.subplots(figsize=(6,4))
@@ -309,7 +309,7 @@ def batch_process(directory: str, args):
309
309
  ax_b.set_xlabel("Radial distance (Å)")
310
310
  else:
311
311
  ax_b.set_xlabel(r"$2\theta\ (\mathrm{deg})$")
312
- ax_b.set_ylabel("Intensity" if args.raw else "Normalized intensity (a.u.)")
312
+ ax_b.set_ylabel("Normalized intensity (a.u.)" if getattr(args, 'norm', False) else "Intensity")
313
313
  ax_b.set_title(fname)
314
314
  fig_b.subplots_adjust(left=0.18, right=0.97, bottom=0.16, top=0.90)
315
315
  out_name = os.path.splitext(fname)[0] + ".svg"
@@ -28,6 +28,8 @@ from .readers import (
28
28
  read_mpt_file,
29
29
  read_ec_csv_file,
30
30
  read_ec_csv_dqdv_file,
31
+ read_csv_time_voltage,
32
+ read_mpt_time_voltage,
31
33
  )
32
34
  from .cif import (
33
35
  simulate_cif_pattern_Q,
@@ -1090,11 +1092,12 @@ def batplot_main() -> int:
1090
1092
  except Exception:
1091
1093
  pass
1092
1094
  try:
1093
- if has_ec and (operando_ec_interactive_menu is not None) and (ec_ax is not None):
1095
+ # Call interactive menu regardless of EC presence
1096
+ # When ec_ax is None, EC-related commands will be disabled
1097
+ if operando_ec_interactive_menu is not None:
1094
1098
  operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax)
1095
1099
  else:
1096
- # Operando-only interactive menu has been removed; fall back to non-interactive view
1097
- 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.")
1100
+ print("Interactive menu not available.")
1098
1101
  except Exception as _ie:
1099
1102
  print(f"Interactive menu failed: {_ie}")
1100
1103
  _plt.show()
@@ -1236,7 +1239,7 @@ def batplot_main() -> int:
1236
1239
  except Exception:
1237
1240
  pass
1238
1241
  try:
1239
- if operando_ec_interactive_menu is not None and ec_ax2 is not None:
1242
+ if operando_ec_interactive_menu is not None:
1240
1243
  operando_ec_interactive_menu(fig2, ax2, im2, cbar2, ec_ax2)
1241
1244
  except Exception as _ie:
1242
1245
  print(f"Interactive menu failed: {_ie}")
@@ -1641,8 +1644,8 @@ def batplot_main() -> int:
1641
1644
  args_subset = sess.get('args_subset', {})
1642
1645
  if 'autoscale' in args_subset:
1643
1646
  args.autoscale = bool(args_subset['autoscale'])
1644
- if 'raw' in args_subset:
1645
- args.raw = bool(args_subset['raw'])
1647
+ if 'norm' in args_subset:
1648
+ args.norm = bool(args_subset['norm'])
1646
1649
  except Exception:
1647
1650
  pass
1648
1651
  try:
@@ -1701,49 +1704,59 @@ def batplot_main() -> int:
1701
1704
  # ---------------- Determine X-axis type ----------------
1702
1705
  def _ext_token(path):
1703
1706
  return os.path.splitext(path)[1].lower() # includes leading dot
1704
- any_qye = any(f.lower().endswith(".qye") for f in args.files)
1705
- any_gr = any(f.lower().endswith(".gr") for f in args.files)
1706
- any_nor = any(f.lower().endswith(".nor") for f in args.files)
1707
- any_chik = any("chik" in _ext_token(f) for f in args.files)
1708
- any_chir = any("chir" in _ext_token(f) for f in args.files)
1709
- any_txt = any(f.lower().endswith(".txt") for f in args.files)
1710
- any_cif = any(f.lower().endswith(".cif") for f in args.files)
1711
- non_cif_count = sum(0 if f.lower().endswith('.cif') else 1 for f in args.files)
1712
- cif_only = any_cif and non_cif_count == 0
1713
- any_lambda = any(":" in f for f in args.files) or args.wl is not None
1714
-
1715
- # Incompatibilities (no mixing of fundamentally different axis domains)
1716
- if sum(bool(x) for x in (any_gr, any_nor, any_chik, any_chir, (any_qye or any_lambda or any_cif))) > 1:
1717
- raise ValueError("Cannot mix .gr (r), .nor (energy), .chik (k), .chir (FT-EXAFS R), and Q/2θ/CIF data together. Split runs.")
1718
-
1719
- # Automatic axis selection based on file extensions
1720
- if any_qye:
1721
- axis_mode = "Q"
1722
- elif any_gr:
1723
- axis_mode = "r"
1724
- elif any_nor:
1725
- axis_mode = "energy"
1726
- elif any_chik:
1727
- axis_mode = "k"
1728
- elif any_chir:
1729
- axis_mode = "rft"
1730
- elif any_txt:
1731
- # .txt is generic, require --xaxis
1732
- if args.xaxis:
1707
+
1708
+ # Check for CSV/MPT files with --xaxis time
1709
+ any_csv = any(f.lower().endswith((".csv", ".mpt")) for f in args.files)
1710
+ use_time_mode = any_csv and args.xaxis and args.xaxis.lower() == "time"
1711
+
1712
+ if use_time_mode:
1713
+ # Special mode: plot time (h) vs voltage (V) for electrochemistry CSV/MPT files
1714
+ axis_mode = "time"
1715
+ else:
1716
+ # Regular XRD/PDF/XAS mode - proceed with normal detection
1717
+ any_qye = any(f.lower().endswith(".qye") for f in args.files)
1718
+ any_gr = any(f.lower().endswith(".gr") for f in args.files)
1719
+ any_nor = any(f.lower().endswith(".nor") for f in args.files)
1720
+ any_chik = any("chik" in _ext_token(f) for f in args.files)
1721
+ any_chir = any("chir" in _ext_token(f) for f in args.files)
1722
+ any_txt = any(f.lower().endswith(".txt") for f in args.files)
1723
+ any_cif = any(f.lower().endswith(".cif") for f in args.files)
1724
+ non_cif_count = sum(0 if f.lower().endswith('.cif') else 1 for f in args.files)
1725
+ cif_only = any_cif and non_cif_count == 0
1726
+ any_lambda = any(":" in f for f in args.files) or args.wl is not None
1727
+
1728
+ # Incompatibilities (no mixing of fundamentally different axis domains)
1729
+ if sum(bool(x) for x in (any_gr, any_nor, any_chik, any_chir, (any_qye or any_lambda or any_cif))) > 1:
1730
+ raise ValueError("Cannot mix .gr (r), .nor (energy), .chik (k), .chir (FT-EXAFS R), and Q/2θ/CIF data together. Split runs.")
1731
+
1732
+ # Automatic axis selection based on file extensions
1733
+ if any_qye:
1734
+ axis_mode = "Q"
1735
+ elif any_gr:
1736
+ axis_mode = "r"
1737
+ elif any_nor:
1738
+ axis_mode = "energy"
1739
+ elif any_chik:
1740
+ axis_mode = "k"
1741
+ elif any_chir:
1742
+ axis_mode = "rft"
1743
+ elif any_txt:
1744
+ # .txt is generic, require --xaxis
1745
+ if args.xaxis:
1746
+ axis_mode = args.xaxis
1747
+ else:
1748
+ raise ValueError("Cannot determine X-axis type for .txt files. Please specify --xaxis (Q, 2theta, r, k, energy, rft, or 'user defined').")
1749
+ elif any_lambda or any_cif:
1750
+ if args.xaxis and args.xaxis.lower() in ("2theta","two_theta","tth"):
1751
+ axis_mode = "2theta"
1752
+ else:
1753
+ # If wavelength is provided, user wants to convert to Q
1754
+ # CIF files are in Q space
1755
+ axis_mode = "Q"
1756
+ elif args.xaxis:
1733
1757
  axis_mode = args.xaxis
1734
1758
  else:
1735
- raise ValueError("Cannot determine X-axis type for .txt files. Please specify --xaxis (Q, 2theta, r, k, energy, rft, or 'user defined').")
1736
- elif any_lambda or any_cif:
1737
- if args.xaxis and args.xaxis.lower() in ("2theta","two_theta","tth"):
1738
- axis_mode = "2theta"
1739
- else:
1740
- # If wavelength is provided, user wants to convert to Q
1741
- # CIF files are in Q space
1742
- axis_mode = "Q"
1743
- elif args.xaxis:
1744
- axis_mode = args.xaxis
1745
- else:
1746
- raise ValueError("Cannot determine X-axis type (need .qye / .gr / .nor / .chik / .chir / .cif / wavelength / --xaxis). For .txt or unknown file types, use --xaxis Q, 2theta, r, k, energy, rft, or 'user defined'.")
1759
+ raise ValueError("Cannot determine X-axis type (need .qye / .gr / .nor / .chik / .chir / .cif / wavelength / --xaxis). For .txt or unknown file types, use --xaxis Q, 2theta, r, k, energy, rft, or 'user defined'.")
1747
1760
 
1748
1761
  use_Q = axis_mode == "Q"
1749
1762
  use_2th = axis_mode == "2theta"
@@ -1751,6 +1764,7 @@ def batplot_main() -> int:
1751
1764
  use_E = axis_mode == "energy"
1752
1765
  use_k = axis_mode == "k" # NEW
1753
1766
  use_rft = axis_mode == "rft" # NEW
1767
+ use_time = axis_mode == "time" # NEW: electrochemistry time mode
1754
1768
 
1755
1769
  # Validate: if using 2theta mode with CIF files, wavelength is required
1756
1770
  if use_2th and any_cif and not wavelength_file:
@@ -1816,8 +1830,19 @@ def batplot_main() -> int:
1816
1830
  if wavelength_file and not use_r and not use_E and file_ext not in (".gr", ".nor", ".cif"):
1817
1831
  label += f" (λ={wavelength_file:.5f} Å)"
1818
1832
 
1819
- # ---- Read data (added .nor branch) ----
1820
- if is_cif:
1833
+ # ---- Read data (time mode for CSV/MPT or regular mode) ----
1834
+ if use_time and file_ext in ('.csv', '.mpt'):
1835
+ # Time mode: read time (h) vs voltage (V) for electrochemistry files
1836
+ try:
1837
+ if file_ext == '.csv':
1838
+ x, y = read_csv_time_voltage(fname)
1839
+ elif file_ext == '.mpt':
1840
+ x, y = read_mpt_time_voltage(fname)
1841
+ e = None
1842
+ except Exception as e_read:
1843
+ print(f"Error reading {fname} in time mode: {e_read}")
1844
+ continue
1845
+ elif is_cif:
1821
1846
  try:
1822
1847
  # Simulate pattern directly in Q space regardless of current axis_mode
1823
1848
  Q_sim, I_sim = simulate_cif_pattern_Q(fname)
@@ -1889,15 +1914,18 @@ def batplot_main() -> int:
1889
1914
  args._warned_extensions.add(file_ext)
1890
1915
  print(f"Note: Reading '{file_ext}' file as 2-column (x, y) data. Use --xaxis to specify x-axis type if needed.")
1891
1916
 
1892
- # ---- X-axis conversion logic updated (no conversion for energy) ----
1893
- if use_Q and file_ext not in (".qye", ".gr", ".nor"):
1917
+ # ---- X-axis conversion logic updated (no conversion for energy or time) ----
1918
+ if use_time:
1919
+ # Time mode: data already in hours, no conversion needed
1920
+ x_plot = x
1921
+ elif use_Q and file_ext not in (".qye", ".gr", ".nor"):
1894
1922
  if wavelength_file:
1895
1923
  theta_rad = np.radians(x/2)
1896
1924
  x_plot = 4*np.pi*np.sin(theta_rad)/wavelength_file
1897
1925
  else:
1898
1926
  x_plot = x
1899
1927
  else:
1900
- # r, energy, or already Q: direct
1928
+ # r, energy, k, rft, or already Q: direct
1901
1929
  x_plot = x
1902
1930
 
1903
1931
  # ---- Store full (converted) arrays BEFORE cropping ----
@@ -1920,7 +1948,9 @@ def batplot_main() -> int:
1920
1948
  x_plot = x_full
1921
1949
 
1922
1950
  # ---- Normalize (display subset) ----
1923
- if not args.raw:
1951
+ # Auto-normalize for --stack mode, or explicit --norm flag
1952
+ should_normalize = args.stack or getattr(args, 'norm', False)
1953
+ if should_normalize:
1924
1954
  # Min–max normalization to 0..1 within the currently displayed (cropped) segment
1925
1955
  if y_plot.size:
1926
1956
  y_min = float(y_plot.min())
@@ -2295,15 +2325,20 @@ def batplot_main() -> int:
2295
2325
  elif use_rft: x_label = "Radial distance (Å)"
2296
2326
  elif use_Q: x_label = r"Q ($\mathrm{\AA}^{-1}$)"
2297
2327
  elif use_2th: x_label = r"$2\theta$ (deg)"
2328
+ elif use_time: x_label = "Time (h)"
2298
2329
  elif args.xaxis:
2299
2330
  x_label = str(args.xaxis)
2300
2331
  else:
2301
2332
  x_label = "X"
2302
2333
  ax.set_xlabel(x_label, fontsize=16)
2303
- if args.raw:
2304
- ax.set_ylabel("Intensity", fontsize=16)
2305
- else:
2334
+ # Y-axis label: normalized if --stack or --norm, or voltage for time mode
2335
+ should_normalize = args.stack or getattr(args, 'norm', False)
2336
+ if use_time:
2337
+ ax.set_ylabel("Voltage (V)", fontsize=16)
2338
+ elif should_normalize:
2306
2339
  ax.set_ylabel("Normalized intensity (a.u.)", fontsize=16)
2340
+ else:
2341
+ ax.set_ylabel("Intensity", fontsize=16)
2307
2342
 
2308
2343
  # Store originals for axis-title toggle restoration (t menu bn/ln)
2309
2344
  try:
@@ -59,6 +59,7 @@ def _print_menu():
59
59
  " l: line",
60
60
  " m: marker sizes",
61
61
  " c: colors",
62
+ " k: spine colors",
62
63
  "ry: show/hide efficiency",
63
64
  " t: toggle axes",
64
65
  " h: legend",
@@ -1133,6 +1134,57 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1133
1134
  if is_multi_file:
1134
1135
  _print_file_list(file_data, current_file_idx)
1135
1136
  continue
1137
+ elif key == 'k':
1138
+ # Spine colors (w=top, a=left, s=bottom, d=right)
1139
+ try:
1140
+ print("Set spine colors (with matching tick and label colors):")
1141
+ print(" w : top spine | a : left spine")
1142
+ print(" s : bottom spine | d : right spine")
1143
+ print("Example: w:red a:#4561F7 s:blue d:green")
1144
+ line = input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
1145
+ if line and line.lower() != 'q':
1146
+ push_state("color-spine")
1147
+ # Map wasd to spine names
1148
+ key_to_spine = {'w': 'top', 'a': 'left', 's': 'bottom', 'd': 'right'}
1149
+ tokens = line.split()
1150
+ for token in tokens:
1151
+ if ':' not in token:
1152
+ print(f"Skip malformed token: {token}")
1153
+ continue
1154
+ key_part, color = token.split(':', 1)
1155
+ key_part = key_part.lower()
1156
+ if key_part not in key_to_spine:
1157
+ print(f"Unknown key: {key_part} (use w/a/s/d)")
1158
+ continue
1159
+ spine_name = key_to_spine[key_part]
1160
+ # Set for both axes
1161
+ for curr_ax in [ax, ax2]:
1162
+ if curr_ax is None:
1163
+ continue
1164
+ if spine_name not in curr_ax.spines:
1165
+ continue
1166
+ try:
1167
+ # Set spine color
1168
+ curr_ax.spines[spine_name].set_edgecolor(color)
1169
+ # Set tick colors and axis label color for this axis
1170
+ if spine_name in ('top', 'bottom'):
1171
+ curr_ax.tick_params(axis='x', which='both', colors=color)
1172
+ curr_ax.xaxis.label.set_color(color)
1173
+ else: # left or right
1174
+ curr_ax.tick_params(axis='y', which='both', colors=color)
1175
+ curr_ax.yaxis.label.set_color(color)
1176
+ except Exception as e:
1177
+ print(f"Error setting {spine_name} color: {e}")
1178
+ print(f"Set {spine_name} spine to {color}")
1179
+ fig.canvas.draw()
1180
+ else:
1181
+ print("Canceled.")
1182
+ except Exception as e:
1183
+ print(f"Error in spine color menu: {e}")
1184
+ _print_menu()
1185
+ if is_multi_file:
1186
+ _print_file_list(file_data, current_file_idx)
1187
+ continue
1136
1188
  elif key == 'e':
1137
1189
  try:
1138
1190
  fname = input("Export filename (default .svg if no extension, q=cancel): ").strip()