batplot 1.7.21__py3-none-any.whl → 1.7.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

batplot/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.7.21"
3
+ __version__ = "1.7.23"
4
4
 
5
5
  __all__ = ["__version__"]
batplot/args.py CHANGED
@@ -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,80 @@ 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
+ " Personal page: https://www.mn.uio.no/kjemi/english/people/aca/tianda/\n"
194
+ )
194
195
  _print_help(msg)
195
196
 
196
197
 
197
198
  def _print_xy_help() -> None:
198
199
  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"
200
+ "XY plots (XRD/PDF/XAS and many more)\n\n"
201
+ "Supported files: .xye .xy .qye .dat .csv .gr .nor .chik .chir .txt and other user specified formats. CIF overlays supported.\n\n"
202
+ "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"
203
+ "If mixing 2θ data in Q, give wavelength per-file (file.xye:1.5406) or global flag --wl.\n"
204
+ "A wavelength can be converted into a different wave length by file.xye:1.54:0.709.\n"
203
205
  "For electrochemistry CSV/MPT time-voltage plots, use --xaxis time.\n\n"
204
206
  "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"
207
+ " batplot a.xye:1.5406 b.qye --stack --i\n"
208
+ " batplot a.dat b.xy --wl 1.54 --i\n"
209
+ " batplot pattern.qye ticks.cif:1.54 --i\n\n"
208
210
  "Plot all files together:\n"
209
211
  " batplot allfiles # Plot all XY files on same figure\n"
210
212
  " batplot allfiles /path/to/dir # Plot all XY files in specified directory\n"
batplot/batplot.py CHANGED
@@ -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
@@ -3480,6 +3615,15 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3480
3615
  if poly < 1:
3481
3616
  poly = 1
3482
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
+ })
3483
3627
  cleaned_curves = 0
3484
3628
  total_removed = 0
3485
3629
  for cyc, parts in cycle_lines.items():
@@ -3500,6 +3644,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3500
3644
  y_smooth = _savgol_smooth(y_clean, window, poly)
3501
3645
  ln.set_xdata(x_clean)
3502
3646
  ln.set_ydata(y_smooth)
3647
+ ln._smooth_applied = True
3503
3648
  cleaned_curves += 1
3504
3649
  total_removed += removed
3505
3650
  if cleaned_curves:
@@ -3577,6 +3722,15 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3577
3722
  if thresh_input and thresh_input.lower() == 'q': # User quit
3578
3723
  continue
3579
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
+ })
3580
3734
  filtered = 0
3581
3735
  total_before = 0
3582
3736
  total_after = 0
@@ -3613,6 +3767,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3613
3767
  if after < before:
3614
3768
  ln.set_xdata(filtered_x)
3615
3769
  ln.set_ydata(filtered_y)
3770
+ ln._smooth_applied = True
3616
3771
  filtered += 1
3617
3772
  total_before += before
3618
3773
  total_after += after
@@ -3620,7 +3775,6 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3620
3775
  removed = total_before - total_after
3621
3776
  pct = 100 * removed / total_before if total_before else 0
3622
3777
  method_name = "Z-score" if method == '1' else "MAD"
3623
- thresh_val = z_threshold if method == '1' else mad_threshold
3624
3778
  print(f"Removed outliers from {filtered} curve(s) using {method_name} (threshold={thresh_val}).")
3625
3779
  print(f"Removed {removed} of {total_before} points ({pct:.1f}%).")
3626
3780
  print("Tip: Adjust threshold to control sensitivity (always applied to raw data).")
batplot/operando.py CHANGED
@@ -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
 
@@ -1050,6 +1050,15 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1050
1050
  # Horizontal offsets (relative to canvas center, in inches)
1051
1051
  cb_h_offset = getattr(cbar.ax, '_cb_h_offset_in', 0.0)
1052
1052
  ec_h_offset = getattr(ec_ax, '_ec_h_offset_in', 0.0) if ec_ax is not None else None
