batplot 1.7.20__tar.gz → 1.7.22__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.

Potentially problematic release.


This version of batplot might be problematic. Click here for more details.

Files changed (60) hide show
  1. {batplot-1.7.20/batplot.egg-info → batplot-1.7.22}/PKG-INFO +1 -1
  2. {batplot-1.7.20 → batplot-1.7.22}/batplot/__init__.py +1 -1
  3. {batplot-1.7.20 → batplot-1.7.22}/batplot/args.py +69 -68
  4. {batplot-1.7.20 → batplot-1.7.22}/batplot/batplot.py +2 -3
  5. {batplot-1.7.20 → batplot-1.7.22}/batplot/cpc_interactive.py +71 -8
  6. {batplot-1.7.20 → batplot-1.7.22}/batplot/electrochem_interactive.py +181 -23
  7. {batplot-1.7.20 → batplot-1.7.22}/batplot/operando.py +3 -15
  8. {batplot-1.7.20 → batplot-1.7.22}/batplot/operando_ec_interactive.py +47 -41
  9. {batplot-1.7.20 → batplot-1.7.22/batplot.egg-info}/PKG-INFO +1 -1
  10. {batplot-1.7.20 → batplot-1.7.22}/pyproject.toml +1 -1
  11. {batplot-1.7.20 → batplot-1.7.22}/LICENSE +0 -0
  12. {batplot-1.7.20 → batplot-1.7.22}/MANIFEST.in +0 -0
  13. {batplot-1.7.20 → batplot-1.7.22}/README.md +0 -0
  14. {batplot-1.7.20 → batplot-1.7.22}/USER_MANUAL.md +0 -0
  15. {batplot-1.7.20 → batplot-1.7.22}/batplot/batch.py +0 -0
  16. {batplot-1.7.20 → batplot-1.7.22}/batplot/cif.py +0 -0
  17. {batplot-1.7.20 → batplot-1.7.22}/batplot/cli.py +0 -0
  18. {batplot-1.7.20 → batplot-1.7.22}/batplot/color_utils.py +0 -0
  19. {batplot-1.7.20 → batplot-1.7.22}/batplot/config.py +0 -0
  20. {batplot-1.7.20 → batplot-1.7.22}/batplot/converters.py +0 -0
  21. {batplot-1.7.20 → batplot-1.7.22}/batplot/data/USER_MANUAL.md +0 -0
  22. {batplot-1.7.20 → batplot-1.7.22}/batplot/interactive.py +0 -0
  23. {batplot-1.7.20 → batplot-1.7.22}/batplot/manual.py +0 -0
  24. {batplot-1.7.20 → batplot-1.7.22}/batplot/modes.py +0 -0
  25. {batplot-1.7.20 → batplot-1.7.22}/batplot/plotting.py +0 -0
  26. {batplot-1.7.20 → batplot-1.7.22}/batplot/readers.py +0 -0
  27. {batplot-1.7.20 → batplot-1.7.22}/batplot/session.py +0 -0
  28. {batplot-1.7.20 → batplot-1.7.22}/batplot/style.py +0 -0
  29. {batplot-1.7.20 → batplot-1.7.22}/batplot/ui.py +0 -0
  30. {batplot-1.7.20 → batplot-1.7.22}/batplot/utils.py +0 -0
  31. {batplot-1.7.20 → batplot-1.7.22}/batplot/version_check.py +0 -0
  32. {batplot-1.7.20 → batplot-1.7.22}/batplot.egg-info/SOURCES.txt +0 -0
  33. {batplot-1.7.20 → batplot-1.7.22}/batplot.egg-info/dependency_links.txt +0 -0
  34. {batplot-1.7.20 → batplot-1.7.22}/batplot.egg-info/entry_points.txt +0 -0
  35. {batplot-1.7.20 → batplot-1.7.22}/batplot.egg-info/requires.txt +0 -0
  36. {batplot-1.7.20 → batplot-1.7.22}/batplot.egg-info/top_level.txt +0 -0
  37. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/__init__.py +0 -0
  38. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/args.py +0 -0
  39. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/batch.py +0 -0
  40. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/batplot.py +0 -0
  41. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/cif.py +0 -0
  42. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/cli.py +0 -0
  43. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/color_utils.py +0 -0
  44. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/config.py +0 -0
  45. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/converters.py +0 -0
  46. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
  47. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
  48. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/interactive.py +0 -0
  49. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/modes.py +0 -0
  50. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/operando.py +0 -0
  51. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
  52. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/plotting.py +0 -0
  53. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/readers.py +0 -0
  54. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/session.py +0 -0
  55. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/style.py +0 -0
  56. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/ui.py +0 -0
  57. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/utils.py +0 -0
  58. {batplot-1.7.20 → batplot-1.7.22}/batplot_backup_20251121_223043/version_check.py +0 -0
  59. {batplot-1.7.20 → batplot-1.7.22}/setup.cfg +0 -0
  60. {batplot-1.7.20 → batplot-1.7.22}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.7.20
3
+ Version: 1.7.22
4
4
  Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.7.20"
3
+ __version__ = "1.7.22"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -9,7 +9,7 @@ the command-line interface, including:
9
9
 
10
10
  HOW COMMAND-LINE ARGUMENTS WORK:
11
11
  --------------------------------
12
- When you run 'batplot file.xy --interactive', Python's argparse library:
12
+ When you run (for example) 'batplot --xaxis 2theta file.xy --i', Python's argparse library:
13
13
  1. Parses the command line into structured arguments
14
14
  2. Validates that required arguments are present
15
15
  3. Converts string arguments to appropriate types (int, float, bool, etc.)
@@ -26,7 +26,7 @@ import sys
26
26
  import re
27
27
 
28
28
  # ====================================================================
29
- # COLORED HELP OUTPUT (OPTIONAL ENHANCEMENT)
29
+ # COLORED HELP OUTPUT
30
30
  # ====================================================================
31
31
  # The 'rich' library provides colored terminal output. If available,
32
32
  # we use it to make help text more readable by highlighting:
@@ -64,8 +64,8 @@ def _colorize_help(text: str) -> str:
64
64
  - Bullet points: • → bold
65
65
 
66
66
  Example:
67
- Input: "batplot file.xy --interactive"
68
- Output: "[green]batplot[/green] [yellow]file.xy[/yellow] [cyan]--interactive[/cyan]"
67
+ Input: "batplot file.qye --i"
68
+ Output: "[green]batplot[/green] [yellow]file.qye[/yellow] [cyan]--i[/cyan]"
69
69
 
70
70
  Args:
71
71
  text: Plain help text (uncolored)
@@ -133,78 +133,79 @@ def _print_general_help() -> None:
133
133
  msg = (
134
134
  version_str +
135
135
  "What it does:\n"
136
- " • XY: XRD/PDF/XAS/... curves\n"
137
- " • EC: GC/CPC/dQdV/CV (from .csv or .mpt)\n"
138
- " • Operando: contour maps from a folder of XY and .mpt files\n"
139
- " • Batch: export SVG plots for all files in a directory\n\n"
140
- " • Interactive mode: --i / --interactive flag opens a menu for styling, ranges, fonts, export, sessions\n\n"
141
- "How to run (basics):\n"
142
- " [XY curves]\n"
143
- " batplot file1.xy file2.qye [option1] [option2] # XY curves\n"
144
- " batplot allfiles # Plot all XY files in current directory on same figure\n"
145
- " batplot allfiles /path/to/dir # Plot all XY files in specified directory\n"
146
- " batplot allfiles --interactive # Plot all files with interactive menu\n"
147
- " batplot allxyfiles # Plot only .xy files (natural sorted)\n"
148
- " batplot /path/to/data allnorfiles --i # Plot only .nor files from a directory\n"
149
- " batplot --all # Batch mode: all XY files → Figures/ as .svg\n"
150
- " batplot --all --format png # Batch mode: export as .png files\n"
151
- " batplot --all --xaxis 2theta --xrange 10 80 # Batch mode with custom axis and range\n"
152
- " batplot --all style.bps # Batch with style: apply style.bps to all XY files\n"
153
- " batplot --all ./Style/style.bps # Batch with style: use relative path to style file\n"
154
- " batplot --all config.bpsg # Batch with style+geom: apply to all XY files\n"
155
- " batplot file1.xy file2.xye style.bps # Apply style to multiple files and export\n"
156
- " batplot file1.xy file2.xye ./Style/style.bps # Apply style from relative path\n\n"
157
- " [Electrochemistry]\n"
158
- " batplot --gc FILE.mpt --mass 7.0 # EC GC from .mpt (requires --mass mg)\n"
159
- " batplot --gc FILE.csv # EC GC from supported .csv (no mass required)\n"
160
- " batplot --gc --all --mass 7.0 # Batch: all .mpt/.csv → Figures/ as .svg\n"
161
- " batplot --gc --all --mass 7 --format pdf # Batch: export as .pdf files\n"
162
- " batplot --all --gc style.bps --mass 7 # Batch with style: apply style.bps to all GC files\n"
163
- " batplot --all --gc ./Style/style.bps --mass 7 # Batch with style: use relative path\n"
164
- " batplot --all --cv config.bpsg # Batch with style+geom: apply to all CV files\n"
165
- " batplot --all --cv ./Style/config.bpsg # Batch with style+geom: use relative path\n"
166
- " batplot --dqdv FILE.csv # EC dQ/dV from supported .csv\n"
167
- " batplot --dqdv --all # Batch: all .csv in directory (dQdV mode)\n"
168
- " batplot --cv FILE.mpt # EC CV (cyclic voltammetry) from .mpt\n"
169
- " batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
170
- " batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
171
- " [Operando]\n"
172
- " batplot --operando [FOLDER] # Operando contour (with or without .mpt file)\n\n"
173
- "Features:\n"
174
- " • Quick plotting with sensible defaults, no config files needed\n"
175
- " • Supports many common file formats (see -h xy/ec/op)\n"
176
- " • Interactive menus (--interactive): styling, ranges, fonts, export, sessions\n"
177
- " • Batch processing: use 'allfiles' / 'all<ext>files' to plot together, or --all for separate files\n"
178
- " • Batch exports saved to Figures/ subdirectory (default: .svg format)\n"
179
- " • Batch styling: apply .bps/.bpsg files to all exports (use --all flag)\n"
180
- " • Format option: use --format png/pdf/jpg/etc to change export format\n\n"
181
-
182
- "More help:\n"
183
- " batplot -h xy # XY file plotting guide\n"
184
- " batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
185
- " batplot -h op # Operando guide\n"
186
- " batplot -m # Open the illustrated txt manual with highlights\n"
136
+ " • XY: XRD/PDF/XAS/User defined curves\n"
137
+ " • EC: Galvanostatic cycling(GC)/Capacity per cycle(CPC)/Diffrential capacity(dQdV)/Cyclic Voltammetry(CV) from Neware (.csv) or Biologic (.mpt)\n"
138
+ " • Operando: contour maps from a folder of .xy/.xye/.dat/.txt and optional file as side panel\n"
139
+ " • Batch: export vector plots for all files in a directory\n\n"
140
+ " • Interactive mode: --i / --interactive flag opens a menu for styling, ranges, export, and save\n\n"
141
+ "How to run (basics):\n"
142
+ " [1D(XY) curves]\n"
143
+ " batplot file1.xy file2.qye [option1] [option2] # 1D curves, read the first two columns as X and Y axis by default\n"
144
+ " batplot allfiles # Plot all files in current directory on same figure\n"
145
+ " batplot allfiles /path/to/dir # Plot all files in specified directory\n"
146
+ " batplot allfiles --i # Plot all files with interactive menu\n"
147
+ " batplot allxyfiles # Plot only .xy files (natural sorted)\n"
148
+ " batplot /path/to/data allnorfiles --i # Plot only .nor files from a directory\n"
149
+ " batplot --all # Batch mode: all XY files → Figures/ as .svg\n"
150
+ " batplot --all --format png # Batch mode: export as .png files\n"
151
+ " batplot --all --xaxis 2theta --xrange 10 80 # Batch mode with custom axis and range\n"
152
+ " batplot --all style.bps # Batch with style: apply style.bps to all files\n"
153
+ " batplot --all ./Style/style.bps # Batch with style: use relative path to style file\n"
154
+ " batplot --all config.bpsg # Batch with style+geom: apply to all XY files\n"
155
+ " batplot file1.qye file2.qye style.bps # Apply style to multiple files and export\n"
156
+ " batplot file1.xy file2.xye ./Style/style.bps # Apply style from relative path\n\n"
157
+ " [Electrochemistry]\n"
158
+ " batplot --gc file.mpt --mass 7.0 # EC GC from .mpt (requires --mass mg)\n"
159
+ " batplot --gc file.csv --i # EC GC from supported .csv (no mass required) with interactive menu\n"
160
+ " batplot --gc --all --mass 7.0 # Batch: all .mpt/.csv → Figures/ as .svg\n"
161
+ " batplot --gc --all --mass 7 --format png # Batch: export as .png files\n"
162
+ " batplot --all --dqdv style.bps --mass 7 # Batch with style: apply style.bps to all GC files\n"
163
+ " batplot --all --gc ./Style/style.bps --mass 7 # Batch with style: use relative path\n"
164
+ " batplot --all --cpc config.bpsg # Batch with style+geom: apply to all CV files\n"
165
+ " batplot --all --cv ./Style/config.bpsg # Batch with style+geom: use relative path\n"
166
+ " batplot --dqdv FILE.csv # EC dQ/dV from supported .csv\n"
167
+ " batplot --dqdv --all # Batch: all .csv in directory (dQdV mode)\n"
168
+ " batplot --cv FILE.mpt # EC CV (cyclic voltammetry) from .mpt\n"
169
+ " batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
170
+ " batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
171
+ " [Operando]\n"
172
+ " batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file) with interactive menu\n\n"
173
+ "Features:\n"
174
+ " • Quick plotting with sensible defaults, no config files needed\n"
175
+ " • Supports many common file formats (see -h xy/ec/op)\n"
176
+ " • Interactive menus (--interactive): styling, ranges, fonts, export, sessions\n"
177
+ " • Batch processing: use 'allfiles' / 'all<ext>files' to plot together, or --all for separate files\n"
178
+ " • Batch exports saved to Figures/ subdirectory (default: .svg format)\n"
179
+ " • Batch styling: apply .bps/.bpsg files to all exports (use --all flag)\n"
180
+ " • Format option: use --format png/pdf/jpg/etc to change export format\n\n"
181
+
182
+ "More help:\n"
183
+ " batplot -h xy # XY file plotting guide\n"
184
+ " batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
185
+ " batplot -h op # Operando guide\n"
186
+ " batplot -m # Open the illustrated txt manual with highlights\n"
187
187
 
188
- "Contact & Updates:\n"
189
- " Subscribe to batplot-lab@kjemi.uio.no for updates\n"
190
- " (If you are not from UiO, send an email to sympa@kjemi.uio.no with the subject line \"subscribe batplot-lab@kjemi.uio.no your-name\")\n"
191
- " Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
192
- " Email: tianda@uio.no\n"
193
- )
188
+ "Contact & Updates:\n\n"
189
+ " Subscribe to batplot-lab@kjemi.uio.no for updates\n"
190
+ " (If you are not from UiO, send an email to sympa@kjemi.uio.no with the subject line \"subscribe batplot-lab@kjemi.uio.no your-name\")\n"
191
+ " Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
192
+ " Email: tianda@uio.no\n"
193
+ )
194
194
  _print_help(msg)
