batplot 1.7.21__tar.gz → 1.7.23__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.
- {batplot-1.7.21/batplot.egg-info → batplot-1.7.23}/PKG-INFO +4 -2
- {batplot-1.7.21 → batplot-1.7.23}/README.md +3 -1
- {batplot-1.7.21/batplot/data → batplot-1.7.23}/USER_MANUAL.md +2 -2
- {batplot-1.7.21 → batplot-1.7.23}/batplot/__init__.py +1 -1
- {batplot-1.7.21 → batplot-1.7.23}/batplot/args.py +70 -68
- {batplot-1.7.21 → batplot-1.7.23}/batplot/batplot.py +2 -3
- {batplot-1.7.21 → batplot-1.7.23}/batplot/cpc_interactive.py +71 -8
- {batplot-1.7.21 → batplot-1.7.23}/batplot/electrochem_interactive.py +173 -19
- {batplot-1.7.21 → batplot-1.7.23}/batplot/operando.py +3 -15
- {batplot-1.7.21 → batplot-1.7.23}/batplot/operando_ec_interactive.py +83 -51
- {batplot-1.7.21 → batplot-1.7.23}/batplot/session.py +8 -1
- {batplot-1.7.21 → batplot-1.7.23/batplot.egg-info}/PKG-INFO +4 -2
- {batplot-1.7.21 → batplot-1.7.23}/pyproject.toml +1 -1
- {batplot-1.7.21 → batplot-1.7.23}/LICENSE +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/MANIFEST.in +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/batch.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/cif.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/cli.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/color_utils.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/config.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/converters.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23/batplot/data}/USER_MANUAL.md +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/interactive.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/manual.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/modes.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/plotting.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/readers.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/style.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/ui.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/utils.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot/version_check.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/__init__.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/args.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/batch.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/batplot.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/cif.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/cli.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/color_utils.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/config.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/converters.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/interactive.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/modes.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/operando.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/plotting.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/readers.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/session.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/style.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/ui.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/utils.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/batplot_backup_20251121_223043/version_check.py +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/setup.cfg +0 -0
- {batplot-1.7.21 → batplot-1.7.23}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.7.
|
|
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
|
|
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"
|
|
@@ -141,7 +141,9 @@ See [LICENSE](LICENSE)
|
|
|
141
141
|
|
|
142
142
|
## Author & Contact
|
|
143
143
|
|
|
144
|
-
Tian Dai
|
|
144
|
+
Tian Dai
|
|
145
|
+
tianda@uio.no
|
|
145
146
|
University of Oslo
|
|
147
|
+
https://www.mn.uio.no/kjemi/english/people/aca/tianda/
|
|
146
148
|
|
|
147
149
|
**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"
|
|
@@ -456,8 +456,8 @@ If no `.mpt` file is present, operando mode displays only the contour plot. The
|
|
|
456
456
|
|
|
457
457
|
For questions, bug reports, or feature requests:
|
|
458
458
|
|
|
459
|
-
|
|
459
|
+
Tian Dai
|
|
460
460
|
- **Email**: tianda@uio.no
|
|
461
461
|
- **Mailing List**: Subscribe to batplot-lab@kjemi.uio.no for updates, feature announcements, and community discussions
|
|
462
462
|
|
|
463
|
-
Feel free to
|
|
463
|
+
Feel free to reach out via email!
|
|
@@ -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 --
|
|
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
|
|
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.
|
|
68
|
-
Output: "[green]batplot[/green] [yellow]file.
|
|
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
|
|
137
|
-
" • EC: GC/CPC/dQdV/CV
|
|
138
|
-
" • Operando: contour maps from a folder of
|
|
139
|
-
" • Batch: export
|
|
140
|
-
" • Interactive mode: --i / --interactive flag opens a menu for styling, ranges,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 (
|
|
200
|
-
"Supported files: .xye .xy .qye .dat .csv .gr .nor .chik .chir .txt
|
|
201
|
-
"Axis detection: .qye→Q, .gr→r, .nor→energy, .chik→k, .chir→r, else use --xaxis (Q, 2theta, r, k, energy,
|
|
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 --
|
|
206
|
-
" batplot a.dat b.xy --wl 1.54 --
|
|
207
|
-
" batplot pattern.qye ticks.cif --
|
|
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"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
"""batplot - Interactive plotting for
|
|
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
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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).")
|
|
@@ -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
|
|
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
|
-
-
|
|
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
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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
|
-
|
|
2109
|
-
spine.
|
|
2110
|
-
|
|
2111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
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:
|
|
@@ -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=
|
|
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.
|
|
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
|
|
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"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "batplot"
|
|
7
|
-
version = "1.7.
|
|
7
|
+
version = "1.7.23"
|
|
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
|
|
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
|
|
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
|