1053
+ # Colorbar tick/label positions (left/right)
1054
+ cb_ticks_left = True
1055
+ cb_label_left = True
1056
+ try:
1057
+ cb_ticks_left = any(getattr(tick, 'tick1line', None) and tick.tick1line.get_visible() for tick in cbar.ax.yaxis.get_major_ticks())
1058
+ # label position is stored on axis; capture current setting
1059
+ cb_label_left = (cbar.ax.yaxis.get_label_position() == 'left')
1060
+ except Exception:
1061
+ pass
1053
1062
  # Label pads (save current labelpad values to restore later)
1054
1063
  op_labelpads = {
1055
1064
  'x': getattr(ax.xaxis, 'labelpad', None),
@@ -1084,6 +1093,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1084
1093
  'ec_visible': ec_visible,
1085
1094
  'cb_h_offset': float(cb_h_offset),
1086
1095
  'ec_h_offset': float(ec_h_offset) if ec_h_offset is not None else None,
1096
+ 'cb_ticks_left': cb_ticks_left,
1097
+ 'cb_label_left': cb_label_left,
1087
1098
  'op_labelpads': dict(op_labelpads),
1088
1099
  'ec_labelpads': dict(ec_labelpads) if ec_labelpads is not None else None,
1089
1100
  'op_title_offsets': {
@@ -1138,6 +1149,14 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1138
1149
  _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_i, ax_h_i, cb_w_i, cb_gap_i, ec_gap_i, ec_w_i)
1139
1150
  except Exception:
1140
1151
  pass
1152
+ # Colorbar tick/label side
1153
+ try:
1154
+ cb_ticks_left = snap.get('cb_ticks_left', True)
1155
+ cb_label_left = snap.get('cb_label_left', True)
1156
+ cbar.ax.yaxis.set_ticks_position('left' if cb_ticks_left else 'right')
1157
+ cbar.ax.yaxis.set_label_position('left' if cb_label_left else 'right')
1158
+ except Exception:
1159
+ pass
1141
1160
  # Labels
1142
1161
  try:
1143
1162
  op_l = snap.get('op_labels', {})
@@ -1170,6 +1189,14 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1170
1189
  pass
1171
1190
  try:
1172
1191
  if snap.get('clim') is not None:
1192
+ # Detach built-in colorbar update to avoid artist removal errors; we redraw custom below.
1193
+ try:
1194
+ if hasattr(cbar, 'mappable'):
1195
+ cbar.mappable = None
1196
+ if hasattr(cbar, 'solids'):
1197
+ cbar.solids = None
1198
+ except Exception:
1199
+ pass
1173
1200
  lo, hi = snap['clim']; im.set_clim(float(lo), float(hi))
1174
1201
  except Exception:
1175
1202
  pass
@@ -1180,6 +1207,15 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1180
1207
  cbar.update_normal(im)
1181
1208
  except Exception:
1182
1209
  pass
1210
+ # Restore colorbar side (ticks/label) and redraw custom colorbar to keep position
1211
+ try:
1212
+ cb_ticks_left = snap.get('cb_ticks_left', True)
1213
+ cb_label_left = snap.get('cb_label_left', True)
1214
+ cbar.ax.yaxis.set_ticks_position('left' if cb_ticks_left else 'right')
1215
+ cbar.ax.yaxis.set_label_position('left' if cb_label_left else 'right')
1216
+ _update_custom_colorbar(cbar.ax, im)
1217
+ except Exception:
1218
+ pass
1183
1219
  # EC axes
1184
1220
  try:
1185
1221
  if ec_ax is not None:
@@ -2000,19 +2036,20 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2000
2036
  ax.set_ylim(y1, y0)
2001
2037
  except Exception as e:
2002
2038
  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}")
2039
+ if ec_ax is not None:
2040
+ try:
2041
+ ey0, ey1 = ec_ax.get_ylim()
2042
+ ec_ax.set_ylim(ey1, ey0)
2043
+ # If we have a stored time ylim for restoration later, invert it too
2044
+ if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
2045
+ lo, hi = ec_ax._saved_time_ylim
2046
+ try:
2047
+ ec_ax._saved_time_ylim = (hi, lo)
2048
+ except Exception:
2049
+ pass
2050
+ fig.canvas.draw_idle()
2051
+ except Exception as e:
2052
+ print(f"EC reverse failed: {e}")
2016
2053
  print_menu()
2017
2054
  elif cmd == 'f':
