batplot 1.8.44__tar.gz → 1.8.45__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {batplot-1.8.44 → batplot-1.8.45}/PKG-INFO +1 -1
  2. {batplot-1.8.44 → batplot-1.8.45}/batplot/__init__.py +1 -1
  3. batplot-1.8.45/batplot/_mpl_backend.py +231 -0
  4. {batplot-1.8.44 → batplot-1.8.45}/batplot/batch.py +16 -11
  5. {batplot-1.8.44 → batplot-1.8.45}/batplot/batplot.py +4 -62
  6. {batplot-1.8.44 → batplot-1.8.45}/batplot/canvas_interactive.py +4 -2
  7. {batplot-1.8.44 → batplot-1.8.45}/batplot/cli.py +2 -0
  8. {batplot-1.8.44 → batplot-1.8.45}/batplot/color_utils.py +6 -21
  9. {batplot-1.8.44 → batplot-1.8.45}/batplot/converters.py +3 -0
  10. {batplot-1.8.44 → batplot-1.8.45}/batplot/data/CHANGELOG.md +4 -0
  11. {batplot-1.8.44 → batplot-1.8.45}/batplot/dev_upgrade.py +1 -1
  12. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/palettes.py +5 -3
  13. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/spines.py +2 -2
  14. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/colors.py +4 -3
  15. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/interactive.py +2 -2
  16. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/legend.py +2 -2
  17. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/routing.py +14 -34
  18. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/colors.py +17 -10
  19. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/dqdv_2d.py +1 -1
  20. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/interactive.py +2 -2
  21. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/routing.py +37 -81
  22. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/style.py +2 -2
  23. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/colors.py +4 -5
  24. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/interactive.py +8 -6
  25. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/labels.py +1 -0
  26. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/plot.py +12 -9
  27. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/routing.py +4 -25
  28. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/style.py +2 -2
  29. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/session_routing.py +11 -1
  30. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/interactive.py +2 -2
  31. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/pipeline.py +15 -14
  32. {batplot-1.8.44 → batplot-1.8.45}/batplot/readers.py +2 -2
  33. {batplot-1.8.44 → batplot-1.8.45}/batplot/session.py +7 -7
  34. {batplot-1.8.44 → batplot-1.8.45}/batplot/version_check.py +2 -2
  35. {batplot-1.8.44 → batplot-1.8.45}/batplot.egg-info/PKG-INFO +1 -1
  36. {batplot-1.8.44 → batplot-1.8.45}/batplot.egg-info/SOURCES.txt +2 -0
  37. {batplot-1.8.44 → batplot-1.8.45}/pyproject.toml +7 -1
  38. {batplot-1.8.44 → batplot-1.8.45}/tests/test_cpc_roundtrip.py +9 -1
  39. {batplot-1.8.44 → batplot-1.8.45}/tests/test_ec_roundtrip.py +1 -1
  40. {batplot-1.8.44 → batplot-1.8.45}/tests/test_interactive_menu_smoke.py +2 -2
  41. batplot-1.8.45/tests/test_mpl_backend.py +104 -0
  42. {batplot-1.8.44 → batplot-1.8.45}/tests/test_operando_roundtrip.py +3 -3
  43. {batplot-1.8.44 → batplot-1.8.45}/DEVELOPING.md +0 -0
  44. {batplot-1.8.44 → batplot-1.8.45}/LICENSE +0 -0
  45. {batplot-1.8.44 → batplot-1.8.45}/MANIFEST.in +0 -0
  46. {batplot-1.8.44 → batplot-1.8.45}/NOTICE +0 -0
  47. {batplot-1.8.44 → batplot-1.8.45}/README.md +0 -0
  48. {batplot-1.8.44 → batplot-1.8.45}/batplot/args.py +0 -0
  49. {batplot-1.8.44 → batplot-1.8.45}/batplot/cif.py +0 -0
  50. {batplot-1.8.44 → batplot-1.8.45}/batplot/config.py +0 -0
  51. {batplot-1.8.44 → batplot-1.8.45}/batplot/ec_common.py +0 -0
  52. {batplot-1.8.44 → batplot-1.8.45}/batplot/modes.py +0 -0
  53. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/__init__.py +0 -0
  54. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/__init__.py +0 -0
  55. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/axis_state.py +0 -0
  56. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/files.py +0 -0
  57. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/fonts.py +0 -0
  58. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/interactive_state.py +0 -0
  59. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/menu_rendering.py +0 -0
  60. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/menus.py +0 -0
  61. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/smoothing.py +0 -0
  62. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/sources.py +0 -0
  63. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/terminal.py +0 -0
  64. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/common/title_offsets.py +0 -0
  65. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/__init__.py +0 -0
  66. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/actions.py +0 -0
  67. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/labels.py +0 -0
  68. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/menu.py +0 -0
  69. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/session.py +0 -0
  70. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/cpc/snapshots.py +0 -0
  71. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/__init__.py +0 -0
  72. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/actions.py +0 -0
  73. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/export.py +0 -0
  74. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/labels.py +0 -0
  75. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/legend.py +0 -0
  76. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/legend_order.py +0 -0
  77. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/line_style.py +0 -0
  78. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/menu.py +0 -0
  79. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/session.py +0 -0
  80. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/electrochem/spine_colors.py +0 -0
  81. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/__init__.py +0 -0
  82. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/actions.py +0 -0
  83. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/grid.py +0 -0
  84. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/ions_axis.py +0 -0
  85. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/layout.py +0 -0
  86. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/line_style.py +0 -0
  87. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/menu.py +0 -0
  88. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/peaks.py +0 -0
  89. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/session.py +0 -0
  90. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/operando/visibility.py +0 -0
  91. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/__init__.py +0 -0
  92. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/actions.py +0 -0
  93. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/arrange.py +0 -0
  94. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/axis_range.py +0 -0
  95. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/cif.py +0 -0
  96. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/colors.py +0 -0
  97. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/data_ops.py +0 -0
  98. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/derivative.py +0 -0
  99. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/game.py +0 -0
  100. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/labels.py +0 -0
  101. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/line_style.py +0 -0
  102. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/menu.py +0 -0
  103. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/peaks.py +0 -0
  104. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/session.py +0 -0
  105. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/smoothing.py +0 -0
  106. {batplot-1.8.44 → batplot-1.8.45}/batplot/plot_modes/xy/style.py +0 -0
  107. {batplot-1.8.44 → batplot-1.8.45}/batplot/plotting.py +0 -0
  108. {batplot-1.8.44 → batplot-1.8.45}/batplot/showcol.py +0 -0
  109. {batplot-1.8.44 → batplot-1.8.45}/batplot/style.py +0 -0
  110. {batplot-1.8.44 → batplot-1.8.45}/batplot/ui.py +0 -0
  111. {batplot-1.8.44 → batplot-1.8.45}/batplot/utils.py +0 -0
  112. {batplot-1.8.44 → batplot-1.8.45}/batplot.egg-info/dependency_links.txt +0 -0
  113. {batplot-1.8.44 → batplot-1.8.45}/batplot.egg-info/entry_points.txt +0 -0
  114. {batplot-1.8.44 → batplot-1.8.45}/batplot.egg-info/requires.txt +0 -0
  115. {batplot-1.8.44 → batplot-1.8.45}/batplot.egg-info/top_level.txt +0 -0
  116. {batplot-1.8.44 → batplot-1.8.45}/setup.cfg +0 -0
  117. {batplot-1.8.44 → batplot-1.8.45}/setup.py +0 -0
  118. {batplot-1.8.44 → batplot-1.8.45}/tests/test_cli_smoke.py +0 -0
  119. {batplot-1.8.44 → batplot-1.8.45}/tests/test_color_utils.py +0 -0
  120. {batplot-1.8.44 → batplot-1.8.45}/tests/test_common_files.py +0 -0
  121. {batplot-1.8.44 → batplot-1.8.45}/tests/test_common_palettes.py +0 -0
  122. {batplot-1.8.44 → batplot-1.8.45}/tests/test_contracts.py +0 -0
  123. {batplot-1.8.44 → batplot-1.8.45}/tests/test_csv_readers.py +0 -0
  124. {batplot-1.8.44 → batplot-1.8.45}/tests/test_dev_upgrade.py +0 -0
  125. {batplot-1.8.44 → batplot-1.8.45}/tests/test_interactive_state.py +0 -0
  126. {batplot-1.8.44 → batplot-1.8.45}/tests/test_xy_modules.py +0 -0
  127. {batplot-1.8.44 → batplot-1.8.45}/tests/test_xy_roundtrip.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.8.44
