batplot 1.8.0__py3-none-any.whl → 1.8.1__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 +1 -1
- batplot/args.py +3 -3
- batplot/cpc_interactive.py +86 -3
- batplot/electrochem_interactive.py +28 -0
- batplot/interactive.py +8 -2
- batplot/operando.py +2 -0
- batplot/operando_ec_interactive.py +116 -15
- batplot/session.py +18 -1
- batplot/utils.py +40 -0
- batplot/version_check.py +85 -6
- {batplot-1.8.0.dist-info → batplot-1.8.1.dist-info}/METADATA +1 -1
- {batplot-1.8.0.dist-info → batplot-1.8.1.dist-info}/RECORD +16 -16
- {batplot-1.8.0.dist-info → batplot-1.8.1.dist-info}/WHEEL +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.1.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.1.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.1.dist-info}/top_level.txt +0 -0
batplot/__init__.py
CHANGED
batplot/args.py
CHANGED
|
@@ -169,7 +169,7 @@ def _print_general_help() -> None:
|
|
|
169
169
|
" batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
|
|
170
170
|
" batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
|
|
171
171
|
" [Operando]\n"
|
|
172
|
-
" batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)
|
|
172
|
+
" batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)\n\n"
|
|
173
173
|
"Features:\n"
|
|
174
174
|
" • Quick plotting with sensible defaults, no config files needed\n"
|
|
175
175
|
" • Supports many common file formats (see -h xy/ec/op)\n"
|
|
@@ -183,9 +183,9 @@ def _print_general_help() -> None:
|
|
|
183
183
|
" batplot -h xy # XY file plotting guide\n"
|
|
184
184
|
" batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
|
|
185
185
|
" batplot -h op # Operando guide\n"
|
|
186
|
-
" batplot -m # Open the illustrated txt manual with highlights\n"
|
|
186
|
+
" batplot -m # Open the illustrated txt manual with highlights\n\n"
|
|
187
187
|
|
|
188
|
-
"Contact & Updates:\n
|
|
188
|
+
"Contact & Updates:\n"
|
|
189
189
|
" Subscribe to batplot-lab@kjemi.uio.no for updates\n"
|
|
190
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
191
|
" Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
|
batplot/cpc_interactive.py
CHANGED
|
@@ -86,6 +86,7 @@ from .ui import (
|
|
|
86
86
|
from .utils import (
|
|
87
87
|
_confirm_overwrite,
|
|
88
88
|
choose_save_path,
|
|
89
|
+
convert_label_shortcuts,
|
|
89
90
|
choose_style_file,
|
|
90
91
|
list_files_in_subdirectory,
|
|
91
92
|
get_organized_path,
|
|
@@ -335,13 +336,36 @@ def _rebuild_legend(ax, ax2, file_data, preserve_position=True):
|
|
|
335
336
|
"""
|
|
336
337
|
try:
|
|
337
338
|
fig = ax.figure
|
|
338
|
-
# Get stored position before rebuilding
|
|
339
|
+
# Get stored position before rebuilding. If none is stored yet, try to
|
|
340
|
+
# capture the current on-canvas position once so subsequent rebuilds
|
|
341
|
+
# (e.g., after renaming) do not jump to a new "best" location.
|
|
339
342
|
xy_in = None
|
|
340
343
|
if preserve_position:
|
|
341
344
|
try:
|
|
342
345
|
xy_in = getattr(fig, '_cpc_legend_xy_in', None)
|
|
343
346
|
except Exception:
|
|
344
|
-
|
|
347
|
+
xy_in = None
|
|
348
|
+
if xy_in is None:
|
|
349
|
+
try:
|
|
350
|
+
leg0 = ax.get_legend()
|
|
351
|
+
if leg0 is not None and leg0.get_visible():
|
|
352
|
+
try:
|
|
353
|
+
renderer = fig.canvas.get_renderer()
|
|
354
|
+
except Exception:
|
|
355
|
+
fig.canvas.draw()
|
|
356
|
+
renderer = fig.canvas.get_renderer()
|
|
357
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
358
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
359
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
360
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
361
|
+
fw, fh = fig.get_size_inches()
|
|
362
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
363
|
+
offset = _sanitize_legend_offset(offset)
|
|
364
|
+
if offset is not None:
|
|
365
|
+
fig._cpc_legend_xy_in = offset
|
|
366
|
+
xy_in = offset
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
345
369
|
|
|
346
370
|
h1, l1 = ax.get_legend_handles_labels()
|
|
347
371
|
h2, l2 = ax2.get_legend_handles_labels()
|
|
@@ -1643,6 +1667,31 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1643
1667
|
else:
|
|
1644
1668
|
# Single file mode: toggle efficiency
|
|
1645
1669
|
push_state("visibility-eff")
|
|
1670
|
+
# Capture current legend position BEFORE toggling visibility
|
|
1671
|
+
try:
|
|
1672
|
+
if not hasattr(fig, '_cpc_legend_xy_in') or getattr(fig, '_cpc_legend_xy_in') is None:
|
|
1673
|
+
leg0 = ax.get_legend()
|
|
1674
|
+
if leg0 is not None and leg0.get_visible():
|
|
1675
|
+
try:
|
|
1676
|
+
# Ensure renderer exists
|
|
1677
|
+
try:
|
|
1678
|
+
renderer = fig.canvas.get_renderer()
|
|
1679
|
+
except Exception:
|
|
1680
|
+
fig.canvas.draw()
|
|
1681
|
+
renderer = fig.canvas.get_renderer()
|
|
1682
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
1683
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
1684
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
1685
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
1686
|
+
fw, fh = fig.get_size_inches()
|
|
1687
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
1688
|
+
offset = _sanitize_legend_offset(offset)
|
|
1689
|
+
if offset is not None:
|
|
1690
|
+
fig._cpc_legend_xy_in = offset
|
|
1691
|
+
except Exception:
|
|
1692
|
+
pass
|
|
1693
|
+
except Exception:
|
|
1694
|
+
pass
|
|
1646
1695
|
vis = sc_eff.get_visible()
|
|
1647
1696
|
sc_eff.set_visible(not vis)
|
|
1648
1697
|
try:
|
|
@@ -1650,7 +1699,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1650
1699
|
except Exception:
|
|
1651
1700
|
pass
|
|
1652
1701
|
|
|
1653
|
-
_rebuild_legend(ax, ax2, file_data)
|
|
1702
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
1654
1703
|
fig.canvas.draw_idle()
|
|
1655
1704
|
except ValueError:
|
|
1656
1705
|
print("Invalid input.")
|
|
@@ -1676,6 +1725,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1676
1725
|
elif key == 'c':
|
|
1677
1726
|
# Colors submenu: ly (left Y series) and ry (right Y efficiency), with user colors and palettes
|
|
1678
1727
|
try:
|
|
1728
|
+
# Note: Individual series may use different colors, so we can't show a single "current" palette
|
|
1679
1729
|
# Use same palettes as EC interactive
|
|
1680
1730
|
palette_opts = ['tab10', 'Set2', 'Dark2', 'viridis', 'plasma']
|
|
1681
1731
|
def _palette_color(name, idx=0, total=1, default_val=0.4):
|
|
@@ -2668,6 +2718,33 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2668
2718
|
try:
|
|
2669
2719
|
push_state("toggle-eff")
|
|
2670
2720
|
|
|
2721
|
+
# Capture current legend position BEFORE toggling visibility
|
|
2722
|
+
# This ensures the position is preserved when legend is rebuilt
|
|
2723
|
+
try:
|
|
2724
|
+
if not hasattr(fig, '_cpc_legend_xy_in') or getattr(fig, '_cpc_legend_xy_in') is None:
|
|
2725
|
+
leg0 = ax.get_legend()
|
|
2726
|
+
if leg0 is not None and leg0.get_visible():
|
|
2727
|
+
try:
|
|
2728
|
+
# Ensure renderer exists
|
|
2729
|
+
try:
|
|
2730
|
+
renderer = fig.canvas.get_renderer()
|
|
2731
|
+
except Exception:
|
|
2732
|
+
fig.canvas.draw()
|
|
2733
|
+
renderer = fig.canvas.get_renderer()
|
|
2734
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
2735
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
2736
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
2737
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
2738
|
+
fw, fh = fig.get_size_inches()
|
|
2739
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
2740
|
+
offset = _sanitize_legend_offset(offset)
|
|
2741
|
+
if offset is not None:
|
|
2742
|
+
fig._cpc_legend_xy_in = offset
|
|
2743
|
+
except Exception:
|
|
2744
|
+
pass
|
|
2745
|
+
except Exception:
|
|
2746
|
+
pass
|
|
2747
|
+
|
|
2671
2748
|
# Determine current visibility state (check if any efficiency is visible)
|
|
2672
2749
|
if is_multi_file:
|
|
2673
2750
|
# In multi-file mode, check if any efficiency is visible
|
|
@@ -3738,6 +3815,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3738
3815
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
3739
3816
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
3740
3817
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3818
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
3741
3819
|
while True:
|
|
3742
3820
|
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3743
3821
|
sub = _safe_input("Rename> ").strip().lower()
|
|
@@ -3788,6 +3866,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3788
3866
|
print(f"Current file name in legend: '{base_name}'")
|
|
3789
3867
|
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3790
3868
|
if new_name and new_name.lower() != 'q':
|
|
3869
|
+
new_name = convert_label_shortcuts(new_name)
|
|
3791
3870
|
try:
|
|
3792
3871
|
push_state("rename-legend")
|
|
3793
3872
|
|
|
@@ -3895,6 +3974,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3895
3974
|
print(f"Current file name in legend: '{base_name}'")
|
|
3896
3975
|
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3897
3976
|
if new_name and new_name.lower() != 'q':
|
|
3977
|
+
new_name = convert_label_shortcuts(new_name)
|
|
3898
3978
|
try:
|
|
3899
3979
|
push_state("rename-legend")
|
|
3900
3980
|
|
|
@@ -3966,6 +4046,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3966
4046
|
print(f"Current x-axis title: '{current}'")
|
|
3967
4047
|
new_title = _safe_input("Enter new x-axis title (q=cancel): ")
|
|
3968
4048
|
if new_title and new_title.lower() != 'q':
|
|
4049
|
+
new_title = convert_label_shortcuts(new_title)
|
|
3969
4050
|
try:
|
|
3970
4051
|
push_state("rename-x")
|
|
3971
4052
|
ax.set_xlabel(new_title)
|
|
@@ -3985,6 +4066,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3985
4066
|
print(f"Current left y-axis title: '{current}'")
|
|
3986
4067
|
new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
|
|
3987
4068
|
if new_title and new_title.lower() != 'q':
|
|
4069
|
+
new_title = convert_label_shortcuts(new_title)
|
|
3988
4070
|
try:
|
|
3989
4071
|
push_state("rename-ly")
|
|
3990
4072
|
ax.set_ylabel(new_title)
|
|
@@ -3999,6 +4081,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3999
4081
|
print(f"Current right y-axis title: '{current}'")
|
|
4000
4082
|
new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
|
|
4001
4083
|
if new_title and new_title.lower() != 'q':
|
|
4084
|
+
new_title = convert_label_shortcuts(new_title)
|
|
4002
4085
|
try:
|
|
4003
4086
|
push_state("rename-ry")
|
|
4004
4087
|
ax2.set_ylabel(new_title)
|
|
@@ -31,6 +31,7 @@ from .utils import (
|
|
|
31
31
|
choose_style_file,
|
|
32
32
|
list_files_in_subdirectory,
|
|
33
33
|
get_organized_path,
|
|
34
|
+
convert_label_shortcuts,
|
|
34
35
|
)
|
|
35
36
|
import time
|
|
36
37
|
from .color_utils import (
|
|
@@ -502,6 +503,28 @@ def _rebuild_legend(ax):
|
|
|
502
503
|
fig = ax.figure
|
|
503
504
|
# Capture existing title before any rebuild so it isn't lost
|
|
504
505
|
_store_legend_title(fig, ax)
|
|
506
|
+
# If no stored position yet, try to capture the current legend location once
|
|
507
|
+
# so rebuilds (e.g., after renaming) don't jump to a new "best" spot.
|
|
508
|
+
try:
|
|
509
|
+
if getattr(fig, '_ec_legend_xy_in', None) is None:
|
|
510
|
+
leg0 = ax.get_legend()
|
|
511
|
+
if leg0 is not None and leg0.get_visible():
|
|
512
|
+
try:
|
|
513
|
+
renderer = fig.canvas.get_renderer()
|
|
514
|
+
except Exception:
|
|
515
|
+
fig.canvas.draw()
|
|
516
|
+
renderer = fig.canvas.get_renderer()
|
|
517
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
518
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
519
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
520
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
521
|
+
fw, fh = fig.get_size_inches()
|
|
522
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
523
|
+
offset = _sanitize_legend_offset(fig, offset)
|
|
524
|
+
if offset is not None:
|
|
525
|
+
fig._ec_legend_xy_in = offset
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
505
528
|
if not _get_legend_user_pref(fig):
|
|
506
529
|
leg = ax.get_legend()
|
|
507
530
|
if leg is not None:
|
|
@@ -2697,6 +2720,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2697
2720
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2698
2721
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2699
2722
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2723
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2700
2724
|
while True:
|
|
2701
2725
|
print("Rename axis: x, y, both, q=back")
|
|
2702
2726
|
sub = _safe_input("Rename> ").strip().lower()
|
|
@@ -2707,6 +2731,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2707
2731
|
if sub in ('x','both'):
|
|
2708
2732
|
txt = _safe_input("New X-axis label (blank=cancel): ")
|
|
2709
2733
|
if txt:
|
|
2734
|
+
txt = convert_label_shortcuts(txt)
|
|
2710
2735
|
push_state("rename-x")
|
|
2711
2736
|
try:
|
|
2712
2737
|
# Freeze layout and preserve existing pad for one-shot restore
|
|
@@ -2730,6 +2755,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2730
2755
|
if sub in ('y','both'):
|
|
2731
2756
|
txt = _safe_input("New Y-axis label (blank=cancel): ")
|
|
2732
2757
|
if txt:
|
|
2758
|
+
txt = convert_label_shortcuts(txt)
|
|
2733
2759
|
push_state("rename-y")
|
|
2734
2760
|
base_ylabel = txt
|
|
2735
2761
|
try:
|
|
@@ -3027,6 +3053,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3027
3053
|
_print_menu(len(all_cycles), is_dqdv)
|
|
3028
3054
|
continue
|
|
3029
3055
|
elif key == 'c':
|
|
3056
|
+
# Show current palette if one is applied (this is informational only)
|
|
3057
|
+
# Note: Individual cycles may use different colors, so we can't show a single "current" palette
|
|
3030
3058
|
print(f"Total cycles: {len(all_cycles)}")
|
|
3031
3059
|
print("Enter one of:")
|
|
3032
3060
|
print(_colorize_inline_commands(" - numbers: e.g. 1 5 10"))
|
batplot/interactive.py
CHANGED
|
@@ -25,6 +25,7 @@ from .utils import (
|
|
|
25
25
|
choose_save_path,
|
|
26
26
|
choose_style_file,
|
|
27
27
|
list_files_in_subdirectory,
|
|
28
|
+
convert_label_shortcuts,
|
|
28
29
|
get_organized_path,
|
|
29
30
|
)
|
|
30
31
|
import time
|
|
@@ -1955,12 +1956,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1955
1956
|
ax._cif_draw_func()
|
|
1956
1957
|
fig.canvas.draw()
|
|
1957
1958
|
elif sub == 'p':
|
|
1959
|
+
# Show current palette if one is applied
|
|
1958
1960
|
history = getattr(fig, '_curve_palette_history', [])
|
|
1959
1961
|
current_palette = history[-1]['palette'] if history else None
|
|
1960
1962
|
if current_palette:
|
|
1961
1963
|
print(f"Current palette: {current_palette}")
|
|
1962
|
-
else:
|
|
1963
|
-
print("Current palette: manual/custom")
|
|
1964
1964
|
base_palettes = ['viridis', 'cividis', 'plasma', 'inferno', 'magma', 'batlow']
|
|
1965
1965
|
extras = []
|
|
1966
1966
|
def _palette_available(name: str) -> bool:
|
|
@@ -2211,6 +2211,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2211
2211
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2212
2212
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2213
2213
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2214
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2214
2215
|
idx_in = _safe_input("Curve number to rename (q=cancel): ").strip()
|
|
2215
2216
|
if not idx_in or idx_in.lower() == 'q':
|
|
2216
2217
|
print("Canceled.")
|
|
@@ -2227,6 +2228,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2227
2228
|
if not new_label or new_label.lower() == 'q':
|
|
2228
2229
|
print("Canceled.")
|
|
2229
2230
|
continue
|
|
2231
|
+
new_label = convert_label_shortcuts(new_label)
|
|
2230
2232
|
push_state("rename-curve")
|
|
2231
2233
|
labels[idx] = new_label
|
|
2232
2234
|
label_text_objects[idx].set_text(f"{idx+1}: {new_label}")
|
|
@@ -2250,9 +2252,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2250
2252
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2251
2253
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2252
2254
|
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2255
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2253
2256
|
new_name = _safe_input("New CIF tick label (q=cancel): ")
|
|
2254
2257
|
if not new_name or new_name.lower()=='q':
|
|
2255
2258
|
print("Canceled."); continue
|
|
2259
|
+
new_name = convert_label_shortcuts(new_name)
|
|
2256
2260
|
lab,fname,peaksQ,wl,qmax_sim,color = cts[idx]
|
|
2257
2261
|
# Suspend extension while updating label
|
|
2258
2262
|
if _bp is not None:
|
|
@@ -2278,10 +2282,12 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2278
2282
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2279
2283
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2280
2284
|
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2285
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2281
2286
|
new_axis = _safe_input("New axis label: ")
|
|
2282
2287
|
if not new_axis or new_axis.lower() == 'q':
|
|
2283
2288
|
print("Canceled.")
|
|
2284
2289
|
continue
|
|
2290
|
+
new_axis = convert_label_shortcuts(new_axis)
|
|
2285
2291
|
new_axis = normalize_label_text(new_axis)
|
|
2286
2292
|
push_state("rename-axis")
|
|
2287
2293
|
# Freeze layout and preserve current pad via one-shot pending to avoid drift
|
batplot/operando.py
CHANGED
|
@@ -315,6 +315,8 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
315
315
|
extent = (grid_x.min(), grid_x.max(), 0, Zm.shape[0]-1)
|
|
316
316
|
# Bottom-to-top visual order (scan 0 at bottom) to match EC time progression -> origin='lower'
|
|
317
317
|
im = ax.imshow(Zm, aspect='auto', origin='lower', extent=extent, cmap='viridis', interpolation='nearest')
|
|
318
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
319
|
+
setattr(im, '_operando_cmap_name', 'viridis')
|
|
318
320
|
# Create custom colorbar axes on the left (will be positioned by layout function)
|
|
319
321
|
# Create a dummy axes that will be replaced by the custom colorbar in interactive menu
|
|
320
322
|
cbar_ax = fig.add_axes([0.0, 0.0, 0.01, 0.01]) # Temporary position, will be repositioned
|
|
@@ -39,7 +39,7 @@ from .color_utils import (
|
|
|
39
39
|
resolve_color_token,
|
|
40
40
|
manage_user_colors,
|
|
41
41
|
)
|
|
42
|
-
from .utils import choose_style_file
|
|
42
|
+
from .utils import choose_style_file, convert_label_shortcuts
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class _FilterIMKWarning:
|
|
@@ -354,7 +354,7 @@ def _draw_custom_colorbar(cbar_ax, im, label='Intensity', label_mode='normal'):
|
|
|
354
354
|
|
|
355
355
|
def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
|
|
356
356
|
"""Update the custom colorbar when colormap or limits change.
|
|
357
|
-
|
|
357
|
+
|
|
358
358
|
Args:
|
|
359
359
|
cbar_ax: Axes object containing the colorbar
|
|
360
360
|
im: Optional AxesImage object (if None, uses stored reference)
|
|
@@ -365,17 +365,82 @@ def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
|
|
|
365
365
|
im = getattr(cbar_ax, '_colorbar_im', None)
|
|
366
366
|
if im is None:
|
|
367
367
|
return
|
|
368
|
-
|
|
368
|
+
|
|
369
369
|
if label is None:
|
|
370
370
|
label = getattr(cbar_ax, '_colorbar_label', 'Intensity')
|
|
371
|
-
|
|
371
|
+
|
|
372
372
|
if label_mode is None:
|
|
373
373
|
label_mode = getattr(cbar_ax, '_colorbar_label_mode', 'normal')
|
|
374
|
-
|
|
374
|
+
|
|
375
375
|
# Redraw the colorbar
|
|
376
376
|
_draw_custom_colorbar(cbar_ax, im, label, label_mode)
|
|
377
377
|
|
|
378
378
|
|
|
379
|
+
def _safe_set_clim(im, vmin, vmax):
|
|
380
|
+
"""Safely set color limits without triggering matplotlib colorbar callbacks.
|
|
381
|
+
|
|
382
|
+
This wrapper around im.set_clim() prevents the NotImplementedError: cannot remove artist
|
|
383
|
+
error when using custom colorbars. The issue occurs because matplotlib's callback system
|
|
384
|
+
tries to update the colorbar when set_clim() is called, but our custom colorbar drawing
|
|
385
|
+
has already cleared the axes, causing the update to fail.
|
|
386
|
+
|
|
387
|
+
This function suppresses the callback traceback by redirecting stderr to a null device
|
|
388
|
+
during the set_clim() call, preventing matplotlib's callback system from printing
|
|
389
|
+
tracebacks to the terminal.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
im: AxesImage object
|
|
393
|
+
vmin: Minimum value for color scale
|
|
394
|
+
vmax: Maximum value for color scale
|
|
395
|
+
"""
|
|
396
|
+
import sys
|
|
397
|
+
import os
|
|
398
|
+
from io import StringIO
|
|
399
|
+
|
|
400
|
+
# Create a null device for stderr redirection
|
|
401
|
+
class NullDevice:
|
|
402
|
+
def write(self, s):
|
|
403
|
+
pass
|
|
404
|
+
def flush(self):
|
|
405
|
+
pass
|
|
406
|
+
def close(self):
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
# Suppress matplotlib's exception printing by redirecting stderr AND excepthook
|
|
410
|
+
old_stderr = sys.stderr
|
|
411
|
+
old_excepthook = sys.excepthook
|
|
412
|
+
null_dev = NullDevice()
|
|
413
|
+
|
|
414
|
+
# Create a no-op excepthook that suppresses all exceptions
|
|
415
|
+
def suppress_excepthook(exc_type, exc_value, exc_traceback):
|
|
416
|
+
# Only suppress if it's the specific error we're looking for
|
|
417
|
+
if exc_type == NotImplementedError and 'cannot remove artist' in str(exc_value).lower():
|
|
418
|
+
return # Suppress this specific exception
|
|
419
|
+
# For any other exception, use the original handler
|
|
420
|
+
old_excepthook(exc_type, exc_value, exc_traceback)
|
|
421
|
+
|
|
422
|
+
sys.stderr = null_dev
|
|
423
|
+
sys.excepthook = suppress_excepthook
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
# Set the color limits - matplotlib's callback will try to print traceback
|
|
427
|
+
# but both stderr and excepthook are suppressed
|
|
428
|
+
im.set_clim(vmin, vmax)
|
|
429
|
+
# The operation succeeds; any tracebacks are suppressed
|
|
430
|
+
except NotImplementedError:
|
|
431
|
+
# Suppress the specific error - color limits were still updated successfully
|
|
432
|
+
pass
|
|
433
|
+
except Exception:
|
|
434
|
+
# For any other unexpected error, restore handlers and re-raise
|
|
435
|
+
sys.stderr = old_stderr
|
|
436
|
+
sys.excepthook = old_excepthook
|
|
437
|
+
raise
|
|
438
|
+
finally:
|
|
439
|
+
# Always restore both stderr and excepthook
|
|
440
|
+
sys.stderr = old_stderr
|
|
441
|
+
sys.excepthook = old_excepthook
|
|
442
|
+
|
|
443
|
+
|
|
379
444
|
def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
|
|
380
445
|
"""Detach a Matplotlib Colorbar from its mappable callbacks.
|
|
381
446
|
|
|
@@ -737,7 +802,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
737
802
|
|
|
738
803
|
if intensity_max > intensity_min:
|
|
739
804
|
# Update color limits
|
|
740
|
-
im
|
|
805
|
+
_safe_set_clim(im, intensity_min, intensity_max)
|
|
741
806
|
|
|
742
807
|
# Update colorbar if available
|
|
743
808
|
try:
|
|
@@ -1111,7 +1176,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1111
1176
|
clim = im.get_clim()
|
|
1112
1177
|
except Exception:
|
|
1113
1178
|
clim = None
|
|
1114
|
-
|
|
1179
|
+
# Get colormap name: first check if we stored it explicitly, otherwise try to get from colormap object
|
|
1180
|
+
cmap_name = getattr(im, '_operando_cmap_name', None)
|
|
1181
|
+
if cmap_name is None:
|
|
1182
|
+
cmap_name = getattr(im.get_cmap(), 'name', None)
|
|
1115
1183
|
# EC mode and caches (only if ec_ax exists)
|
|
1116
1184
|
if ec_ax is not None:
|
|
1117
1185
|
mode = getattr(ec_ax, '_ec_y_mode', 'time')
|
|
@@ -1325,12 +1393,15 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1325
1393
|
cbar.solids = None
|
|
1326
1394
|
except Exception:
|
|
1327
1395
|
pass
|
|
1328
|
-
lo, hi = snap['clim']; im
|
|
1396
|
+
lo, hi = snap['clim']; _safe_set_clim(im, float(lo), float(hi))
|
|
1329
1397
|
except Exception:
|
|
1330
1398
|
pass
|
|
1331
1399
|
try:
|
|
1332
1400
|
if snap.get('cmap'):
|
|
1333
|
-
|
|
1401
|
+
cmap_name = snap['cmap']
|
|
1402
|
+
im.set_cmap(cmap_name)
|
|
1403
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
1404
|
+
setattr(im, '_operando_cmap_name', cmap_name)
|
|
1334
1405
|
if cbar is not None:
|
|
1335
1406
|
_update_custom_colorbar(cbar.ax, im)
|
|
1336
1407
|
except Exception:
|
|
@@ -2023,8 +2094,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2023
2094
|
continue
|
|
2024
2095
|
except ValueError:
|
|
2025
2096
|
print("Invalid input. Enter a number.")
|
|
2097
|
+
continue
|
|
2026
2098
|
except Exception as e:
|
|
2027
2099
|
print(f"Error: {e}")
|
|
2100
|
+
continue
|
|
2028
2101
|
elif sub == 'e':
|
|
2029
2102
|
if ec_ax is None:
|
|
2030
2103
|
print("EC panel not available.")
|
|
@@ -2042,10 +2115,13 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2042
2115
|
continue
|
|
2043
2116
|
except ValueError:
|
|
2044
2117
|
print("Invalid input. Enter a number.")
|
|
2118
|
+
continue
|
|
2045
2119
|
except Exception as e:
|
|
2046
2120
|
print(f"Error: {e}")
|
|
2121
|
+
continue
|
|
2047
2122
|
else:
|
|
2048
2123
|
print("Invalid choice.")
|
|
2124
|
+
continue
|
|
2049
2125
|
elif choice != 'q':
|
|
2050
2126
|
print("Invalid choice")
|
|
2051
2127
|
else:
|
|
@@ -2107,10 +2183,13 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2107
2183
|
continue
|
|
2108
2184
|
except ValueError:
|
|
2109
2185
|
print("Invalid input. Enter a number.")
|
|
2186
|
+
continue
|
|
2110
2187
|
except Exception as e:
|
|
2111
2188
|
print(f"Error: {e}")
|
|
2189
|
+
continue
|
|
2112
2190
|
else:
|
|
2113
2191
|
print("Invalid choice.")
|
|
2192
|
+
continue
|
|
2114
2193
|
elif choice != 'q':
|
|
2115
2194
|
print("Invalid choice")
|
|
2116
2195
|
fig.canvas.draw_idle()
|
|
@@ -3280,7 +3359,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3280
3359
|
print("Invalid value, ignored.")
|
|
3281
3360
|
continue
|
|
3282
3361
|
_snapshot("operando-intensity-range")
|
|
3283
|
-
im
|
|
3362
|
+
_safe_set_clim(im, cur[0], new_upper)
|
|
3284
3363
|
try:
|
|
3285
3364
|
if cbar is not None:
|
|
3286
3365
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3306,7 +3385,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3306
3385
|
print("Invalid value, ignored.")
|
|
3307
3386
|
continue
|
|
3308
3387
|
_snapshot("operando-intensity-range")
|
|
3309
|
-
im
|
|
3388
|
+
_safe_set_clim(im, new_lower, cur[1])
|
|
3310
3389
|
try:
|
|
3311
3390
|
if cbar is not None:
|
|
3312
3391
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3320,7 +3399,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3320
3399
|
if line.lower() == 'a':
|
|
3321
3400
|
# Apply auto-normalization to visible data
|
|
3322
3401
|
if auto_available:
|
|
3323
|
-
im
|
|
3402
|
+
_safe_set_clim(im, auto_lo, auto_hi)
|
|
3324
3403
|
try:
|
|
3325
3404
|
if cbar is not None:
|
|
3326
3405
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3332,7 +3411,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3332
3411
|
print("Auto-fit unavailable: no finite data in visible area")
|
|
3333
3412
|
else:
|
|
3334
3413
|
lo, hi = map(float, line.split())
|
|
3335
|
-
im
|
|
3414
|
+
_safe_set_clim(im, lo, hi)
|
|
3336
3415
|
try:
|
|
3337
3416
|
if cbar is not None:
|
|
3338
3417
|
_update_custom_colorbar(cbar.ax, im)
|
|
@@ -3381,6 +3460,15 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3381
3460
|
print_menu()
|
|
3382
3461
|
elif cmd == 'oc':
|
|
3383
3462
|
# Change operando colormap (perceptually uniform suggestions)
|
|
3463
|
+
# Show current colormap if one is applied
|
|
3464
|
+
try:
|
|
3465
|
+
current_cmap = getattr(im, '_operando_cmap_name', None)
|
|
3466
|
+
if current_cmap is None:
|
|
3467
|
+
current_cmap = getattr(im.get_cmap(), 'name', None)
|
|
3468
|
+
if current_cmap:
|
|
3469
|
+
print(f"Current operando colormap: {current_cmap}")
|
|
3470
|
+
except Exception:
|
|
3471
|
+
pass
|
|
3384
3472
|
def _refresh_available():
|
|
3385
3473
|
return set(name.lower() for name in plt.colormaps())
|
|
3386
3474
|
available = _refresh_available()
|
|
@@ -3453,6 +3541,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3453
3541
|
if reversed_choice:
|
|
3454
3542
|
palette_obj = palette_obj.reversed()
|
|
3455
3543
|
im.set_cmap(palette_obj)
|
|
3544
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
3545
|
+
setattr(im, '_operando_cmap_name', choice)
|
|
3456
3546
|
try:
|
|
3457
3547
|
# Update custom colorbar with new colormap
|
|
3458
3548
|
if cbar is not None:
|
|
@@ -3489,7 +3579,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3489
3579
|
cb_w_in, cb_gap_in, ec_gap_in, ec_w_in, ax_w_in, ax_h_in = _ensure_fixed_params(fig, ax, cbar.ax, ec_ax)
|
|
3490
3580
|
fam = plt.rcParams.get('font.sans-serif', [''])[0]
|
|
3491
3581
|
fsize = plt.rcParams.get('font.size', None)
|
|
3492
|
-
|
|
3582
|
+
# Get colormap name: first check if we stored it explicitly, otherwise try to get from colormap object
|
|
3583
|
+
cmap_name = getattr(im, '_operando_cmap_name', None)
|
|
3584
|
+
if cmap_name is None:
|
|
3585
|
+
cmap_name = getattr(im.get_cmap(), 'name', None)
|
|
3493
3586
|
cb_vis = bool(cbar.ax.get_visible())
|
|
3494
3587
|
ec_vis = bool(ec_ax.get_visible()) if ec_ax is not None else None
|
|
3495
3588
|
cb_label_text = str(getattr(cbar.ax, '_colorbar_label', cbar.ax.get_ylabel() or 'Intensity'))
|
|
@@ -4056,6 +4149,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4056
4149
|
if cmap:
|
|
4057
4150
|
try:
|
|
4058
4151
|
im.set_cmap(cmap)
|
|
4152
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
4153
|
+
setattr(im, '_operando_cmap_name', cmap)
|
|
4059
4154
|
if cbar is not None:
|
|
4060
4155
|
_update_custom_colorbar(cbar.ax, im)
|
|
4061
4156
|
except Exception:
|
|
@@ -4329,7 +4424,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4329
4424
|
try:
|
|
4330
4425
|
intensity_range = op.get('intensity_range')
|
|
4331
4426
|
if intensity_range and isinstance(intensity_range, (list, tuple)) and len(intensity_range) == 2:
|
|
4332
|
-
im
|
|
4427
|
+
_safe_set_clim(im, float(intensity_range[0]), float(intensity_range[1]))
|
|
4333
4428
|
print(f"Applied intensity range: {intensity_range[0]:.4g} to {intensity_range[1]:.4g}")
|
|
4334
4429
|
except Exception as e:
|
|
4335
4430
|
print(f"Warning: Could not apply intensity range: {e}")
|
|
@@ -4623,6 +4718,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4623
4718
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
4624
4719
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
4625
4720
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
4721
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
4626
4722
|
while True:
|
|
4627
4723
|
sub = _safe_input("or> ").strip().lower()
|
|
4628
4724
|
if not sub:
|
|
@@ -4633,6 +4729,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4633
4729
|
cur = ax.get_xlabel() or ''
|
|
4634
4730
|
lab = _safe_input(f"New operando X label (blank=cancel, current='{cur}'): ")
|
|
4635
4731
|
if lab:
|
|
4732
|
+
lab = convert_label_shortcuts(lab)
|
|
4636
4733
|
_snapshot("rename-op-x")
|
|
4637
4734
|
try:
|
|
4638
4735
|
ax.set_xlabel(lab)
|
|
@@ -4646,6 +4743,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4646
4743
|
cur = ax.get_ylabel() or ''
|
|
4647
4744
|
lab = _safe_input(f"New operando Y label (blank=cancel, current='{cur}'): ")
|
|
4648
4745
|
if lab:
|
|
4746
|
+
lab = convert_label_shortcuts(lab)
|
|
4649
4747
|
_snapshot("rename-op-y")
|
|
4650
4748
|
try:
|
|
4651
4749
|
ax.set_ylabel(lab)
|
|
@@ -4675,6 +4773,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4675
4773
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
4676
4774
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
4677
4775
|
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
4776
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
4678
4777
|
while True:
|
|
4679
4778
|
sub = _safe_input("er> ").strip().lower()
|
|
4680
4779
|
if not sub:
|
|
@@ -4685,6 +4784,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4685
4784
|
cur = ec_ax.get_xlabel() or ''
|
|
4686
4785
|
lab = _safe_input(f"New EC X label (blank=cancel, current='{cur}'): ")
|
|
4687
4786
|
if lab:
|
|
4787
|
+
lab = convert_label_shortcuts(lab)
|
|
4688
4788
|
_snapshot("rename-ec-x")
|
|
4689
4789
|
try:
|
|
4690
4790
|
ec_ax.set_xlabel(lab)
|
|
@@ -4697,6 +4797,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
4697
4797
|
cur = ec_ax.get_ylabel() or ''
|
|
4698
4798
|
lab = _safe_input(f"New EC Y label (blank=cancel, current='{cur}'): ")
|
|
4699
4799
|
if lab:
|
|
4800
|
+
lab = convert_label_shortcuts(lab)
|
|
4700
4801
|
_snapshot("rename-ec-y")
|
|
4701
4802
|
try:
|
|
4702
4803
|
ec_ax.set_ylabel(lab)
|
batplot/session.py
CHANGED
|
@@ -578,7 +578,10 @@ def dump_operando_session(
|
|
|
578
578
|
# Use masked arrays to preserve NaNs if present
|
|
579
579
|
data = _np.array(arr) # preserves mask where possible
|
|
580
580
|
extent = tuple(map(float, im.get_extent())) if hasattr(im, 'get_extent') else None
|
|
581
|
-
|
|
581
|
+
# Get colormap name: first check if we stored it explicitly, otherwise try to get from colormap object
|
|
582
|
+
cmap_name = getattr(im, '_operando_cmap_name', None)
|
|
583
|
+
if cmap_name is None:
|
|
584
|
+
cmap_name = getattr(im.get_cmap(), 'name', None)
|
|
582
585
|
clim = tuple(map(float, im.get_clim())) if hasattr(im, 'get_clim') else None
|
|
583
586
|
origin = getattr(im, 'origin', 'upper')
|
|
584
587
|
interpolation = getattr(im, 'get_interpolation', lambda: None)() or 'nearest'
|
|
@@ -870,6 +873,8 @@ def load_operando_session(filename: str):
|
|
|
870
873
|
cmap_name = 'viridis'
|
|
871
874
|
im = ax.imshow(arr, aspect='auto', origin=op.get('origin', 'upper'), extent=extent,
|
|
872
875
|
cmap=cmap_name, interpolation=op.get('interpolation', 'nearest'))
|
|
876
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
877
|
+
setattr(im, '_operando_cmap_name', cmap_name)
|
|
873
878
|
if op.get('clim'):
|
|
874
879
|
try:
|
|
875
880
|
im.set_clim(*op['clim'])
|
|
@@ -1315,6 +1320,18 @@ def load_operando_session(filename: str):
|
|
|
1315
1320
|
elif ec_h_offset is not None:
|
|
1316
1321
|
# EC panel doesn't exist but offset was saved - ignore it
|
|
1317
1322
|
pass
|
|
1323
|
+
|
|
1324
|
+
# Apply layout with loaded offsets to ensure visual position matches saved position
|
|
1325
|
+
# This must happen after all offsets and geometry parameters are set
|
|
1326
|
+
try:
|
|
1327
|
+
from .operando_ec_interactive import _apply_group_layout_inches, _ensure_fixed_params
|
|
1328
|
+
# Get current geometry parameters (which should match what was just loaded)
|
|
1329
|
+
cb_w_i, cb_gap_i, ec_gap_i, ec_w_i, ax_w_i, ax_h_i = _ensure_fixed_params(fig, ax, cbar_ax, ec_ax)
|
|
1330
|
+
# Apply layout with loaded offsets (offsets are already set as attributes above)
|
|
1331
|
+
_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)
|
|
1332
|
+
except Exception:
|
|
1333
|
+
# If layout application fails, continue - better to have a slightly wrong layout than crash
|
|
1334
|
+
pass
|
|
1318
1335
|
except Exception:
|
|
1319
1336
|
pass
|
|
1320
1337
|
|
batplot/utils.py
CHANGED
|
@@ -540,6 +540,46 @@ def list_files_in_subdirectory(extensions: tuple, file_type: str, base_path: str
|
|
|
540
540
|
return sorted(files, key=lambda x: x[0])
|
|
541
541
|
|
|
542
542
|
|
|
543
|
+
def convert_label_shortcuts(text: str) -> str:
|
|
544
|
+
"""Convert shortcut syntax to LaTeX format for labels.
|
|
545
|
+
|
|
546
|
+
Converts {super(...)} and {sub(...)} shortcuts to LaTeX superscript/subscript format.
|
|
547
|
+
This allows easier input of mathematical notation without typing full LaTeX.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
text: Label text that may contain {super(...)} or {sub(...)} shortcuts
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Text with shortcuts converted to LaTeX format (uses \\mathrm{} to prevent italic rendering).
|
|
554
|
+
|
|
555
|
+
Examples:
|
|
556
|
+
>>> convert_label_shortcuts("g{super(-1)}")
|
|
557
|
+
"g$^{\\mathrm{-1}}$"
|
|
558
|
+
>>> convert_label_shortcuts("Li{sub(2)}FeSeO")
|
|
559
|
+
"Li$_{\\mathrm{2}}$FeSeO"
|
|
560
|
+
>>> convert_label_shortcuts("H{sub(2)}O")
|
|
561
|
+
"H$_{\\mathrm{2}}$O"
|
|
562
|
+
"""
|
|
563
|
+
if not text:
|
|
564
|
+
return text
|
|
565
|
+
|
|
566
|
+
import re
|
|
567
|
+
|
|
568
|
+
# Convert {super(...)} to $^{\mathrm{...}}$ to prevent italic rendering
|
|
569
|
+
# Pattern matches {super(anything inside)}
|
|
570
|
+
# Use \mathrm{} to ensure non-italic rendering unless explicitly specified
|
|
571
|
+
# Need to escape backslashes in replacement string for LaTeX commands
|
|
572
|
+
text = re.sub(r'\{super\(([^)]+)\)\}', r'$^{\\mathrm{\1}}$', text)
|
|
573
|
+
|
|
574
|
+
# Convert {sub(...)} to $_{\mathrm{...}}$ to prevent italic rendering
|
|
575
|
+
# Pattern matches {sub(anything inside)}
|
|
576
|
+
# Use \mathrm{} to ensure non-italic rendering unless explicitly specified
|
|
577
|
+
# Need to escape backslashes in replacement string for LaTeX commands
|
|
578
|
+
text = re.sub(r'\{sub\(([^)]+)\)\}', r'$_{\\mathrm{\1}}$', text)
|
|
579
|
+
|
|
580
|
+
return text
|
|
581
|
+
|
|
582
|
+
|
|
543
583
|
def normalize_label_text(text: str) -> str:
|
|
544
584
|
"""Normalize axis label text for proper matplotlib rendering.
|
|
545
585
|
|
batplot/version_check.py
CHANGED
|
@@ -53,6 +53,54 @@ import time
|
|
|
53
53
|
from pathlib import Path
|
|
54
54
|
from typing import Optional, Tuple
|
|
55
55
|
|
|
56
|
+
# ====================================================================================
|
|
57
|
+
# UPDATE INFO CONFIGURATION
|
|
58
|
+
# ====================================================================================
|
|
59
|
+
# Edit this section to customize update notification messages and add update info.
|
|
60
|
+
#
|
|
61
|
+
# HOW TO USE:
|
|
62
|
+
# ----------
|
|
63
|
+
# When releasing a new version, edit the UPDATE_INFO dictionary below to include
|
|
64
|
+
# information about what's new or important in the update. This information will
|
|
65
|
+
# be displayed to users when they run batplot and a newer version is available.
|
|
66
|
+
#
|
|
67
|
+
# EXAMPLE:
|
|
68
|
+
# --------
|
|
69
|
+
# UPDATE_INFO = {
|
|
70
|
+
# 'custom_message': "This update includes important bug fixes and new features.",
|
|
71
|
+
# 'update_notes': [
|
|
72
|
+
# "- Fixed colormap preservation issue in session files",
|
|
73
|
+
# "- Improved legend positioning when toggling visibility",
|
|
74
|
+
# "- Added superscript/subscript shortcuts for labels",
|
|
75
|
+
# "- Enhanced version check notifications"
|
|
76
|
+
# ],
|
|
77
|
+
# 'show_update_notes': True,
|
|
78
|
+
# }
|
|
79
|
+
#
|
|
80
|
+
# To disable custom messages, set 'custom_message' to None.
|
|
81
|
+
# To disable update notes, set 'update_notes' to None or an empty list [].
|
|
82
|
+
# ====================================================================================
|
|
83
|
+
|
|
84
|
+
UPDATE_INFO = {
|
|
85
|
+
# Custom message to include in update notification
|
|
86
|
+
# Set to None or empty string to disable
|
|
87
|
+
# This will be displayed as an additional line in the update message box
|
|
88
|
+
'custom_message': "This update includes important bug fixes", # Example: "This update includes important bug fixes."
|
|
89
|
+
|
|
90
|
+
# Additional notes about the update (list of strings)
|
|
91
|
+
# Set to None or empty list [] to disable
|
|
92
|
+
# Each item in the list will be displayed as a separate line
|
|
93
|
+
'update_notes': None, # Example: ["- Fixed colormap preservation issue", "- Improved legend positioning"]
|
|
94
|
+
|
|
95
|
+
# Whether to show update notes if provided
|
|
96
|
+
# Set to False to hide update notes even if they are defined
|
|
97
|
+
'show_update_notes': True,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ====================================================================================
|
|
101
|
+
# END OF UPDATE INFO CONFIGURATION
|
|
102
|
+
# ====================================================================================
|
|
103
|
+
|
|
56
104
|
|
|
57
105
|
def get_cache_file() -> Path:
|
|
58
106
|
"""Get the path to the version check cache file."""
|
|
@@ -160,12 +208,43 @@ def _print_update_message(current: str, latest: str) -> None:
|
|
|
160
208
|
current: Current version
|
|
161
209
|
latest: Latest available version
|
|
162
210
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
211
|
+
# Calculate box width (minimum 68, expand if needed for longer messages)
|
|
212
|
+
box_width = 68
|
|
213
|
+
custom_msg = UPDATE_INFO.get('custom_message')
|
|
214
|
+
update_notes = UPDATE_INFO.get('update_notes')
|
|
215
|
+
show_notes = UPDATE_INFO.get('show_update_notes', True)
|
|
216
|
+
|
|
217
|
+
# Calculate required width based on content
|
|
218
|
+
max_line_len = 68 # Default minimum width
|
|
219
|
+
if custom_msg:
|
|
220
|
+
max_line_len = max(max_line_len, len(custom_msg) + 4)
|
|
221
|
+
if update_notes and show_notes:
|
|
222
|
+
for note in update_notes:
|
|
223
|
+
max_line_len = max(max_line_len, len(note) + 4)
|
|
224
|
+
# Ensure box width is at least the calculated width
|
|
225
|
+
box_width = max(68, min(max_line_len, 100)) # Cap at 100 for readability
|
|
226
|
+
|
|
227
|
+
print(f"\n\033[93m╭{'─' * box_width}╮\033[0m")
|
|
228
|
+
print(f"\033[93m│\033[0m \033[1mA new version of batplot is available!\033[0m" + " " * max(0, box_width - 34) + "\033[93m│\033[0m")
|
|
229
|
+
print(f"\033[93m│\033[0m Current: \033[91m{current}\033[0m → Latest: \033[92m{latest}\033[0m" + " " * max(0, box_width - 20 - len(current) - len(latest)) + "\033[93m│\033[0m")
|
|
230
|
+
|
|
231
|
+
# Add custom message if provided
|
|
232
|
+
if custom_msg and custom_msg.strip():
|
|
233
|
+
# Truncate if too long to fit in box
|
|
234
|
+
msg = custom_msg[:box_width - 6] if len(custom_msg) > box_width - 6 else custom_msg
|
|
235
|
+
print(f"\033[93m│\033[0m {msg}" + " " * max(0, box_width - len(msg) - 4) + "\033[93m│\033[0m")
|
|
236
|
+
|
|
237
|
+
# Add update notes if provided
|
|
238
|
+
if update_notes and show_notes and isinstance(update_notes, list):
|
|
239
|
+
for note in update_notes:
|
|
240
|
+
if note and note.strip():
|
|
241
|
+
# Truncate if too long to fit in box
|
|
242
|
+
note_text = note[:box_width - 6] if len(note) > box_width - 6 else note
|
|
243
|
+
print(f"\033[93m│\033[0m {note_text}" + " " * max(0, box_width - len(note_text) - 4) + "\033[93m│\033[0m")
|
|
244
|
+
|
|
245
|
+
print(f"\033[93m│\033[0m Update with: \033[96mpip install --upgrade batplot\033[0m" + " " * max(0, box_width - 34) + "\033[93m│\033[0m")
|
|
246
|
+
print(f"\033[93m│\033[0m To disable this check: \033[96mexport BATPLOT_NO_VERSION_CHECK=1\033[0m" + " " * max(0, box_width - 45) + "\033[93m│\033[0m")
|
|
247
|
+
print(f"\033[93m╰{'─' * box_width}╯\033[0m\n")
|
|
169
248
|
|
|
170
249
|
|
|
171
250
|
if __name__ == '__main__':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.1
|
|
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: MIT License
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
batplot/__init__.py,sha256=
|
|
2
|
-
batplot/args.py,sha256=
|
|
1
|
+
batplot/__init__.py,sha256=AjJgj7OxrIM-uNCR6JvGnRjpVNZgCUbmj0VE_BpFFF8,118
|
|
2
|
+
batplot/args.py,sha256=GXOO5jfTSmd5y_MiGyabh-mgJsCrg3mKVfTwe1eKOYc,34961
|
|
3
3
|
batplot/batch.py,sha256=YQ7obCIqLCObwDbM7TXpOBh7g7BO95wZNsa2Fy84c6o,53858
|
|
4
4
|
batplot/batplot.py,sha256=40lU1nY1NqeAOpzNG_vLF_L34COKhiA19pMpbvA3SJc,171885
|
|
5
5
|
batplot/cif.py,sha256=JfHwNf3SHrcpALc_F5NjJmQ3lg71MBRSaIUJjGYPTx8,30120
|
|
@@ -7,22 +7,22 @@ batplot/cli.py,sha256=ScDb2je8VQ0mz_z0SLCHEigiTuFPY5pb1snnzCouKms,5828
|
|
|
7
7
|
batplot/color_utils.py,sha256=7InQLVo1XTg7sgAbltM2KeDSFJgr787YEaV9vJbIoWY,20460
|
|
8
8
|
batplot/config.py,sha256=6nGY7fKN4T5KZUGQS2ArUBgEkLAL0j37XwG5SCVQgKA,6420
|
|
9
9
|
batplot/converters.py,sha256=rR2WMPM0nR5E3eZI3gWbaJf_AfbdQx3urVSbJmZXNzo,8237
|
|
10
|
-
batplot/cpc_interactive.py,sha256=
|
|
11
|
-
batplot/electrochem_interactive.py,sha256=
|
|
12
|
-
batplot/interactive.py,sha256=
|
|
10
|
+
batplot/cpc_interactive.py,sha256=HrrjaB8-CNYUitgl5zWMNvWQLZfxyFAtpSm67qoi-nE,238235
|
|
11
|
+
batplot/electrochem_interactive.py,sha256=ti7V8BoAxUk4BD_vDRKAu5ydlHMl75htLvdVYFUUVsw,221778
|
|
12
|
+
batplot/interactive.py,sha256=uerVR-56g2Ur8qDZ-cXffPbpYMQXEXiMNXCxyWZZ8k0,206259
|
|
13
13
|
batplot/manual.py,sha256=pbRI6G4Pm12pOW8LrOLWWu7IEOtqWN3tRHtgge50LlA,11556
|
|
14
14
|
batplot/modes.py,sha256=Utfal5IaV8rfoNyNFziUZpqRlpZAWJdiTc45DY-FJE8,37300
|
|
15
|
-
batplot/operando.py,sha256=
|
|
16
|
-
batplot/operando_ec_interactive.py,sha256=
|
|
15
|
+
batplot/operando.py,sha256=p2Ug1mFUQxaU702cTBGgJKb3_v1C2p3LLUwfXaVBpPY,28311
|
|
16
|
+
batplot/operando_ec_interactive.py,sha256=TMB6rDpeolX0CgE2V7tWC24ffJrnbJomQSnTsTd8CNQ,305121
|
|
17
17
|
batplot/plotting.py,sha256=hG2_EdDhF1Qpn1XfZKdCQ5-w_m9gUYFbr804UQ5QjsU,10841
|
|
18
18
|
batplot/readers.py,sha256=kAI0AvYrdfGRZkvADJ4riN96IWtrH24aAoZpBtONTbw,112960
|
|
19
|
-
batplot/session.py,sha256=
|
|
19
|
+
batplot/session.py,sha256=05JsVi0ygMzOxVRRZ4klhE5Eh6eE6QxKR8p7_j6slBI,134429
|
|
20
20
|
batplot/style.py,sha256=ig1ozX4dhEsXf5JKaPZOvgVS3CWx-BTFSc3vfAH3Y-E,62274
|
|
21
21
|
batplot/ui.py,sha256=ifpbK74juUzLMCt-sJGVaWtpDb1NMRJzs2YyiwwafzY,35302
|
|
22
|
-
batplot/utils.py,sha256=
|
|
23
|
-
batplot/version_check.py,sha256=
|
|
22
|
+
batplot/utils.py,sha256=LY2-Axr3DsQMTxuXe48vSjrLJKEnkzkZjdSFdQizbpg,37599
|
|
23
|
+
batplot/version_check.py,sha256=ztTHwqgWd8OlS9PLLY5A_TabWxBASDA_-5yyN15PZC8,9996
|
|
24
24
|
batplot/data/USER_MANUAL.md,sha256=VYPvNZt3Fy8Z4Izr2FnQBw9vEaFTPkybhHDnF-OuKws,17694
|
|
25
|
-
batplot-1.8.
|
|
25
|
+
batplot-1.8.1.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.8.
|
|
49
|
-
batplot-1.8.
|
|
50
|
-
batplot-1.8.
|
|
51
|
-
batplot-1.8.
|
|
52
|
-
batplot-1.8.
|
|
48
|
+
batplot-1.8.1.dist-info/METADATA,sha256=3n6IsXn-SgLaDVHF6t4HCMmzDpf0Tkkbp23Dva98oN8,7406
|
|
49
|
+
batplot-1.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
50
|
+
batplot-1.8.1.dist-info/entry_points.txt,sha256=73GgH3Zs-qGIvgiyQLgGsSW-ryOwPPKHveOW6TDIR5Q,82
|
|
51
|
+
batplot-1.8.1.dist-info/top_level.txt,sha256=CgqK4RpsYnUFAcqO4bLOnEhCoPY4IPEGLPkiDlzLIxg,39
|
|
52
|
+
batplot-1.8.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|