2018
2055
  # Font submenu with numbered options
@@ -2104,11 +2141,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2104
2141
  ax.tick_params(axis='both', which='major', width=tick_w)
2105
2142
  ax.tick_params(axis='both', which='minor', width=tick_w)
2106
2143
 
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)
2144
+ # Apply to EC pane (ec_ax) if present
2145
+ if ec_ax is not None:
2146
+ for spine in ec_ax.spines.values():
2147
+ spine.set_linewidth(frame_w)
2148
+ ec_ax.tick_params(axis='both', which='major', width=tick_w)
2149
+ ec_ax.tick_params(axis='both', which='minor', width=tick_w)
2112
2150
 
2113
2151
  # Also apply to colorbar if present
2114
2152
  if cbar is not None:
@@ -2125,7 +2163,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2125
2163
  except Exception:
2126
2164
  fig.canvas.draw_idle()
2127
2165
 
2128
- print(f"Applied: frame={frame_w:.2f}, ticks={tick_w:.2f} to operando, EC, and colorbar")
2166
+ if ec_ax is not None:
2167
+ print(f"Applied: frame={frame_w:.2f}, ticks={tick_w:.2f} to operando, EC, and colorbar")
2168
+ else:
2169
+ print(f"Applied: frame={frame_w:.2f}, ticks={tick_w:.2f} to operando and colorbar")
2129
2170
  except ValueError:
2130
2171
  print("Invalid number format.")
2131
2172
  except Exception as e:
@@ -2835,7 +2876,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2835
2876
  continue
2836
2877
  _snapshot("operando-xrange")
2837
2878
  ax.set_xlim(cur[0], new_upper)
2838
- _renormalize_to_visible()
2839
2879
  fig.canvas.draw_idle()
2840
2880
  print(f"Operando X range updated: {ax.get_xlim()[0]:.4g} {ax.get_xlim()[1]:.4g}")
2841
2881
  if line.lower() == 'w':
@@ -2855,7 +2895,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2855
2895
  continue
2856
2896
  _snapshot("operando-xrange")
2857
2897
  ax.set_xlim(new_lower, cur[1])
2858
- _renormalize_to_visible()
2859
2898
  fig.canvas.draw_idle()
2860
2899
  print(f"Operando X range updated: {ax.get_xlim()[0]:.4g} {ax.get_xlim()[1]:.4g}")
2861
2900
  if line.lower() == 's':
@@ -2872,7 +2911,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2872
2911
  orig_min = min(extent[0], extent[1])
2873
2912
  orig_max = max(extent[0], extent[1])
2874
2913
  ax.set_xlim(orig_min, orig_max)
2875
- _renormalize_to_visible()
2876
2914
  fig.canvas.draw_idle()
2877
2915
  print(f"Operando X range restored to original: {ax.get_xlim()[0]:.4g} {ax.get_xlim()[1]:.4g}")
2878
2916
  else:
@@ -2886,8 +2924,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2886
2924
  try:
2887
2925
  lo, hi = map(float, line.split())
2888
2926
  ax.set_xlim(lo, hi)
2889
- # Re-normalize intensity to visible region
2890
- _renormalize_to_visible()
2891
2927
  fig.canvas.draw_idle()
2892
2928
  except Exception as e:
2893
2929
  print(f"Invalid range: {e}")
@@ -2913,7 +2949,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2913
2949
  continue
2914
2950
  _snapshot("operando-yrange")
2915
2951
  ax.set_ylim(cur[0], new_upper)
2916
- _renormalize_to_visible()
2917
2952
  fig.canvas.draw_idle()
2918
2953
  print(f"Operando Y range updated: {ax.get_ylim()[0]:.4g} {ax.get_ylim()[1]:.4g}")
2919
2954
  if line.lower() == 'w':
@@ -2933,7 +2968,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2933
2968
  continue
2934
2969
  _snapshot("operando-yrange")
2935
2970
  ax.set_ylim(new_lower, cur[1])
2936
- _renormalize_to_visible()
2937
2971
  fig.canvas.draw_idle()
2938
2972
  print(f"Operando Y range updated: {ax.get_ylim()[0]:.4g} {ax.get_ylim()[1]:.4g}")