3
+ Version: 1.8.45
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
@@ -7,7 +7,7 @@ import importlib.abc
7
7
  import importlib.machinery
8
8
  import sys
9
9
 
10
- __version__ = "1.8.44"
10
+ __version__ = "1.8.45"
11
11
 
12
12
 
13
13
  _LEGACY_MODULE_ALIASES = {
@@ -0,0 +1,231 @@
1
+ """Matplotlib backend selection for batplot CLI and interactive sessions.
2
+
3
+ Imported early from :mod:`batplot.cli` so pytest/CI on Windows never fall back
4
+ to Tk when ``MPLBACKEND=Agg`` is set explicitly in the environment.
5
+
6
+ When the user has not set ``MPLBACKEND``, we start on Agg for safe imports but
7
+ may switch to a GUI backend before opening interactive windows (``.pkl`` reload,
8
+ ``--gc --i``, ``--canvas``, etc.).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import os
15
+ import sys
16
+
17
+ _USER_SET_MPLBACKEND = "MPLBACKEND" in os.environ
18
+ _DEFAULT_BACKEND = os.environ.get("MPLBACKEND", "Agg")
19
+
20
+ import matplotlib as _mpl
21
+
22
+ _mpl.use(_DEFAULT_BACKEND, force=True)
23
+
24
+ _INTERACTIVE_BACKENDS = frozenset(
25
+ {
26
+ "macosx",
27
+ "tkagg",
28
+ "qt5agg",
29
+ "qt4agg",
30
+ "qtagg",
31
+ "wxagg",
32
+ "gtk3agg",
33
+ "gtk4agg",
34
+ "wx",
35
+ "qt",
36
+ "gtk",
37
+ "gtk3",
38
+ "gtk4",
39
+ }
40
+ )
41
+
42
+
43
+ def _is_noninteractive_backend(name: str | None) -> bool:
44
+ if not isinstance(name, str):
45
+ return False
46
+ low = name.lower()
47
+ if low in _INTERACTIVE_BACKENDS:
48
+ return False
49
+ return ("agg" in low) or ("inline" in low) or (low in {"pdf", "ps", "svg", "template"})
50
+
51
+
52
+ def is_interactive_backend() -> bool:
53
+ """Return True when the active Matplotlib backend can open a GUI window."""
54
+ try:
55
+ return not _is_noninteractive_backend(_mpl.get_backend())
56
+ except Exception:
57
+ return False
58
+
59
+
60
+ def _headless_context() -> bool:
61
+ """True when batplot must stay on a non-interactive backend (CI/pytest)."""
62
+ if os.environ.get("BATPLOT_HEADLESS", "").lower() in ("1", "true", "yes"):
63
+ return True
64
+ if os.environ.get("CI", "").lower() in ("1", "true", "yes"):
65
+ return True
66
+ if os.environ.get("PYTEST_CURRENT_TEST"):
67
+ return True
68
+ if "pytest" in sys.modules:
69
+ return True
70
+ return False
71
+
72
+
73
+ def running_headless() -> bool:
74
+ """True under pytest, CI, or when ``BATPLOT_HEADLESS`` is set."""
75
+ return _headless_context()
76
+
77
+
78
+ def wants_interactive_window(args) -> bool:
79
+ """Return True when this CLI invocation should open an interactive figure."""
80
+ if getattr(args, "all", None) is not None:
81
+ return False
82
+ files = getattr(args, "files", None) or []
83
+ if any(str(f).lower().endswith(".pkl") for f in files):
84
+ return True
85
+ if getattr(args, "canvas", False):
86
+ return True
87
+ if getattr(args, "interactive", False):
88
+ return True
89
+ # Headless export: mode flags with --out/--savefig do not need a GUI window.
90
+ if getattr(args, "savefig", False) or getattr(args, "out", None):
91
+ return False
92
+ for attr in ("operando", "contour", "gc", "cv", "dqdv", "cpc", "epc"):
93
+ if getattr(args, attr, False):
94
+ return True
95
+ return False
96
+
97
+
98
+ def _should_respect_env_agg() -> bool:
99
+ """Keep Agg when the environment requested it and we are in headless context."""
100
+ if not _USER_SET_MPLBACKEND:
101
+ return False
102
+ env_be = os.environ.get("MPLBACKEND")
103
+ if not env_be or not _is_noninteractive_backend(env_be):
104
+ return False
105
+ return _headless_context()
106
+
107
+
108
+ def _has_qt_bindings() -> bool:
109
+ for mod in ("PyQt6", "PyQt5", "PySide6", "PySide2"):
110
+ if importlib.util.find_spec(mod) is not None:
111
+ return True
112
+ return False
113
+
114
+
115
+ def _gui_backend_order() -> list[str]:
116
+ if sys.platform == "darwin":
117
+ return ["MacOSX", "TkAgg", "QtAgg"]
118
+ if sys.platform.startswith("win"):
119
+ return ["TkAgg", "QtAgg"]
120
+ return ["TkAgg", "QtAgg", "Gtk4Agg", "Gtk3Agg"]
121
+
122
+
123
+ def _backend_can_be_tried(name: str) -> bool:
124
+ if name == "TkAgg":
125
+ return importlib.util.find_spec("tkinter") is not None
126
+ if name == "QtAgg":
127
+ return _has_qt_bindings()
128
+ return True
129
+
130
+
131
+ def _backend_setup_tips() -> list[str]:
132
+ if sys.platform == "darwin":
133
+ return [
134
+ " export MPLBACKEND=MacOSX # built-in macOS backend",
135
+ " export MPLBACKEND=TkAgg # if Tk is available",
136
+ " export MPLBACKEND=QtAgg # if PyQt/PySide is installed",
137
+ ]
138
+ if sys.platform.startswith("win"):
139
+ return [
140
+ " set MPLBACKEND=TkAgg # cmd, if Tk is available",
141
+ ' $env:MPLBACKEND="TkAgg" # PowerShell',
142
+ " set MPLBACKEND=QtAgg # if PyQt/PySide is installed",
143
+ ]
144
+ return [
145
+ " export MPLBACKEND=TkAgg # if python3-tk is installed",
146
+ " export MPLBACKEND=QtAgg # if PyQt/PySide is installed",
147
+ " export MPLBACKEND=Gtk3Agg # if GTK3 bindings are installed",
148
+ ]
149
+
150
+
151
+ def ensure_gui_backend(args=None) -> bool:
152
+ """Switch from Agg to a GUI backend when an interactive window is expected.
153
+
154
+ Returns True when the active backend is interactive (or was switched to one).
155
+ Honors ``MPLBACKEND=Agg`` only under pytest/CI; interactive CLI use (``--i``,
156
+ ``.pkl`` reload) overrides an inherited Agg default from conda/shell profiles.
157
+ """
158
+ if args is not None and not wants_interactive_window(args):
159
+ return is_interactive_backend()
160
+
161
+ if _should_respect_env_agg():
162
+ return False
163
+
164
+ if _USER_SET_MPLBACKEND:
165
+ env_be = os.environ.get("MPLBACKEND")
166
+ if env_be and not _is_noninteractive_backend(env_be):
167
+ return True
168
+
169
+ if is_interactive_backend():
170
+ return True
171
+
172
+ for cand in _gui_backend_order():
173
+ if not _backend_can_be_tried(cand):
174
+ continue
175
+ try:
176
+ _mpl.use(cand, force=True)
177
+ if is_interactive_backend():
178
+ return True
179
+ except Exception:
180
+ continue
181
+ return False
182
+
183
+
184
+ def warn_if_noninteractive(context: str = "interactive menu") -> bool:
185
+ """Print a helpful message when the backend cannot show a window."""
186
+ if is_interactive_backend():
187
+ return True
188
+ try:
189
+ backend = _mpl.get_backend()
190
+ except Exception:
191
+ backend = "unknown"
192
+ print(f"Matplotlib backend '{backend}' is non-interactive; a window cannot be shown.")
193
+ print("Tips: unset MPLBACKEND or set a GUI backend:")
194
+ for line in _backend_setup_tips():
195
+ print(line)
196
+ if context:
197
+ print(f"The {context} requires an interactive backend.")
198
+ return False
199
+
200
+
201
+ def require_interactive_display(
202
+ args=None,
203
+ *,
204
+ context: str = "interactive menu",
205
+ export_hint: str | None = "Or run without --interactive and use --out to save the figure.",
206
+ ) -> bool:
207
+ """Ensure a GUI backend, warn if unavailable, return True when display is possible."""
208
+ ensure_gui_backend(args)
209
+ if is_interactive_backend():
210
+ return True
211
+ warn_if_noninteractive(context)
212
+ if export_hint:
213
+ print(export_hint)
214
+ return False
215
+
216
+
217
+ def show_figure_if_possible(args=None, *, block: bool = False) -> bool:
218
+ """Show the current figure when the backend supports windows."""
219
+ ensure_gui_backend(args)
220
+ if not is_interactive_backend():
221
+ if args is not None and not (getattr(args, "savefig", False) or getattr(args, "out", None)):
222
+ try:
223
+ backend = _mpl.get_backend()
224
+ except Exception:
225
+ backend = "unknown"
226
+ print(f"Matplotlib backend '{backend}' is non-interactive; use --out to save the figure.")
227
+ return False
228
+ import matplotlib.pyplot as plt
229
+
230
+ plt.show(block=block)
231
+ return True
@@ -6,9 +6,10 @@ import os
6
6
  import json
7
7
  from typing import Tuple, cast
8
8
 
9
- import matplotlib.cm as cm # type: ignore[import]
10
- import numpy as np # type: ignore[import]
11
9
  import matplotlib.pyplot as plt # type: ignore[import]
10
+ import numpy as np # type: ignore[import]
11
+
12
+ from .color_utils import get_colormap
12
13
 
13
14
  from .readers import (
14
15
  read_gr_file,
@@ -961,9 +962,11 @@ def batch_process_ec(directory: str, args):
961
962
 
962
963
  if remaining <= 60:
963
964
  # Use tab20, tab20b, tab20c for categorical colors (up to 70 total)
964
- tab20 = cm.get_cmap('tab20')
965
- tab20b = cm.get_cmap('tab20b')
966
- tab20c = cm.get_cmap('tab20c')
965
+ tab20 = get_colormap('tab20')
966
+ tab20b = get_colormap('tab20b')
967
+ tab20c = get_colormap('tab20c')
968
+ if tab20 is None or tab20b is None or tab20c is None:
969
+ return colors[:n_colors]
967
970
 
968
971
  for i in range(remaining):
969
972
  cmap_idx = i % 60
@@ -987,7 +990,9 @@ def batch_process_ec(directory: str, args):
987
990
  colors_per_map = (remaining + len(cmaps) - 1) // len(cmaps)
988
991
 
989
992
  for cmap_name in cmaps:
990
- cmap = cm.get_cmap(cmap_name)
993
+ cmap = get_colormap(cmap_name)
994
+ if cmap is None:
995
+ continue
991
996
  # Sample evenly across the colormap
992
997
  for i in range(colors_per_map):
993
998
  if len(colors) >= n_colors:
@@ -1117,7 +1122,7 @@ def batch_process_ec(directory: str, args):
1117
1122
  cycle_colors = get_color_palette(len(cycles_present))
1118
1123
 
1119
1124
  for idx, cyc in enumerate(cycles_present): # Plot all cycles
1120
- mask = (cyc_int == cyc)
1125
+ mask = np.asarray(cyc_int == cyc, dtype=bool)
1121
1126
  mask_idx = np.where(mask)[0]
1122
1127
  if mask_idx.size >= 2:
1123
1128
  color = cycle_colors[idx]
@@ -1265,11 +1270,11 @@ def batch_process_ec(directory: str, args):
1265
1270
  mask_c = (cyc_int_raw == cyc) & charge_mask
1266
1271
  mask_d = (cyc_int_raw == cyc) & discharge_mask
1267
1272
  if np.count_nonzero(mask_c) >= 2:
1268
- en_c = float(np.trapz(voltage[mask_c], cap_x[mask_c]))
1273
+ en_c = float((getattr(np, "trapezoid", None) or np.trapz)(voltage[mask_c], cap_x[mask_c]))
1269
1274
  else:
1270
1275
  en_c = 0.0
1271
1276
  if np.count_nonzero(mask_d) >= 2:
1272
- en_d = float(np.trapz(voltage[mask_d], cap_x[mask_d]))
1277
+ en_d = float((getattr(np, "trapezoid", None) or np.trapz)(voltage[mask_d], cap_x[mask_d]))
1273
1278
  else:
1274
1279
  en_d = 0.0
1275
1280
  en_charge.append(en_c)
@@ -1300,11 +1305,11 @@ def batch_process_ec(directory: str, args):
1300
1305
  val_d = np.max(cap_x[mask_d]) if np.any(mask_d) else 0.0
1301
1306
  else:
1302
1307
  if np.count_nonzero(mask_c) >= 2:
1303
- val_c = float(np.trapz(voltage[mask_c], cap_x[mask_c]))
1308
+ val_c = float((getattr(np, "trapezoid", None) or np.trapz)(voltage[mask_c], cap_x[mask_c]))
1304
1309
  else:
1305
1310
  val_c = 0.0
1306
1311
  if np.count_nonzero(mask_d) >= 2:
1307
- val_d = float(np.trapz(voltage[mask_d], cap_x[mask_d]))
1312
+ val_d = float((getattr(np, "trapezoid", None) or np.trapz)(voltage[mask_d], cap_x[mask_d]))
1308
1313
  else:
1309
1314
  val_d = 0.0
1310
1315
  cap_charge_list.append(val_c)
@@ -421,6 +421,10 @@ def batplot_main() -> int: # type: ignore
421
421
  print("Use 'batplot --v' for version and release info, 'batplot --h' for CLI help, or 'batplot --m' to open the user manual.")
422
422
  return 0 # Exit successfully (not an error, just nothing to do)
423
423
 
424
+ from ._mpl_backend import ensure_gui_backend
425
+
426
+ ensure_gui_backend(args)
427
+
424
428
  # ====================================================================
425
429
  # STEP 3: ROUTE TO APPROPRIATE MODE HANDLER
426
430
  # ====================================================================
@@ -482,64 +486,6 @@ def batplot_main() -> int: # type: ignore
482
486
  """
483
487
 
484
488
 
485
- # Ensure an interactive (GUI) backend when a window is expected (interactive mode or operando/GC plots)
486
- def _ensure_gui_backend_for_interactive():
487
- try:
488
- argv = sys.argv
489
- except Exception:
490
- argv = []
491
- # Trigger if interactive is requested OR when operando/GC plotting likely calls show()
492
- wants_interactive = any(flag in argv for flag in ("--interactive",))
493
- wants_interactive = wants_interactive or ("--operando" in argv or "--contour" in argv)
494
- wants_interactive = wants_interactive or ("--gc" in argv)
495
- if not wants_interactive:
496
- return
497
- # If MPLBACKEND is set to a GUI backend, respect it; if it's non-interactive, we'll override below
498
- env_be = os.environ.get("MPLBACKEND")
499
- if env_be:
500
- low = env_be.lower()
501
- if low in {"macosx","tkagg","qtagg"}:
502
- return
503
- try:
504
- be = _mpl.get_backend()
505
- except Exception:
506
- be = None
507
- def _is_noninteractive(name):
508
- if not isinstance(name, str):
509
- return False
510
- low = name.lower()
511
- return ("agg" in low) or ("inline" in low) or (low in {"pdf","ps","svg","template"})
512
- if not _is_noninteractive(be):
513
- return
514
- # Try GUI backends in order of likelihood
515
- candidates = [
516
- ("darwin", ["MacOSX", "TkAgg", "QtAgg"]),
517
- ("win", ["TkAgg", "QtAgg"]),
518
- ("other", ["TkAgg", "QtAgg"]),
519
- ]
520
- plat = sys.platform
521
- if plat == "darwin":
522
- order = candidates[0][1]
523
- elif plat.startswith("win"):
524
- order = candidates[1][1]
525
- else:
526
- order = candidates[2][1]
527
- for cand in order:
528
- try:
529
- if cand == "TkAgg":
530
- if importlib.util.find_spec("tkinter") is None:
531
- continue
532
- elif cand == "QtAgg":
533
- if (importlib.util.find_spec("PyQt5") is None) and (importlib.util.find_spec("PySide6") is None):
534
- continue
535
- # MacOSX: attempt; will fail on non-framework builds
536
- _mpl.use(cand, force=True)
537
- break
538
- except Exception:
539
- continue
540
-
541
- _ensure_gui_backend_for_interactive()
542
-
543
489
  # Set global default font
544
490
  plt.rcParams.update({
545
491
  'font.family': 'sans-serif',
@@ -550,10 +496,6 @@ def batplot_main() -> int: # type: ignore
550
496
  'font.size': 16
551
497
  })
552
498
 
553
- # Parse CLI arguments early; many top-level branches depend on args
554
- args = _bp_parse_args()
555
-
556
-
557
499
  """
558
500
  Note: CIF parsing and simulation helpers now come from batplot.cif.
559
501
  This file defers to simulate_cif_pattern_Q and cif_reflection_positions
@@ -25,6 +25,7 @@ from matplotlib.patches import Rectangle
25
25
  from matplotlib.text import Text
26
26
 
27
27
  from .session import load_ec_session, load_operando_session, load_cpc_session, load_xy_session
28
+ from ._mpl_backend import ensure_gui_backend
28
29
  from .plot_modes.electrochem.interactive import electrochem_interactive_menu
29
30
  from .plot_modes.xy.interactive import interactive_menu, normalize_xy_menu_kwargs
30
31
  from .utils import _confirm_overwrite
@@ -290,6 +291,7 @@ def _normalize_loaded_annotation(raw: Any, base_dir: Optional[str]) -> Optional[
290
291
 
291
292
 
292
293
  def run_canvas_mode(pkl_paths: List[str]) -> None:
294
+ ensure_gui_backend(None)
293
295
  pkl_paths, loaded_positions, raw_annotations, manifest_base_dir = _expand_canvas_paths(pkl_paths)
294
296
 
295
297
  if not pkl_paths:
@@ -465,7 +467,7 @@ def run_canvas_mode(pkl_paths: List[str]) -> None:
465
467
  ann_runtime.append((None, None))
466
468
  continue
467
469
  try:
468
- ax_img = fig_canvas.add_axes([l, b, w, h], facecolor='none')
470
+ ax_img = fig_canvas.add_axes((l, b, w, h), facecolor='none')
469
471
  ax_img.set_zorder(15)
470
472
  if arr.ndim == 2:
471
473
  ax_img.imshow(arr, cmap='gray', aspect='auto', origin='upper')
@@ -724,7 +726,7 @@ def run_canvas_mode(pkl_paths: List[str]) -> None:
724
726
  continue
725
727
  try:
726
728
  arr_e = mpl_imread(ip)
727
- ax_e = fig_exp.add_axes([l, b, w, h], facecolor='none')
729
+ ax_e = fig_exp.add_axes((l, b, w, h), facecolor='none')
728
730
  ax_e.set_zorder(15)
729
731
  if arr_e.ndim == 2:
730
732
  ax_e.imshow(arr_e, cmap='gray', aspect='auto', origin='upper')
@@ -14,6 +14,8 @@ from __future__ import annotations
14
14
  import sys
15
15
  from typing import Optional
16
16
 
17
+ from . import _mpl_backend # noqa: F401 — set Agg before pyplot loads
18
+
17
19
 
18
20
  def main(argv: Optional[list] = None) -> int:
19
21
  """
@@ -41,7 +41,7 @@ This is useful when you want the color order flipped.
41
41
 
42
42
  from __future__ import annotations
43
43
 
44
- from typing import List, Optional, Sequence
44
+ from typing import List, Optional, Sequence, cast
45
45
 
46
46
  import matplotlib.pyplot as plt # type: ignore[import]
47
47
  from matplotlib import colors as mcolors # type: ignore[import]
@@ -171,7 +171,9 @@ def ensure_colormap(name: Optional[str]) -> bool:
171
171
  # STEP 4: Final fallback - try to get it directly from matplotlib
172
172
  # This handles any other matplotlib-compatible colormap
173
173
  try:
174
- plt.get_cmap(base_lower)
174
+ from matplotlib import colormaps as mpl_colormaps
175
+
176
+ _ = mpl_colormaps[base_lower]
175
177
  return True
176
178
  except Exception:
177
179
  return False
@@ -201,7 +203,7 @@ def get_colormap(name: Optional[str]) -> Optional[Colormap]:
201
203
  if callable(registry_get):
202
204
  for candidate in candidates:
203
205
  try:
204
- return registry_get(candidate)
206
+ return cast(Colormap, registry_get(candidate))
205
207
  except Exception:
206
208
  pass
207
209
  for candidate in candidates:
@@ -313,24 +315,7 @@ def palette_preview(name: str, steps: int = 8) -> str:
313
315
  ensure_colormap(name)
314
316
 
315
317
  # Try to get the colormap from matplotlib
316
- cmap: Optional[Colormap]
317
- try:
318
- cmap = plt.get_cmap(name)
319
- except Exception:
320
- cmap = None
321
- # Fallback: try cmcrameri if it's a batlow variant
322
- lower = name.lower()
323
- if lower.startswith('batlow'):
324
- try:
325
- import cmcrameri.cm as cmc # type: ignore[import]
326
- if hasattr(cmc, lower):
327
- cmap = getattr(cmc, lower)
328
- elif hasattr(cmc, 'batlow'):
329
- cmap = getattr(cmc, 'batlow')
330
- except Exception:
331
- return ""
332
- else:
333
- return ""
318
+ cmap: Optional[Colormap] = get_colormap(name)
334
319
 
335
320
  # If we still don't have a colormap, bail out
336
321
  if cmap is None:
@@ -271,6 +271,7 @@ def convert_xrd_data(
271
271
 
272
272
  # Perform conversion based on type
273
273
  if conversion_type == "wavelength_to_wavelength":
274
+ assert from_wl is not None and to_wl is not None
274
275
  # Step 1: Convert 2θ (from_wl) → Q
275
276
  theta_rad = np.radians(x / 2) # Convert 2θ to θ in radians
276
277
  q = 4 * np.pi * np.sin(theta_rad) / from_wl
@@ -287,12 +288,14 @@ def convert_xrd_data(
287
288
  output_ext = os.path.splitext(fname)[1] # Keep original extension
288
289
 
289
290
  elif conversion_type == "wavelength_to_q":
291
+ assert from_wl is not None
290
292
  # Convert 2θ → Q
291
293
  theta_rad = np.radians(x / 2) # Convert 2θ to θ in radians
292
294
  x_new = 4 * np.pi * np.sin(theta_rad) / from_wl
293
295
  output_ext = ".qye"
294
296
 
295
297
  elif conversion_type == "q_to_wavelength":
298
+ assert to_wl is not None
296
299
  # Convert Q → 2θ
297
300
  # Q = 4π sin(θ) / λ, so sin(θ) = Q × λ / (4π)
298
301
  sin_theta = x * to_wl / (4 * np.pi)
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.45] - 2026-06-30
4
+ - Bug fixes with backend setting
5
+
6
+
3
7
  ## [1.8.44] - 2026-06-25
4
8
  - Bug fixes
5
9
 
@@ -598,7 +598,7 @@ def _required_package_data_files(project_root: Path) -> list[str]:
598
598
  return []
599
599
  rel_paths: list[str] = []
600
600
  for item in re.findall(r'"([^"]+)"', package_match.group("items")):
601
- rel_paths.append(f"batplot/{item.replace('\\\\', '/')}")
601
+ rel_paths.append("batplot/" + item.replace("\\", "/"))
602
602
  return sorted(set(rel_paths))
603
603
 
604
604
 
@@ -9,12 +9,14 @@ keeps its own established defaults.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from typing import Any, Callable, Dict, Iterable, List, Sequence, Tuple
12
+ from typing import Any, Callable, Dict, Iterable, List, Sequence, Tuple, cast
13
13
 
14
14
  import numpy as np
15
15
  import matplotlib.pyplot as plt
16
16
  import matplotlib.colors as mcolors
17
17
 
18
+ from ...color_utils import get_colormap
19
+
18
20
 
19
21
  TAB10_HEX: Tuple[str, ...] = (
20
22
  "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
@@ -181,7 +183,7 @@ def sample_palette_colors(
181
183
  if not ensure_colormap(name):
182
184
  name = "viridis"
183
185
  ensure_colormap(name)
184
- cmap_getter = get_cmap or plt.get_cmap
186
+ cmap_getter = get_cmap or get_colormap
185
187
  try:
186
188
  cmap = cmap_getter(name)
187
189
  except Exception:
@@ -195,7 +197,7 @@ def sample_palette_colors(
195
197
  if isinstance(rgb, tuple) and len(rgb) >= 3:
196
198
  out.append(mcolors.rgb2hex(rgb[:3]))
197
199
  else:
198
- out.append(mcolors.to_hex(rgb))
200
+ out.append(mcolors.to_hex(cast(Any, rgb)))
199
201
  return out
200
202
  sampled = sample_colormap(cmap, n, single=single, pair=pair, span=span)
201
203
  return [mcolors.rgb2hex(color[:3]) for color in sampled]
@@ -7,7 +7,7 @@ application that is identical across modes.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any, Callable, Dict, Iterable, Mapping, MutableMapping, Optional, Sequence
10
+ from typing import Any, Callable, Dict, Iterable, Mapping, MutableMapping, Optional, Sequence, cast
11
11
 
12
12
  import matplotlib.pyplot as plt # type: ignore[import-untyped]
13
13
  from matplotlib.ticker import ( # type: ignore[import-untyped]
@@ -340,7 +340,7 @@ def current_tick_width(axis_obj: Any, which: str) -> Optional[float]:
340
340
  width = tick_kw.get("width")
341
341
  if width is None:
342
342
  axis_name = getattr(axis_obj, "axis_name", "x")
343
- width = plt.rcParams.get(f"{axis_name}tick.{which}.width")
343
+ width = plt.rcParams.get(cast(Any, f"{axis_name}tick.{which}.width"))
344
344
  return float(width) if width is not None else None
345
345
  except Exception:
346
346
  return None
@@ -2,12 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import matplotlib.cm as cm # type: ignore[import]
6
5
  from matplotlib.colors import hsv_to_rgb, rgb_to_hsv, to_rgb # type: ignore[import]
7
6
 
8
7
  from ...color_utils import (
9
8
  color_block,
10
9
  ensure_colormap,
10
+ get_colormap,
11
11
  get_user_color_list,
12
12
  manage_user_colors,
13
13
  palette_preview,
@@ -41,7 +41,7 @@ def cpc_palette_color(name: str, idx: int = 0, total: int = 1):
41
41
  name,
42
42
  max(total, 1),
43
43
  ensure_colormap=ensure_colormap,
44
- get_cmap=cm.get_cmap,
44
+ get_cmap=get_colormap,
45
45
  prefer_listed_colors=True,
46
46
  pair=(0.15, 0.85),
47
47
  span=(0.08, 0.88),
@@ -59,7 +59,8 @@ def parse_file_range_palette(tokens: list[str], n_files: int, palette_opts: list
59
59
  try:
60
60
  if not ensure_colormap(last):
61
61
  raise ValueError(last)
62
- cm.get_cmap(last)
62
+ if get_colormap(last) is None:
63
+ raise ValueError(last)
63
64
  palette = last
64
65
  except Exception:
65
66
  if last.isdigit() and 1 <= int(last) <= len(palette_opts):
@@ -33,7 +33,7 @@ This is useful for comparing different battery cells, materials, or conditions.
33
33
  """
34
34
  from __future__ import annotations
35
35
 
36
- from typing import Any, Dict, List, Optional
36
+ from typing import Any, Dict, List, Optional, cast
37
37
  import json
38
38
  import os
39
39
  import sys
@@ -226,7 +226,7 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
226
226
  if width is None:
227
227
  axis_name = getattr(axis_obj, 'axis_name', 'x')
228
228
  rc_key = f"{axis_name}tick.{which}.width"
229
- width = plt.rcParams.get(rc_key)
229
+ width = plt.rcParams.get(cast(Any, rc_key))
230
230
  if width is not None:
231
231
  return float(width)
232
232
  except Exception:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Optional
5
+ from typing import Any, Optional, cast
6
6
 
7
7
  import numpy as np
8
8
  import matplotlib.patches as mpatches # type: ignore[import]
@@ -116,7 +116,7 @@ def _color_of(artist):
116
116
  return color[0]
117
117
  if color is not None:
118
118
  try:
119
- return to_hex(color)
119
+ return to_hex(cast(Any, color))
120
120
  except Exception:
121
121
  return color
122
122
  if hasattr(artist, "get_facecolors"):