195
195
 
196
196
 
197
197
  def _print_xy_help() -> None:
198
198
  msg = (
199
- "XY plots (diffraction/PDF/XAS)\n\n"
200
- "Supported files: .xye .xy .qye .dat .csv .gr .nor .chik .chir .txt (2-col). CIF overlays supported.\n\n"
201
- "Axis detection: .qye→Q, .gr→r, .nor→energy, .chik→k, .chir→r, else use --xaxis (Q, 2theta, r, k, energy, rft, time).\n"
202
- "If mixing 2θ data in Q, give wavelength per-file (file.xye:1.5406) or global --wl.\n"
199
+ "XY plots (XRD/PDF/XAS and many more)\n\n"
200
+ "Supported files: .xye .xy .qye .dat .csv .gr .nor .chik .chir .txt and other user specified formats. CIF overlays supported.\n\n"
201
+ "Axis detection: .qye→Q, .gr→r, .nor→energy, .chik→k, .chir→r, else use --xaxis (Q, 2theta, r, k, energy, time or user defined).\n"
202
+ "If mixing 2θ data in Q, give wavelength per-file (file.xye:1.5406) or global flag --wl.\n"
203
+ "A wavelength can be converted into a different wave length by file.xye:1.54:0.709.\n"
203
204
  "For electrochemistry CSV/MPT time-voltage plots, use --xaxis time.\n\n"
204
205
  "Examples:\n"
205
- " batplot a.xye:1.5406 b.qye --stack --interactive\n"
206
- " batplot a.dat b.xy --wl 1.54 --out fig.svg\n"
207
- " batplot pattern.qye ticks.cif --interactive\n\n"
206
+ " batplot a.xye:1.5406 b.qye --stack --i\n"
207
+ " batplot a.dat b.xy --wl 1.54 --i\n"
208
+ " batplot pattern.qye ticks.cif:1.54 --i\n\n"
208
209
  "Plot all files together:\n"
209
210
  " batplot allfiles # Plot all XY files on same figure\n"
210
211
  " batplot allfiles /path/to/dir # Plot all XY files in specified directory\n"
@@ -1,6 +1,5 @@
1
- """batplot - Interactive plotting for XRD, PDF, XAS and electrochemistry data.
2
-
3
- This module can be imported as a library (safe, no side effects) or run as CLI (via batplot_main()).
1
+ """batplot - Interactive plotting for 1D, electrochemistry and operando contour plots.
2
+ It is designed for researchers working on materials science and electrochemistry, aiming to speed up the plotting process.
4
3
  """
5
4
 
6
5
  from __future__ import annotations
@@ -428,6 +428,8 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
428
428
  'visible': ax2.spines.get('right').get_visible() if ax2.spines.get('right') else None,
429
429
  'color': ax2.spines.get('right').get_edgecolor() if ax2.spines.get('right') else None},
430
430
  },
431
+ 'spine_colors_auto': getattr(fig, '_cpc_spine_auto', False),
432
+ 'spine_colors': dict(getattr(fig, '_cpc_spine_colors', {})),
431
433
  'labelpads': {
432
434
  'x': getattr(ax.xaxis, 'labelpad', None),
433
435
  'ly': getattr(ax.yaxis, 'labelpad', None), # left y-axis (capacity)
@@ -806,6 +808,24 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
806
808
  pass
807
809
  if spec.get('color') is not None:
808
810
  _set_spine_color('right', spec['color'])
811
+ # Restore spine colors from stored dict
812
+ spine_colors = cfg.get('spine_colors', {})
813
+ if spine_colors:
814
+ for spine_name, color in spine_colors.items():
815
+ _set_spine_color(spine_name, color)
816
+ # Restore auto setting
817
+ spine_auto = cfg.get('spine_colors_auto', False)
818
+ if spine_auto is not None:
819
+ fig._cpc_spine_auto = bool(spine_auto)
820
+ # If auto is enabled, apply colors immediately
821
+ if fig._cpc_spine_auto and not (file_data and len(file_data) > 1):
822
+ try:
823
+ charge_col = _color_of(sc_charge)
824
+ eff_col = _color_of(sc_eff)
825
+ _set_spine_color('left', charge_col)
826
+ _set_spine_color('right', eff_col)
827
+ except Exception:
828
+ pass
809
829
  except Exception:
810
830
  pass
811
831
  # Restore labelpads (preserve current if not in config)
@@ -1428,6 +1448,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1428
1448
  try:
1429
1449
  sc_charge.set_color(charge_col)
1430
1450
  sc_discharge.set_color(discharge_col)
1451
+ # Apply auto colors if enabled
1452
+ if not is_multi_file and getattr(fig, '_cpc_spine_auto', False):
1453
+ _set_spine_color('left', charge_col)
1431
1454
  except Exception:
1432
1455
  pass
1433
1456
  try:
@@ -1555,6 +1578,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1555
1578
  col = val
1556
1579
  try:
1557
1580
  sc_eff.set_color(col)
1581
+ # Apply auto colors if enabled
1582
+ if not is_multi_file and getattr(fig, '_cpc_spine_auto', False):
1583
+ _set_spine_color('right', col)
1558
1584
  except Exception:
1559
1585
  pass
1560
1586
  try:
@@ -1573,12 +1599,42 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1573
1599
  elif key == 'k':
1574
1600
  # Spine colors (w=top, a=left, s=bottom, d=right)
1575
1601
  try:
1576
- print("Set spine colors (with matching tick and label colors):")
1577
- print(_colorize_inline_commands(" w : top spine | a : left spine"))
1578
- print(_colorize_inline_commands(" s : bottom spine | d : right spine"))
1579
- print(_colorize_inline_commands("Example: w:red a:#4561F7 s:blue d:green"))
1580
- line = input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
1581
- if line and line.lower() != 'q':
1602
+ while True:
1603
+ print("\nSet spine colors (with matching tick and label colors):")
1604
+ print(_colorize_inline_commands(" w : top spine | a : left spine"))
1605
+ print(_colorize_inline_commands(" s : bottom spine | d : right spine"))
1606
+ print(_colorize_inline_commands("Example: w:red a:#4561F7 s:blue d:green"))
1607
+ # Add auto function when only one file is loaded
1608
+ if not is_multi_file:
1609
+ auto_enabled = getattr(fig, '_cpc_spine_auto', False)
1610
+ auto_status = "ON" if auto_enabled else "OFF"
1611
+ print(_colorize_inline_commands(f" a : auto (apply capacity curve color to left y-axis, efficiency to right y-axis) [{auto_status}]"))
1612
+ print("q: back to main menu")
1613
+ line = input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
1614
+ if not line or line.lower() == 'q':
1615
+ break
1616
+ # Handle auto toggle when only one file is loaded
1617
+ if not is_multi_file and line.lower() == 'a':
1618
+ auto_enabled = getattr(fig, '_cpc_spine_auto', False)
1619
+ fig._cpc_spine_auto = not auto_enabled
1620
+ new_status = "ON" if fig._cpc_spine_auto else "OFF"
1621
+ print(f"Auto mode: {new_status}")
1622
+ if fig._cpc_spine_auto:
1623
+ # Apply auto colors immediately
1624
+ push_state("color-spine-auto")
1625
+ try:
1626
+ # Get capacity curve color (charge color)
1627
+ charge_col = _color_of(sc_charge)
1628
+ # Get efficiency curve color
1629
+ eff_col = _color_of(sc_eff)
1630
+ # Apply to left and right spines
1631
+ _set_spine_color('left', charge_col)
1632
+ _set_spine_color('right', eff_col)
1633
+ print(f"Applied: left y-axis = {charge_col}, right y-axis = {eff_col}")
1634
+ fig.canvas.draw()
1635
+ except Exception as e:
1636
+ print(f"Error applying auto colors: {e}")
1637
+ continue
1582
1638
  push_state("color-spine")
1583
1639
  # Map wasd to spine names
1584
1640
  key_to_spine = {'w': 'top', 'a': 'left', 's': 'bottom', 'd': 'right'}
@@ -1597,8 +1653,6 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1597
1653
  _set_spine_color(spine_name, resolved)
1598
1654
  print(f"Set {spine_name} spine to {resolved}")
1599
1655
  fig.canvas.draw()
1600
- else:
1601
- print("Canceled.")
1602
1656
  except Exception as e:
1603
1657
  print(f"Error in spine color menu: {e}")
1604
1658
  _print_menu()
@@ -1775,6 +1829,15 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1775
1829
  vis = props.get('visible', False)
1776
1830
  col = props.get('color')
1777
1831
  print(f" {name:<6} lw={lw} visible={vis} color={col}")
1832
+ # Spine colors (k command)
1833
+ spine_colors = snap.get('spine_colors', {})
1834
+ if spine_colors:
1835
+ print("Spine colors:")
1836
+ for name, color in spine_colors.items():
1837
+ print(f" {name}: {color}")
1838
+ spine_auto = snap.get('spine_colors_auto', False)
1839
+ if spine_auto:
1840
+ print(f"Spine colors auto: ON (capacity → left y-axis, efficiency → right y-axis)")
1778
1841
 
1779
1842
  ticks = snap.get('ticks', {})
1780
1843
  print(f"Tick widths: x_major={ticks.get('x_major_width')}, x_minor={ticks.get('x_minor_width')}")
@@ -227,6 +227,120 @@ def _savgol_smooth(y: np.ndarray, window: int = 9, poly: int = 3) -> np.ndarray:
227
227
  return smoothed
228
228
 
229
229
 
230
+ def _apply_stored_smooth_settings(cycle_lines: Dict[int, Dict[str, Optional[object]]], fig) -> None:
231
+ """Apply stored smooth settings to newly visible cycles that haven't been smoothed yet."""
232
+ if not hasattr(fig, '_dqdv_smooth_settings'):
233
+ return
234
+ settings = fig._dqdv_smooth_settings
235
+ if not settings:
236
+ return
237
+
238
+ method = settings.get('method')
239
+ if method == 'diffcap':
240
+ min_step = settings.get('min_step', 0.001)
241
+ window = settings.get('window', 9)
242
+ poly = settings.get('poly', 3)
243
+ for cyc, parts in cycle_lines.items():
244
+ iter_parts = [(None, parts)] if not isinstance(parts, dict) else parts.items()
245
+ for role, ln in iter_parts:
246
+ if ln is None or not ln.get_visible():
247
+ continue
248
+ # Only apply if this cycle hasn't been smoothed yet
249
+ if hasattr(ln, '_smooth_applied') and ln._smooth_applied:
250
+ continue
251
+ xdata = np.asarray(ln.get_xdata(), float)
252
+ ydata = np.asarray(ln.get_ydata(), float)
253
+ if xdata.size < 3:
254
+ continue
255
+ # Get original data if available, otherwise use current data
256
+ if hasattr(ln, '_original_xdata'):
257
+ xdata = np.asarray(ln._original_xdata, float)
258
+ ydata = np.asarray(ln._original_ydata, float)
259
+ else:
260
+ ln._original_xdata = np.array(xdata, copy=True)
261
+ ln._original_ydata = np.array(ydata, copy=True)
262
+ x_clean, y_clean, removed = _diffcap_clean_series(xdata, ydata, min_step)
263
+ if x_clean.size < poly + 2:
264
+ continue
265
+ y_smooth = _savgol_smooth(y_clean, window, poly)
266
+ ln.set_xdata(x_clean)
267
+ ln.set_ydata(y_smooth)
268
+ ln._smooth_applied = True
269
+ elif method == 'voltage_step':
270
+ threshold_v = settings.get('threshold_v', 0.0005)
271
+ for cyc, parts in cycle_lines.items():
272
+ for role in ("charge", "discharge"):
273
+ ln = parts.get(role) if isinstance(parts, dict) else parts
274
+ if ln is None or not ln.get_visible():
275
+ continue
276
+ # Only apply if this cycle hasn't been smoothed yet
277
+ if hasattr(ln, '_smooth_applied') and ln._smooth_applied:
278
+ continue
279
+ xdata = np.asarray(ln.get_xdata(), float)
280
+ ydata = np.asarray(ln.get_ydata(), float)
281
+ if xdata.size < 3:
282
+ continue
283
+ # Get original data if available, otherwise use current data
284
+ if hasattr(ln, '_original_xdata'):
285
+ xdata = np.asarray(ln._original_xdata, float)
286
+ ydata = np.asarray(ln._original_ydata, float)
287
+ else:
288
+ ln._original_xdata = np.array(xdata, copy=True)
289
+ ln._original_ydata = np.array(ydata, copy=True)
290
+ dv = np.abs(np.diff(xdata))
291
+ mask = np.ones_like(xdata, dtype=bool)
292
+ mask[1:] &= dv >= threshold_v
293
+ mask[:-1] &= dv >= threshold_v
294
+ filtered_x = xdata[mask]
295
+ filtered_y = ydata[mask]
296
+ if len(filtered_x) < len(xdata):
297
+ ln.set_xdata(filtered_x)
298
+ ln.set_ydata(filtered_y)
299
+ ln._smooth_applied = True
300
+ elif method == 'outlier':
301
+ outlier_method = settings.get('outlier_method', '1')
302
+ threshold = settings.get('threshold', 5.0)
303
+ for cyc, parts in cycle_lines.items():
304
+ for role in ("charge", "discharge"):
305
+ ln = parts.get(role) if isinstance(parts, dict) else parts
306
+ if ln is None or not ln.get_visible():
307
+ continue
308
+ # Only apply if this cycle hasn't been smoothed yet
309
+ if hasattr(ln, '_smooth_applied') and ln._smooth_applied:
310
+ continue
311
+ xdata = np.asarray(ln.get_xdata(), float)
312
+ ydata = np.asarray(ln.get_ydata(), float)
313
+ if xdata.size < 5:
314
+ continue
315
+ # Get original data if available, otherwise use current data
316
+ if hasattr(ln, '_original_xdata'):
317
+ xdata = np.asarray(ln._original_xdata, float)
318
+ ydata = np.asarray(ln._original_ydata, float)
319
+ else:
320
+ ln._original_xdata = np.array(xdata, copy=True)
321
+ ln._original_ydata = np.array(ydata, copy=True)
322
+ if outlier_method == '1':
323
+ mean_y = np.nanmean(ydata)
324
+ std_y = np.nanstd(ydata)
325
+ if not np.isfinite(std_y) or std_y == 0:
326
+ continue
327
+ zscores = np.abs((ydata - mean_y) / std_y)
328
+ mask = zscores <= threshold
329
+ else:
330
+ median_y = np.nanmedian(ydata)
331
+ mad = np.nanmedian(np.abs(ydata - median_y))
332
+ if not np.isfinite(mad) or mad == 0:
333
+ continue
334
+ deviations = np.abs(ydata - median_y) / mad
335
+ mask = deviations <= threshold
336
+ filtered_x = xdata[mask]
337
+ filtered_y = ydata[mask]
338
+ if len(filtered_x) < len(xdata):
339
+ ln.set_xdata(filtered_x)
340
+ ln.set_ydata(filtered_y)
341
+ ln._smooth_applied = True
342
+
343
+
230
344
  def _print_menu(n_cycles: int, is_dqdv: bool = False):
231
345
  # Three-column menu similar to operando: Styles | Geometries | Options
232
346
  # Use dynamic column widths for clean alignment.
@@ -1202,6 +1316,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1202
1316
  try:
1203
1317
  ax.set_xlim(*snap.get('xlim', ax.get_xlim()))
1204
1318
  ax.set_ylim(*snap.get('ylim', ax.get_ylim()))
1319
+ _apply_nice_ticks()
1205
1320
  except Exception:
1206
1321
  pass
1207
1322
  try:
@@ -1287,6 +1402,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1287
1402
  if font_size is not None:
1288
1403
  mpl.rcParams['font.size'] = font_size
1289
1404
  _apply_font_size(ax, font_size)
1405
+ _rebuild_legend(ax)
1290
1406
  except Exception:
1291
1407
  pass
1292
1408
  try:
@@ -1304,6 +1420,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1304
1420
  _apply_font_family(ax, font_sans_serif[0])
1305
1421
  elif font_family:
1306
1422
  _apply_font_family(ax, font_family)
1423
+ _rebuild_legend(ax)
1307
1424
  except Exception:
1308
1425
  pass
1309
1426
  # Title offsets - all four titles
@@ -2360,22 +2477,24 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2360
2477
  elif key == 'k':
2361
2478
  # Spine colors (w=top, a=left, s=bottom, d=right)
2362
2479
  try:
2363
- print("Set spine colors (with matching tick and label colors):")
2364
- print(_colorize_inline_commands(" w : top spine | a : left spine"))
2365
- print(_colorize_inline_commands(" s : bottom spine | d : right spine"))
2366
- print(_colorize_inline_commands("Example: w:red a:#4561F7 s:blue d:green"))
2367
- user_colors = get_user_color_list(fig)
2368
- if user_colors:
2369
- print("\nSaved colors (enter number or u# to reuse):")
2370
- for idx, color in enumerate(user_colors, 1):
2371
- print(f" {idx}: {color_block(color)} {color}")
2372
- print("Type 'u' to edit saved colors.")
2373
- line = input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
2374
- if line.lower() == 'u':
2375
- manage_user_colors(fig)
2376
- _print_menu(len(all_cycles), is_dqdv)
2377
- continue
2378
- if line and line.lower() != 'q':
2480
+ while True:
2481
+ print("\nSet spine colors (with matching tick and label colors):")
2482
+ print(_colorize_inline_commands(" w : top spine | a : left spine"))
2483
+ print(_colorize_inline_commands(" s : bottom spine | d : right spine"))
2484
+ print(_colorize_inline_commands("Example: w:red a:#4561F7 s:blue d:green"))
2485
+ user_colors = get_user_color_list(fig)
2486
+ if user_colors:
2487
+ print("\nSaved colors (enter number or u# to reuse):")
2488
+ for idx, color in enumerate(user_colors, 1):
2489
+ print(f" {idx}: {color_block(color)} {color}")
2490
+ print("Type 'u' to edit saved colors.")
2491
+ print("q: back to main menu")
2492
+ line = input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
2493
+ if not line or line.lower() == 'q':
2494
+ break
2495
+ if line.lower() == 'u':
2496
+ manage_user_colors(fig)
2497
+ continue
2379
2498
  push_state("color-spine")
2380
2499
  key_to_spine = {'w': 'top', 'a': 'left', 's': 'bottom', 'd': 'right'}
2381
2500
  tokens = line.split()
@@ -2409,8 +2528,6 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2409
2528
  except Exception as e:
2410
2529
  print(f"Error setting {spine_name} color: {e}")
2411
2530
  fig.canvas.draw()
2412
- else:
2413
- print("Canceled.")
2414
2531
  except Exception as e:
2415
2532
  print(f"Error in spine color menu: {e}")
2416
2533
  _print_menu(len(all_cycles), is_dqdv)
@@ -2874,6 +2991,10 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2874
2991
  # Reapply curve linewidth (in case it was set)
2875
2992
  _apply_curve_linewidth(fig, cycle_lines)
2876
2993
 
2994
+ # Apply stored smooth settings to newly visible cycles (only in dQdV mode)
2995
+ if is_dqdv and hasattr(fig, '_dqdv_smooth_settings'):
2996
+ _apply_stored_smooth_settings(cycle_lines, fig)
2997
+
2877
2998
  # Rebuild legend and redraw
2878
2999
  _rebuild_legend(ax)
2879
3000
  _apply_nice_ticks()
@@ -3331,9 +3452,15 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3331
3452
  if hasattr(ln, '_original_xdata'):
3332
3453
  ln.set_xdata(ln._original_xdata)
3333
3454
  ln.set_ydata(ln._original_ydata)
3455
+ # Clear smooth flag so smooth can be reapplied if needed
3456
+ if hasattr(ln, '_smooth_applied'):
3457
+ delattr(ln, '_smooth_applied')
3334
3458
  restored_count += 1
3335
3459
  if restored_count:
3336
3460
  print(f"Reset {restored_count} curve(s) to original data.")
3461
+ # Clear stored smooth settings
3462
+ if hasattr(fig, '_dqdv_smooth_settings'):
3463
+ fig._dqdv_smooth_settings = {}
3337
3464
  fig.canvas.draw_idle()
3338
3465
  else:
3339
3466
  print("No filtered data to reset.")
@@ -3365,6 +3492,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3365
3492
  print("Threshold must be positive.")
3366
3493
  continue
3367
3494
  push_state("smooth-apply")
3495
+ # Store smooth settings for future cycle changes
3496
+ if not hasattr(fig, '_dqdv_smooth_settings'):
3497
+ fig._dqdv_smooth_settings = {}
3498
+ fig._dqdv_smooth_settings.update({
3499
+ 'method': 'voltage_step',
3500
+ 'threshold_v': threshold_v
3501
+ })
3368
3502
  filtered = 0
3369
3503
  total_before = 0
3370
3504
  total_after = 0
@@ -3391,6 +3525,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3391
3525
  if after < before:
3392
3526
  ln.set_xdata(filtered_x)
3393
3527
  ln.set_ydata(filtered_y)
3528
+ ln._smooth_applied = True
3394
3529
  filtered += 1
3395
3530
  total_before += before
3396
3531
  total_after += after
@@ -3428,7 +3563,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3428
3563
  print("ΔV threshold must be positive.")
3429
3564
  continue
3430
3565
  break
3431
- if not delta_input or delta_input.lower() == 'q': # User quit at previous step
3566
+ # Only skip if user explicitly quit with 'q', not if they pressed Enter (empty = use default)
3567
+ if delta_input and delta_input.lower() == 'q': # User quit at previous step
3432
3568
  continue
3433
3569
  while True:
3434
3570
  window_input = input("Savitzky–Golay window (odd, default 9, 'q'=quit, 'e'=explain): ").strip()
@@ -3446,7 +3582,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3446
3582
  continue
3447
3583
  window = 9 if not window_input else int(window_input)
3448
3584
  break
3449
- if not window_input or window_input.lower() == 'q': # User quit at previous step
3585
+ # Only skip if user explicitly quit with 'q', not if they pressed Enter (empty = use default)
3586
+ if window_input and window_input.lower() == 'q': # User quit at previous step
3450
3587
  continue
3451
3588
  while True:
3452
3589
  poly_input = input("Polynomial order (default 3, 'q'=quit, 'e'=explain): ").strip()
@@ -3465,7 +3602,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3465
3602
  continue
3466
3603
  poly = 3 if not poly_input else int(poly_input)
3467
3604
  break
3468
- if not poly_input or poly_input.lower() == 'q': # User quit at previous step
3605
+ # Only skip if user explicitly quit with 'q', not if they pressed Enter (empty = use default)
3606
+ if poly_input and poly_input.lower() == 'q': # User quit at previous step
3469
3607
  continue
3470
3608
  except ValueError:
3471
3609
  print("Invalid number.")
@@ -3477,6 +3615,15 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3477
3615
  if poly < 1:
3478
3616
  poly = 1
3479
3617
  push_state("smooth-diffcap")
3618
+ # Store smooth settings for future cycle changes
3619
+ if not hasattr(fig, '_dqdv_smooth_settings'):
3620
+ fig._dqdv_smooth_settings = {}
3621
+ fig._dqdv_smooth_settings.update({
3622
+ 'method': 'diffcap',
3623
+ 'min_step': min_step,
3624
+ 'window': window,
3625
+ 'poly': poly
3626
+ })
3480
3627
  cleaned_curves = 0
3481
3628
  total_removed = 0
3482
3629
  for cyc, parts in cycle_lines.items():
@@ -3497,6 +3644,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3497
3644
  y_smooth = _savgol_smooth(y_clean, window, poly)
3498
3645
  ln.set_xdata(x_clean)
3499
3646
  ln.set_ydata(y_smooth)
3647
+ ln._smooth_applied = True
3500
3648
  cleaned_curves += 1
3501
3649
  total_removed += removed
3502
3650
  if cleaned_curves:
@@ -3570,9 +3718,19 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3570
3718
  print("Threshold must be positive.")
3571
3719
  continue
3572
3720
  break
3573
- if not thresh_input or thresh_input.lower() == 'q': # User quit
3721
+ # Only skip if user explicitly quit with 'q', not if they pressed Enter (empty = use default)
3722
+ if thresh_input and thresh_input.lower() == 'q': # User quit
3574
3723
  continue
3575
3724
  push_state("smooth-outlier")
3725
+ # Store smooth settings for future cycle changes
3726
+ if not hasattr(fig, '_dqdv_smooth_settings'):
3727
+ fig._dqdv_smooth_settings = {}
3728
+ thresh_val = z_threshold if method == '1' else mad_threshold
3729
+ fig._dqdv_smooth_settings.update({
3730
+ 'method': 'outlier',
3731
+ 'outlier_method': method,
3732
+ 'threshold': thresh_val
3733
+ })
3576
3734
  filtered = 0
3577
3735
  total_before = 0
3578
3736
  total_after = 0
@@ -3609,6 +3767,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3609
3767
  if after < before:
3610
3768
  ln.set_xdata(filtered_x)
3611
3769
  ln.set_ydata(filtered_y)
3770
+ ln._smooth_applied = True
3612
3771
  filtered += 1
3613
3772
  total_before += before
3614
3773
  total_after += after
@@ -3616,7 +3775,6 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3616
3775
  removed = total_before - total_after
3617
3776
  pct = 100 * removed / total_before if total_before else 0
3618
3777
  method_name = "Z-score" if method == '1' else "MAD"
3619
- thresh_val = z_threshold if method == '1' else mad_threshold
3620
3778
  print(f"Removed outliers from {filtered} curve(s) using {method_name} (threshold={thresh_val}).")
3621
3779
  print(f"Removed {removed} of {total_before} points ({pct:.1f}%).")
3622
3780
  print("Tip: Adjust threshold to control sensitivity (always applied to raw data).")
@@ -11,11 +11,6 @@ An operando plot is a 2D visualization where:
11
11
  - Y-axis: Time/scan number (which file/measurement in sequence)
12
12
  - Color/Z-axis: Intensity (how bright the diffraction signal is)
13
13
 
14
- Think of it like a "heat map" where:
15
- - Each row is one diffraction pattern (one file)
16
- - Each column is one angle/Q value
17
- - Color intensity shows how strong the diffraction signal is
18
-
19
14
  Example use cases:
20
15
  - Watching a material transform during heating (phase transitions)
21
16
  - Monitoring battery electrode changes during cycling
@@ -24,12 +19,12 @@ Example use cases:
24
19
 
25
20
  HOW IT WORKS:
26
21
  ------------
27
- 1. Scan folder for diffraction data files (.xy, .xye, .qye, .dat)
22
+ 1. Scan folder for XRD/PDF/XAS or other data files
28
23
  2. Load each file as one "scan" (one row in the contour)
29
24
  3. Create a common X-axis grid (interpolate all scans to same grid)
30
25
  4. Stack all scans vertically to form a 2D array
31
26
  5. Display as intensity contour (color map)
32
- 6. Optionally add electrochemistry data as side panel (if .mpt file present)
27
+ 6. Optionally add electrochemistry/temperature/other data as side panel (if .mpt file present)
33
28
 
34
29
  AXIS MODE DETECTION:
35
30
  -------------------
@@ -37,14 +32,7 @@ The X-axis type is determined automatically:
37
32
  - If --xaxis Q specified → Use Q-space
38
33
  - If files are .qye → Use Q-space (already in Q)
39
34
  - If --wl specified → Convert 2θ to Q using wavelength
40
- - OtherwiseUse (degrees)
41
-
42
- NO NORMALIZATION:
43
- ---------------
44
- Unlike some other modes, operando plots don't normalize intensity.
45
- The color scale spans from minimum to maximum intensity across ALL scans.
46
- This preserves the absolute intensity information, which is important for
47
- comparing different time points.
35
+ - Other operando data (such as PDF/XAS or others) Plot the first two columns as X and Y
48
36
  """
49
37
  from __future__ import annotations
50
38
 
@@ -2000,19 +2000,20 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2000
2000
  ax.set_ylim(y1, y0)
2001
2001
  except Exception as e:
2002
2002
  print(f"Operando reverse failed: {e}")
2003
- try:
2004
- ey0, ey1 = ec_ax.get_ylim()
2005
- ec_ax.set_ylim(ey1, ey0)
2006
- # If we have a stored time ylim for restoration later, invert it too
2007
- if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
2008
- lo, hi = ec_ax._saved_time_ylim
2009
- try:
2010
- ec_ax._saved_time_ylim = (hi, lo)
2011
- except Exception:
2012
- pass
2013
- fig.canvas.draw_idle()
2014
- except Exception as e:
2015
- print(f"EC reverse failed: {e}")
2003
+ if ec_ax is not None:
2004
+ try:
2005
+ ey0, ey1 = ec_ax.get_ylim()
2006
+ ec_ax.set_ylim(ey1, ey0)
2007
+ # If we have a stored time ylim for restoration later, invert it too
2008
+ if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
2009
+ lo, hi = ec_ax._saved_time_ylim
2010
+ try:
2011
+ ec_ax._saved_time_ylim = (hi, lo)
2012
+ except Exception:
2013
+ pass
2014
+ fig.canvas.draw_idle()
2015
+ except Exception as e:
2016
+ print(f"EC reverse failed: {e}")
2016
2017
  print_menu()
2017
2018
  elif cmd == 'f':
2018
2019
  # Font submenu with numbered options
@@ -2104,11 +2105,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2104
2105
  ax.tick_params(axis='both', which='major', width=tick_w)
2105
2106
  ax.tick_params(axis='both', which='minor', width=tick_w)
2106
2107
 
2107
- # Apply to EC pane (ec_ax)
2108
- for spine in ec_ax.spines.values():
2109
- spine.set_linewidth(frame_w)
2110
- ec_ax.tick_params(axis='both', which='major', width=tick_w)
2111
- ec_ax.tick_params(axis='both', which='minor', width=tick_w)
2108
+ # Apply to EC pane (ec_ax) if present
2109
+ if ec_ax is not None:
2110
+ for spine in ec_ax.spines.values():
2111
+ spine.set_linewidth(frame_w)
2112
+ ec_ax.tick_params(axis='both', which='major', width=tick_w)
2113
+ ec_ax.tick_params(axis='both', which='minor', width=tick_w)
2112
2114
 
2113
2115
  # Also apply to colorbar if present
2114
2116
  if cbar is not None:
@@ -2125,7 +2127,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2125
2127
  except Exception:
2126
2128
  fig.canvas.draw_idle()
2127
2129
 
2128
- print(f"Applied: frame={frame_w:.2f}, ticks={tick_w:.2f} to operando, EC, and colorbar")
2130
+ if ec_ax is not None:
2131
+ print(f"Applied: frame={frame_w:.2f}, ticks={tick_w:.2f} to operando, EC, and colorbar")
2132
+ else:
2133
+ print(f"Applied: frame={frame_w:.2f}, ticks={tick_w:.2f} to operando and colorbar")
2129
2134
  except ValueError:
2130
2135
  print("Invalid number format.")
2131
2136
  except Exception as e:
@@ -4027,28 +4032,29 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4027
4032
  except Exception as e:
4028
4033
  print(f"Warning: Could not apply operando reverse: {e}")
4029
4034
 
4030
- try:
4031
- # EC Y-axis reverse
4032
- ec_cfg = cfg.get('ec', {})
4033
- ec_y_reversed = ec_cfg.get('y_reversed', False)
4034
- if ec_y_reversed:
4035
- ey0, ey1 = ec_ax.get_ylim()
4036
- if ey0 < ey1: # Only reverse if not already reversed
4037
- ec_ax.set_ylim(ey1, ey0)
4038
- # Also update stored time ylim if present
4039
- if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
4040
- lo, hi = ec_ax._saved_time_ylim
4041
- ec_ax._saved_time_ylim = (hi, lo)
4042
- else:
4043
- ey0, ey1 = ec_ax.get_ylim()
4044
- if ey0 > ey1: # Un-reverse if currently reversed
4045
- ec_ax.set_ylim(ey1, ey0)
4046
- # Also update stored time ylim if present
4047
- if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
4048
- lo, hi = ec_ax._saved_time_ylim
4049
- ec_ax._saved_time_ylim = (hi, lo)
4050
- except Exception as e:
4051
- print(f"Warning: Could not apply EC reverse: {e}")
4035
+ if ec_ax is not None:
4036
+ try:
4037
+ # EC Y-axis reverse
4038
+ ec_cfg = cfg.get('ec', {})
4039
+ ec_y_reversed = ec_cfg.get('y_reversed', False)
4040
+ if ec_y_reversed:
4041
+ ey0, ey1 = ec_ax.get_ylim()
4042
+ if ey0 < ey1: # Only reverse if not already reversed
4043
+ ec_ax.set_ylim(ey1, ey0)
4044
+ # Also update stored time ylim if present
4045
+ if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
4046
+ lo, hi = ec_ax._saved_time_ylim
4047
+ ec_ax._saved_time_ylim = (hi, lo)
4048
+ else:
4049
+ ey0, ey1 = ec_ax.get_ylim()
4050
+ if ey0 > ey1: # Un-reverse if currently reversed
4051
+ ec_ax.set_ylim(ey1, ey0)
4052
+ # Also update stored time ylim if present
4053
+ if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
4054
+ lo, hi = ec_ax._saved_time_ylim
4055
+ ec_ax._saved_time_ylim = (hi, lo)
4056
+ except Exception as e:
4057
+ print(f"Warning: Could not apply EC reverse: {e}")
4052
4058
 
4053
4059
  # Apply intensity range (oz command)
4054
4060
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.7.20
3
+ Version: 1.7.22
4
4
  Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "batplot"
7
- version = "1.7.20"
7
+ version = "1.7.22"
8
8
  description = "Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing"
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
File without changes
File without changes
File without changes