2939
2973
  if line.lower() == 's':
@@ -2950,7 +2984,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2950
2984
  orig_min = min(extent[2], extent[3])
2951
2985
  orig_max = max(extent[2], extent[3])
2952
2986
  ax.set_ylim(orig_min, orig_max)
2953
- _renormalize_to_visible()
2954
2987
  fig.canvas.draw_idle()
2955
2988
  print(f"Operando Y range restored to original: {ax.get_ylim()[0]:.4g} {ax.get_ylim()[1]:.4g}")
2956
2989
  else:
@@ -2964,8 +2997,6 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2964
2997
  try:
2965
2998
  lo, hi = map(float, line.split())
2966
2999
  ax.set_ylim(lo, hi)
2967
- # Re-normalize intensity to visible region
2968
- _renormalize_to_visible()
2969
3000
  fig.canvas.draw_idle()
2970
3001
  except Exception as e:
2971
3002
  print(f"Invalid range: {e}")
@@ -4027,28 +4058,29 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4027
4058
  except Exception as e:
4028
4059
  print(f"Warning: Could not apply operando reverse: {e}")
4029
4060
 
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}")
4061
+ if ec_ax is not None:
4062
+ try:
4063
+ # EC Y-axis reverse
4064
+ ec_cfg = cfg.get('ec', {})
4065
+ ec_y_reversed = ec_cfg.get('y_reversed', False)
4066
+ if ec_y_reversed:
4067
+ ey0, ey1 = ec_ax.get_ylim()
4068
+ if ey0 < ey1: # Only reverse if not already reversed
4069
+ ec_ax.set_ylim(ey1, ey0)
4070
+ # Also update stored time ylim if present
4071
+ if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
4072
+ lo, hi = ec_ax._saved_time_ylim
4073
+ ec_ax._saved_time_ylim = (hi, lo)
4074
+ else:
4075
+ ey0, ey1 = ec_ax.get_ylim()
4076
+ if ey0 > ey1: # Un-reverse if currently reversed
4077
+ ec_ax.set_ylim(ey1, ey0)
4078
+ # Also update stored time ylim if present
4079
+ if hasattr(ec_ax, '_saved_time_ylim') and isinstance(ec_ax._saved_time_ylim, (tuple, list)) and len(ec_ax._saved_time_ylim)==2:
4080
+ lo, hi = ec_ax._saved_time_ylim
4081
+ ec_ax._saved_time_ylim = (hi, lo)
4082
+ except Exception as e:
4083
+ print(f"Warning: Could not apply EC reverse: {e}")
4052
4084
 
4053
4085
  # Apply intensity range (oz command)
4054
4086
  try:
batplot/session.py CHANGED
@@ -38,6 +38,7 @@ import matplotlib.pyplot as plt
38
38
  import numpy as np
39
39
 
40
40
  from .utils import _confirm_overwrite
41
+ from .color_utils import ensure_colormap
41
42
 
42
43
 
43
44
  def _current_tick_width(axis_obj, which: str):
@@ -831,8 +832,14 @@ def load_operando_session(filename: str):
831
832
  op = sess['operando']
832
833
  arr = _ma.masked_invalid(op['array'])
833
834
  extent = tuple(op['extent']) if op['extent'] is not None else None
835
+ cmap_name = op.get('cmap') or 'viridis'
836
+ try:
837
+ if not ensure_colormap(cmap_name):
838
+ cmap_name = 'viridis'
839
+ except Exception:
840
+ cmap_name = 'viridis'
834
841
  im = ax.imshow(arr, aspect='auto', origin=op.get('origin', 'upper'), extent=extent,
835
- cmap=op.get('cmap') or 'viridis', interpolation=op.get('interpolation', 'nearest'))
842
+ cmap=cmap_name, interpolation=op.get('interpolation', 'nearest'))
836
843
  if op.get('clim'):
837
844
  try:
838
845
  im.set_clim(*op['clim'])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.7.21
3
+ Version: 1.7.23
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
@@ -171,7 +171,9 @@ See [LICENSE](LICENSE)
171
171
 
172
172
  ## Author & Contact
173
173
 
174
- Tian Dai (tianda@uio.no)
174
+ Tian Dai
175
+ tianda@uio.no
175
176
  University of Oslo
177
+ https://www.mn.uio.no/kjemi/english/people/aca/tianda/
176
178
 
177
179
  **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
@@ -1,28 +1,28 @@
1
- batplot/__init__.py,sha256=pBlm8KrETGMEuMg9SQKe981DRAp6yRj-OovrBkyjWAY,119
2
- batplot/args.py,sha256=DYo3qYHL4UpZwcZLJ_70NpPgNv6SQSOdo_jsV4mYqaQ,34355
1
+ batplot/__init__.py,sha256=vkOVAS6v23TnQZXUW0hx9pGUWLY7MUF__AXzrydzgBs,119
2
+ batplot/args.py,sha256=6g1eHVsycDSAenwrXuCTcyuXCy_-30zy1lWtdguBG2s,34983
3
3
  batplot/batch.py,sha256=YQ7obCIqLCObwDbM7TXpOBh7g7BO95wZNsa2Fy84c6o,53858
4
- batplot/batplot.py,sha256=pUISNB_HxjcNvnzrC3Qy-nyk5MZctG8ptuhPh-7gho8,170659
4
+ batplot/batplot.py,sha256=h960XKSmJ825DqI99Xnyomg_gaB0aFDUIKxkovm8BPQ,170691
5
5
  batplot/cif.py,sha256=JfHwNf3SHrcpALc_F5NjJmQ3lg71MBRSaIUJjGYPTx8,30120
6
6
  batplot/cli.py,sha256=ScDb2je8VQ0mz_z0SLCHEigiTuFPY5pb1snnzCouKms,5828
7
7
  batplot/color_utils.py,sha256=ow2ElqjIWFLRdrnLwQvrnfa3w3IEB0FodPFdoDQR_Dc,19990
8
8
  batplot/config.py,sha256=6nGY7fKN4T5KZUGQS2ArUBgEkLAL0j37XwG5SCVQgKA,6420
9
9
  batplot/converters.py,sha256=rR2WMPM0nR5E3eZI3gWbaJf_AfbdQx3urVSbJmZXNzo,8237
10
- batplot/cpc_interactive.py,sha256=Q4MEr_eb9KNYIxHPko2DFnzO2p66lPx8I_S1dXkOFSo,183452
11
- batplot/electrochem_interactive.py,sha256=kGgkmyxPh74E8VxvmpKkpWtAJ-ohE8s-zfbt0E5w9tQ,201277
10
+ batplot/cpc_interactive.py,sha256=ICrY25rSqKsds9Cb2lW1GiTdY-lTQaAuD4NRzQCZGCw,187304
11
+ batplot/electrochem_interactive.py,sha256=f71CsXDNoge-7ENKX3PhmnSf7uBFXoHhcPSHL9_cF-Q,209167
12
12
  batplot/interactive.py,sha256=gAmfHLVu4dAC_-hpmpfJj8dyQTBbzRNweVODuQDIocw,196317
13
13
  batplot/manual.py,sha256=pbRI6G4Pm12pOW8LrOLWWu7IEOtqWN3tRHtgge50LlA,11556
14
14
  batplot/modes.py,sha256=qE2OsOQQKhwOWene5zxJeuuewTrZxubtahQuz5je7ok,37252
15
- batplot/operando.py,sha256=SuTbbm8QvrE-AleHv89HGEyuynGU1Ig_1doANaPCO3g,28510
16
- batplot/operando_ec_interactive.py,sha256=hLzZMTKbVNpKiJ7PNCNjiWaem_tFbAXcy3Ak0f59kCs,280055
15
+ batplot/operando.py,sha256=CdTZJa6Cr1wNczFEbwAido2mc7C_h1xxoQ5b045ktSk,28105
16
+ batplot/operando_ec_interactive.py,sha256=snGzZBlgmXhvGupaMv-1Jvb9V7E3APMadYXKfQ4rZB4,281889
17
17
  batplot/plotting.py,sha256=hG2_EdDhF1Qpn1XfZKdCQ5-w_m9gUYFbr804UQ5QjsU,10841
18
18
  batplot/readers.py,sha256=kAI0AvYrdfGRZkvADJ4riN96IWtrH24aAoZpBtONTbw,112960
19
- batplot/session.py,sha256=5PVxu8Q7T6Fonq0zgvUD3SkoN-9QnlBa8l3_R02B_K0,120820
19
+ batplot/session.py,sha256=3oo0jimTEdK7HpcBb326F8iBCM_LKPwNaSnmqSKU92Y,121025
20
20
  batplot/style.py,sha256=wmg4D4LdunLU88YYzh9y2REwet6xc5Ox2naa1ZW2OlM,58276
21
21
  batplot/ui.py,sha256=MIY2x_ghCYxjdYhjMUZsMMnQEUBLgrIT37hfPGZf_cs,36320
22
22
  batplot/utils.py,sha256=3dBZALWiCu5c6uc5MBII7n8329BZjieTEw4qithTlow,33939
23
23
  batplot/version_check.py,sha256=OG4LuHo5-rSqLLHQo5nWbX9lbNq6NyxRdvVUUcJRBqQ,6219
24
24
  batplot/data/USER_MANUAL.md,sha256=VYPvNZt3Fy8Z4Izr2FnQBw9vEaFTPkybhHDnF-OuKws,17694
25
- batplot-1.7.21.dist-info/licenses/LICENSE,sha256=2PAnHeCiTfgI7aKZLWr0G56HI9fGKQ0CEbQ02H-yExQ,1065
25
+ batplot-1.7.23.dist-info/licenses/LICENSE,sha256=2PAnHeCiTfgI7aKZLWr0G56HI9fGKQ0CEbQ02H-yExQ,1065
26
26
  batplot_backup_20251121_223043/__init__.py,sha256=3s2DUQuTbWs65hoN9cQQ8IiJbaFJY8fNxiCpwRBYoOA,118
27
27
  batplot_backup_20251121_223043/args.py,sha256=OH-h84QhN-IhMS8sPAsSEqccHD3wpeMgmXa_fqv5xtg,21215
28
28
  batplot_backup_20251121_223043/batch.py,sha256=oI7PONJyciHDOqNPq-8fnOQMyn9CpAdVznKaEdsy0ig,48650
@@ -45,8 +45,8 @@ batplot_backup_20251121_223043/style.py,sha256=xg-tj6bEbFUVjjxYMokiLehS4tSfKanLI
45
45
  batplot_backup_20251121_223043/ui.py,sha256=K0XZWyiuBRNkFod9mgZyJ9CLN78GR1-hh6EznnIb5S8,31208
46
46
  batplot_backup_20251121_223043/utils.py,sha256=jydA0JxsCWWAudXEwSjlxTG17y2F8U6hIAukAzi1P0g,32526
47
47
  batplot_backup_20251121_223043/version_check.py,sha256=vlHkGkgUJcD_Z4KZmwonxZvKZh0MwHLaBSxaLPc66AQ,4555
48
- batplot-1.7.21.dist-info/METADATA,sha256=0WwHb-tIxYhlZipOJoofws5omkviVSzKyNq8o9s7hVM,6137
49
- batplot-1.7.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
- batplot-1.7.21.dist-info/entry_points.txt,sha256=73GgH3Zs-qGIvgiyQLgGsSW-ryOwPPKHveOW6TDIR5Q,82
51
- batplot-1.7.21.dist-info/top_level.txt,sha256=CgqK4RpsYnUFAcqO4bLOnEhCoPY4IPEGLPkiDlzLIxg,39
52
- batplot-1.7.21.dist-info/RECORD,,
48
+ batplot-1.7.23.dist-info/METADATA,sha256=9CLx5Pt0Lrcu3244DqSt7DjHP8zO9FfqsGh-nFmvtfs,6188
49
+ batplot-1.7.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
+ batplot-1.7.23.dist-info/entry_points.txt,sha256=73GgH3Zs-qGIvgiyQLgGsSW-ryOwPPKHveOW6TDIR5Q,82
51
+ batplot-1.7.23.dist-info/top_level.txt,sha256=CgqK4RpsYnUFAcqO4bLOnEhCoPY4IPEGLPkiDlzLIxg,39
52
+ batplot-1.7.23.dist-info/RECORD,,