quantized-lab 0.8.0__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.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Publication rendering for vector field plots (quiver and streamline).
|
|
2
|
+
|
|
3
|
+
ORIGIN_GAP_PLAN #23 / GAP_TIER3_PLAN item 4. Pure layer: gridded (x, y, u, v)
|
|
4
|
+
field data -> quiver or streamline vector plot in image bytes. Shares
|
|
5
|
+
``calc.figure_styles`` presets and ``calc.figure``'s resolved-dpi convention.
|
|
6
|
+
|
|
7
|
+
Validates that x/y axes have matching grid shape to the (u, v) components.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from io import BytesIO
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import matplotlib
|
|
16
|
+
|
|
17
|
+
matplotlib.use("Agg") # headless
|
|
18
|
+
|
|
19
|
+
import matplotlib.pyplot as plt # noqa: E402
|
|
20
|
+
import numpy as np # noqa: E402
|
|
21
|
+
from numpy.typing import ArrayLike # noqa: E402
|
|
22
|
+
|
|
23
|
+
from quantized.calc.figure_labels import safe_mathtext_label # noqa: E402
|
|
24
|
+
from quantized.calc.figure_styles import figure_style # noqa: E402
|
|
25
|
+
|
|
26
|
+
__all__ = ["render_field_figure"]
|
|
27
|
+
|
|
28
|
+
_FORMATS = ("pdf", "svg", "png", "tiff")
|
|
29
|
+
_FIGURE_SIZE_IN = (10, 8) # inches
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def render_field_figure(
|
|
33
|
+
x_axis: ArrayLike,
|
|
34
|
+
y_axis: ArrayLike,
|
|
35
|
+
u_grid: ArrayLike,
|
|
36
|
+
v_grid: ArrayLike,
|
|
37
|
+
*,
|
|
38
|
+
kind: str = "quiver",
|
|
39
|
+
fmt: str = "pdf",
|
|
40
|
+
style: str = "default",
|
|
41
|
+
dpi: int | None = None,
|
|
42
|
+
title: str = "",
|
|
43
|
+
x_label: str = "",
|
|
44
|
+
y_label: str = "",
|
|
45
|
+
) -> bytes:
|
|
46
|
+
"""Render a vector field plot (quiver or streamline).
|
|
47
|
+
|
|
48
|
+
``x_axis`` and ``y_axis`` are 1-D coordinate arrays defining a regular grid.
|
|
49
|
+
``u_grid`` and ``v_grid`` are 2-D arrays (shape: len(y_axis) × len(x_axis))
|
|
50
|
+
of x and y components of the field at each grid point.
|
|
51
|
+
|
|
52
|
+
``kind`` is either "quiver" (arrows at each grid point) or "streamline"
|
|
53
|
+
(streamlines following the field). ``fmt``, ``style``, and ``dpi`` follow
|
|
54
|
+
``render_figure``'s conventions: ``dpi`` defaults to the preset's calibrated
|
|
55
|
+
resolution when not given.
|
|
56
|
+
|
|
57
|
+
``title``, ``x_label``, and ``y_label`` are optional figure labels.
|
|
58
|
+
|
|
59
|
+
Returns image bytes in the requested ``fmt``. Raises ``ValueError`` on
|
|
60
|
+
malformed input (wrong shape, bad kind, bad format).
|
|
61
|
+
"""
|
|
62
|
+
if fmt not in _FORMATS:
|
|
63
|
+
raise ValueError(f"fmt must be one of {_FORMATS}")
|
|
64
|
+
if kind not in ("quiver", "streamline"):
|
|
65
|
+
raise ValueError("kind must be 'quiver' or 'streamline'")
|
|
66
|
+
# Rich-text labels (GOTO #5): de-math INVALID $...$ so savefig never raises.
|
|
67
|
+
title = safe_mathtext_label(title)
|
|
68
|
+
x_label = safe_mathtext_label(x_label)
|
|
69
|
+
y_label = safe_mathtext_label(y_label)
|
|
70
|
+
|
|
71
|
+
x_arr = np.asarray(x_axis, dtype=float)
|
|
72
|
+
y_arr = np.asarray(y_axis, dtype=float)
|
|
73
|
+
u_arr = np.asarray(u_grid, dtype=float)
|
|
74
|
+
v_arr = np.asarray(v_grid, dtype=float)
|
|
75
|
+
|
|
76
|
+
if x_arr.ndim != 1 or y_arr.ndim != 1:
|
|
77
|
+
raise ValueError("x_axis and y_axis must be 1-D arrays")
|
|
78
|
+
if u_arr.ndim != 2 or v_arr.ndim != 2:
|
|
79
|
+
raise ValueError("u_grid and v_grid must be 2-D arrays")
|
|
80
|
+
if u_arr.shape != v_arr.shape:
|
|
81
|
+
raise ValueError("u_grid and v_grid must have the same shape")
|
|
82
|
+
if u_arr.shape != (len(y_arr), len(x_arr)):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"u_grid/v_grid shape {u_arr.shape} doesn't match "
|
|
85
|
+
f"grid dimensions ({len(y_arr)}, {len(x_arr)})"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
st = figure_style(style)
|
|
89
|
+
resolved_dpi = int(dpi) if dpi is not None else int(st.dpi)
|
|
90
|
+
fallback = "DejaVu Serif" if st.font_generic == "serif" else "DejaVu Sans"
|
|
91
|
+
rc: dict[str, Any] = {
|
|
92
|
+
"font.family": st.font_generic,
|
|
93
|
+
f"font.{st.font_generic}": [st.font_name, fallback],
|
|
94
|
+
"font.size": st.font_size,
|
|
95
|
+
"axes.labelsize": st.font_size,
|
|
96
|
+
"axes.titlesize": st.title_font_size,
|
|
97
|
+
"xtick.labelsize": st.font_size - 1,
|
|
98
|
+
"ytick.labelsize": st.font_size - 1,
|
|
99
|
+
"xtick.direction": st.tick_dir,
|
|
100
|
+
"ytick.direction": st.tick_dir,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Build a meshgrid for proper vector field plotting
|
|
104
|
+
xx, yy = np.meshgrid(x_arr, y_arr, indexing="xy")
|
|
105
|
+
|
|
106
|
+
with matplotlib.rc_context(rc): # type: ignore[arg-type]
|
|
107
|
+
fig, ax = plt.subplots(figsize=_FIGURE_SIZE_IN)
|
|
108
|
+
try:
|
|
109
|
+
if kind == "quiver":
|
|
110
|
+
# Quiver: arrows at grid points, colored by magnitude
|
|
111
|
+
magnitude = np.sqrt(u_arr**2 + v_arr**2)
|
|
112
|
+
q = ax.quiver(xx, yy, u_arr, v_arr, magnitude, cmap="viridis")
|
|
113
|
+
fig.colorbar(q, ax=ax, label="Magnitude")
|
|
114
|
+
else: # streamline
|
|
115
|
+
# Streamline: field lines following the vector field
|
|
116
|
+
speed = np.sqrt(u_arr**2 + v_arr**2)
|
|
117
|
+
strm = ax.streamplot(
|
|
118
|
+
x_arr, y_arr, u_arr, v_arr,
|
|
119
|
+
color=speed, cmap="viridis", density=1.5, linewidth=1.0,
|
|
120
|
+
)
|
|
121
|
+
fig.colorbar(strm.lines, ax=ax, label="Speed")
|
|
122
|
+
|
|
123
|
+
ax.set_xlabel(x_label)
|
|
124
|
+
ax.set_ylabel(y_label)
|
|
125
|
+
if title:
|
|
126
|
+
ax.set_title(title, fontsize=st.title_font_size)
|
|
127
|
+
|
|
128
|
+
if not st.box_on:
|
|
129
|
+
ax.spines["top"].set_visible(False)
|
|
130
|
+
ax.spines["right"].set_visible(False)
|
|
131
|
+
|
|
132
|
+
fig.tight_layout()
|
|
133
|
+
buf = BytesIO()
|
|
134
|
+
fig.savefig(buf, format=fmt, dpi=resolved_dpi)
|
|
135
|
+
return buf.getvalue()
|
|
136
|
+
finally:
|
|
137
|
+
plt.close(fig)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Figure-hitmap element collection (gap #13): draw a figure and harvest one
|
|
2
|
+
pixel bounding box per interactive artist (title / axis labels / legend /
|
|
3
|
+
series lines / annotations) + the axes rect with data limits, so the client
|
|
4
|
+
can hit-test a preview render and map pixels back to data coordinates.
|
|
5
|
+
|
|
6
|
+
Split out of ``calc.figure`` purely to stay under the 500-line god-module
|
|
7
|
+
ceiling (mirrors ``figure_break``/``figure_scale``/``figure_overrides``);
|
|
8
|
+
``figure.render_figure_map`` (``_render_impl(..., collect_map=True)``) is the
|
|
9
|
+
only caller. Pure layer: a live ``Figure``/``Axes`` in -> a plain dict out.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from io import BytesIO
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
__all__ = ["collect_map"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _bbox_to_pixels(bbox: Any, height: float) -> dict[str, float]:
|
|
23
|
+
"""Window extent (origin bottom-left) -> image pixels (origin top-left)."""
|
|
24
|
+
return {
|
|
25
|
+
"x0": float(bbox.x0),
|
|
26
|
+
"y0": float(height - bbox.y1),
|
|
27
|
+
"x1": float(bbox.x1),
|
|
28
|
+
"y1": float(height - bbox.y0),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _artist_window_extent(artist: Any, renderer: Any) -> Any:
|
|
33
|
+
"""``artist.get_window_extent(renderer)``, with a workaround for
|
|
34
|
+
matplotlib's ``Collection`` (what ``ax.scatter`` -- MAIN #14's colour-
|
|
35
|
+
mapped scatter -- returns): ``Collection.get_window_extent`` calls
|
|
36
|
+
``get_datalim(IdentityTransform())`` instead of transforming to display
|
|
37
|
+
space, which returns a degenerate all-``inf`` bbox for a plain scatter.
|
|
38
|
+
Detected via ``get_offsets``/``get_offset_transform`` (present on any
|
|
39
|
+
``Collection`` with point offsets, scatter included) -- compute the real
|
|
40
|
+
screen-space bbox from the transformed offsets instead. Falls through to
|
|
41
|
+
the artist's own ``get_window_extent`` for everything else (``Line2D``,
|
|
42
|
+
``Text``, ``Legend``, ...)."""
|
|
43
|
+
get_offsets = getattr(artist, "get_offsets", None)
|
|
44
|
+
get_offset_transform = getattr(artist, "get_offset_transform", None)
|
|
45
|
+
if get_offsets is not None and get_offset_transform is not None:
|
|
46
|
+
pts = get_offset_transform().transform(get_offsets())
|
|
47
|
+
if len(pts):
|
|
48
|
+
from matplotlib.transforms import Bbox
|
|
49
|
+
|
|
50
|
+
return Bbox([pts.min(axis=0), pts.max(axis=0)])
|
|
51
|
+
return artist.get_window_extent(renderer)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def collect_map(
|
|
55
|
+
fig: Any, ax: Any, *, series_artists: Sequence[Any], dpi: int, x_scale: str, y_scale: str
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""Draw at ``dpi`` and harvest artist extents in image-pixel coords.
|
|
58
|
+
``series_artists`` is ``figure.draw_series_axes``'s return value (one
|
|
59
|
+
artist per series, in order -- a ``Line2D`` normally, a
|
|
60
|
+
``PathCollection`` for a colour-mapped-scatter series) rather than
|
|
61
|
+
re-derived from ``ax.lines``: a colour-mapped series draws via
|
|
62
|
+
``ax.scatter``, so it has NO entry in ``ax.lines`` at all -- indexing
|
|
63
|
+
``ax.lines[:n_series]`` would silently misalign every series hit-box
|
|
64
|
+
after it. ``x_scale``/``y_scale`` are the ALREADY-RESOLVED scale names
|
|
65
|
+
(MAIN #12) -- not re-derived from ``ax.get_xscale()``, which reports a
|
|
66
|
+
reciprocal axis as ``"function"`` (matplotlib's generic custom-scale
|
|
67
|
+
name), not ``"reciprocal"`` -- the client's ``lib/previewmap.ts`` needs
|
|
68
|
+
the real name to invert a preview pixel drag back to data coordinates
|
|
69
|
+
(``pxToData``)."""
|
|
70
|
+
fig.set_dpi(dpi)
|
|
71
|
+
fig.canvas.draw()
|
|
72
|
+
renderer = fig.canvas.get_renderer()
|
|
73
|
+
width, height = fig.canvas.get_width_height()
|
|
74
|
+
|
|
75
|
+
elements: list[dict[str, Any]] = []
|
|
76
|
+
|
|
77
|
+
def add(el_id: str, artist: Any) -> None:
|
|
78
|
+
try:
|
|
79
|
+
bbox = _artist_window_extent(artist, renderer)
|
|
80
|
+
except (RuntimeError, AttributeError):
|
|
81
|
+
return
|
|
82
|
+
if bbox.width <= 0 or bbox.height <= 0:
|
|
83
|
+
return
|
|
84
|
+
elements.append({"id": el_id, **_bbox_to_pixels(bbox, height)})
|
|
85
|
+
|
|
86
|
+
if ax.get_title():
|
|
87
|
+
add("title", ax.title)
|
|
88
|
+
if ax.get_xlabel():
|
|
89
|
+
add("xlabel", ax.xaxis.label)
|
|
90
|
+
if ax.get_ylabel():
|
|
91
|
+
add("ylabel", ax.yaxis.label)
|
|
92
|
+
if ax.get_legend() is not None:
|
|
93
|
+
add("legend", ax.get_legend())
|
|
94
|
+
for i, artist in enumerate(series_artists):
|
|
95
|
+
add(f"series:{i}", artist)
|
|
96
|
+
for i, txt in enumerate(ax.texts):
|
|
97
|
+
add(f"ann:{i}", txt)
|
|
98
|
+
|
|
99
|
+
axes_px = _bbox_to_pixels(ax.get_window_extent(renderer), height)
|
|
100
|
+
buf = BytesIO()
|
|
101
|
+
fig.savefig(buf, format="png")
|
|
102
|
+
return {
|
|
103
|
+
"image": base64.b64encode(buf.getvalue()).decode("ascii"),
|
|
104
|
+
"width": int(width),
|
|
105
|
+
"height": int(height),
|
|
106
|
+
"elements": elements,
|
|
107
|
+
"axes": {
|
|
108
|
+
**axes_px,
|
|
109
|
+
"xlim": [float(v) for v in ax.get_xlim()],
|
|
110
|
+
"ylim": [float(v) for v in ax.get_ylim()],
|
|
111
|
+
"xlog": x_scale == "log",
|
|
112
|
+
"ylog": y_scale == "log",
|
|
113
|
+
"xscale": x_scale,
|
|
114
|
+
"yscale": y_scale,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Mathtext label guard for figure export (GOTO #5 rich-text labels).
|
|
2
|
+
|
|
3
|
+
Labels flow from the UI as plain text with optional ``$...$`` mathtext
|
|
4
|
+
regions (the frontend's ``lib/richtext`` micro-syntax is a strict subset of
|
|
5
|
+
matplotlib mathtext, so passthrough is the rendering mechanism). matplotlib
|
|
6
|
+
parses math regions at DRAW time, which means an invalid label would raise
|
|
7
|
+
inside ``fig.savefig`` and turn an export into a 500. This module is the
|
|
8
|
+
shared chokepoint every ``calc.figure*`` renderer routes its user-supplied
|
|
9
|
+
strings through: it pre-validates with matplotlib's own mathtext parser and,
|
|
10
|
+
on failure, escapes the dollar signs so the label renders as literal text
|
|
11
|
+
(matplotlib renders ``\\$`` as a plain ``$``) -- an invalid label must NEVER
|
|
12
|
+
error an export.
|
|
13
|
+
|
|
14
|
+
Pure layer: string in -> string out. matplotlib is imported lazily (same
|
|
15
|
+
convention as the figure modules -- the heavy import is paid only on export).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from functools import lru_cache
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
__all__ = ["safe_mathtext_label"]
|
|
25
|
+
|
|
26
|
+
# A "$" not preceded by a backslash -- matplotlib's own math-region rule
|
|
27
|
+
# (matplotlib.cbook.is_math_text counts these; an odd count means the string
|
|
28
|
+
# is rendered as literal text, dollars visible, no mathtext at all).
|
|
29
|
+
_UNESCAPED_DOLLAR = re.compile(r"(?<!\\)\$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@lru_cache(maxsize=1)
|
|
33
|
+
def _parser() -> Any:
|
|
34
|
+
"""The mathtext trial parser (cached -- construction is not free)."""
|
|
35
|
+
from matplotlib.mathtext import MathTextParser
|
|
36
|
+
|
|
37
|
+
return MathTextParser("agg")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def safe_mathtext_label(label: str) -> str:
|
|
41
|
+
"""Return ``label`` unchanged when it renders safely; else de-math it.
|
|
42
|
+
|
|
43
|
+
- No ``$`` at all: fast path, returned as-is (byte-identical to today).
|
|
44
|
+
- Odd count of unescaped ``$``: matplotlib already treats the whole
|
|
45
|
+
string as literal text -- returned as-is.
|
|
46
|
+
- Balanced ``$...$`` regions: trial-parsed with matplotlib's mathtext
|
|
47
|
+
parser. Valid -> unchanged (mathtext renders it at export). Invalid ->
|
|
48
|
+
every unescaped ``$`` is escaped to ``\\$`` so matplotlib renders the
|
|
49
|
+
literal string (dollars visible) instead of raising at savefig time.
|
|
50
|
+
"""
|
|
51
|
+
if not label or "$" not in label:
|
|
52
|
+
return label
|
|
53
|
+
n_math = len(_UNESCAPED_DOLLAR.findall(label))
|
|
54
|
+
if n_math == 0 or n_math % 2 == 1:
|
|
55
|
+
# Zero unescaped $ (all \$-escaped) or an odd count: matplotlib
|
|
56
|
+
# already renders the whole string as literal text -- leave it be.
|
|
57
|
+
return label
|
|
58
|
+
try:
|
|
59
|
+
_parser().parse(label)
|
|
60
|
+
except Exception: # ANY parse failure means "render literal", never raise
|
|
61
|
+
return _UNESCAPED_DOLLAR.sub(r"\\$", label)
|
|
62
|
+
return label
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Publication rendering for 2-D maps: contour, filled contour, 3-D surface.
|
|
2
|
+
|
|
3
|
+
ORIGIN_GAP_PLAN #17 (filled/labeled contour, incl. the scattered tri-contour
|
|
4
|
+
remainder) + #19 (3-D static export). Pure layer: either a gridded map
|
|
5
|
+
(``x_axis``/``y_axis``/``z_grid``, the ``calc.map.MapData`` shape,
|
|
6
|
+
``contour_source="grid"``) or a raw scattered cloud (``x_axis``/``y_axis``/
|
|
7
|
+
``z_values`` all the same length -- the RSM point-cloud shape produced by
|
|
8
|
+
``io/_xrdml_scan.py``'s snapshot/coupled layouts, ``contour_source="points"``)
|
|
9
|
+
in -> image bytes out, via matplotlib (vector by default), matching
|
|
10
|
+
``calc.figure.render_figure``'s style/format/dpi/tick conventions so 1-D and
|
|
11
|
+
2-D exports share presets. 3-D (surface / scatter / waterfall) is the static
|
|
12
|
+
publication path; interactive 3-D is deferred (#22).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from io import BytesIO
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import matplotlib
|
|
21
|
+
|
|
22
|
+
matplotlib.use("Agg") # headless
|
|
23
|
+
|
|
24
|
+
import matplotlib.pyplot as plt # noqa: E402
|
|
25
|
+
import matplotlib.tri as mtri # noqa: E402
|
|
26
|
+
import numpy as np # noqa: E402
|
|
27
|
+
from mpl_toolkits.mplot3d import Axes3D # noqa: E402,F401 (registers the 3d projection)
|
|
28
|
+
from numpy.typing import ArrayLike, NDArray # noqa: E402
|
|
29
|
+
|
|
30
|
+
from quantized.calc.figure_labels import safe_mathtext_label # noqa: E402
|
|
31
|
+
from quantized.calc.figure_styles import figure_style # noqa: E402
|
|
32
|
+
|
|
33
|
+
__all__ = ["MAP_KINDS", "render_map_figure"]
|
|
34
|
+
|
|
35
|
+
_FORMATS = ("pdf", "svg", "png", "tiff")
|
|
36
|
+
MAP_KINDS = ("contourf", "contour", "heatmap", "surface", "scatter3d", "waterfall")
|
|
37
|
+
_3D_KINDS = ("surface", "scatter3d", "waterfall")
|
|
38
|
+
_CONTOUR_SOURCES = ("grid", "points")
|
|
39
|
+
_POINTS_KINDS = ("contour", "contourf") # tricontour has no heatmap/3-D analogue here
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _contour_levels(
|
|
43
|
+
z_min: float, z_max: float, levels: int | list[float], scale: str
|
|
44
|
+
) -> NDArray[np.float64]:
|
|
45
|
+
"""Explicit contour levels: ``levels`` count (or a list), lin or log spaced."""
|
|
46
|
+
if isinstance(levels, (list, tuple)):
|
|
47
|
+
arr = np.asarray(sorted(float(x) for x in levels), dtype=float)
|
|
48
|
+
if arr.size < 2: # matplotlib's contourf needs >= 2 levels
|
|
49
|
+
raise ValueError("levels list needs at least 2 entries")
|
|
50
|
+
return arr
|
|
51
|
+
n = int(levels)
|
|
52
|
+
if n < 2:
|
|
53
|
+
raise ValueError("levels count must be >= 2")
|
|
54
|
+
if not (np.isfinite(z_min) and np.isfinite(z_max) and z_max > z_min):
|
|
55
|
+
raise ValueError("map has no finite z-range to contour")
|
|
56
|
+
if scale == "log":
|
|
57
|
+
if z_max <= 0:
|
|
58
|
+
raise ValueError("log level_scale needs a positive z-range")
|
|
59
|
+
lo = z_min if z_min > 0 else z_max * 1e-3
|
|
60
|
+
return np.asarray(np.logspace(np.log10(lo), np.log10(z_max), n), dtype=float)
|
|
61
|
+
if scale != "linear":
|
|
62
|
+
raise ValueError("level_scale must be 'linear' or 'log'")
|
|
63
|
+
return np.asarray(np.linspace(z_min, z_max, n), dtype=float)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def render_map_figure(
|
|
67
|
+
x_axis: ArrayLike,
|
|
68
|
+
y_axis: ArrayLike,
|
|
69
|
+
z_grid: ArrayLike | None = None,
|
|
70
|
+
*,
|
|
71
|
+
contour_source: str = "grid",
|
|
72
|
+
z_values: ArrayLike | None = None,
|
|
73
|
+
kind: str = "contourf",
|
|
74
|
+
title: str = "",
|
|
75
|
+
x_label: str = "",
|
|
76
|
+
y_label: str = "",
|
|
77
|
+
z_label: str = "",
|
|
78
|
+
fmt: str = "pdf",
|
|
79
|
+
style: str = "default",
|
|
80
|
+
cmap: str = "viridis",
|
|
81
|
+
levels: int | list[float] = 12,
|
|
82
|
+
level_scale: str = "linear",
|
|
83
|
+
label_contours: bool = True,
|
|
84
|
+
colorbar: bool = True,
|
|
85
|
+
width_in: float | None = None,
|
|
86
|
+
height_in: float | None = None,
|
|
87
|
+
dpi: int | None = None,
|
|
88
|
+
view_elev: float = 30.0,
|
|
89
|
+
view_azim: float = -60.0,
|
|
90
|
+
) -> bytes:
|
|
91
|
+
"""Render a 2-D map to image bytes in the chosen ``kind``.
|
|
92
|
+
|
|
93
|
+
Two input shapes, selected by ``contour_source``:
|
|
94
|
+
|
|
95
|
+
- ``"grid"`` (default) — ``z_grid`` is ``(ny, nx)`` over ``x_axis``
|
|
96
|
+
``(nx,)`` / ``y_axis`` ``(ny,)`` (NaN outside the data hull is left
|
|
97
|
+
blank), the regridded ``calc.map.MapData`` shape. All of ``MAP_KINDS``
|
|
98
|
+
are available.
|
|
99
|
+
- ``"points"`` — ``x_axis`` / ``y_axis`` / ``z_values`` are raw scattered
|
|
100
|
+
arrays of equal length (e.g. an RSM point cloud straight off
|
|
101
|
+
``io/_xrdml_scan.py``'s snapshot/coupled layouts, never regridded).
|
|
102
|
+
Only ``kind`` ``"contour"`` / ``"contourf"`` apply; the cloud is
|
|
103
|
+
Delaunay-triangulated (``matplotlib.tri.Triangulation``) and drawn with
|
|
104
|
+
``tricontour`` / ``tricontourf``. Degenerate input (e.g. collinear
|
|
105
|
+
points, or fewer than 3 finite points) raises ``ValueError``.
|
|
106
|
+
|
|
107
|
+
``kind``:
|
|
108
|
+
|
|
109
|
+
- ``contourf`` / ``contour`` — filled / line contours; ``levels`` is a
|
|
110
|
+
count or explicit list, ``level_scale`` lin or log, ``label_contours``
|
|
111
|
+
draws inline labels on the line variant. Same level semantics
|
|
112
|
+
(:func:`_contour_levels`) for both ``contour_source`` values.
|
|
113
|
+
- ``heatmap`` — ``pcolormesh`` of the grid (``"grid"`` source only).
|
|
114
|
+
- ``surface`` / ``scatter3d`` / ``waterfall`` — static 3-D (mplot3d),
|
|
115
|
+
viewed from (``view_elev``, ``view_azim``) (``"grid"`` source only).
|
|
116
|
+
|
|
117
|
+
``fmt`` / ``style`` / size overrides match ``render_figure``. ``dpi``
|
|
118
|
+
defaults to the style preset's calibrated resolution when not given
|
|
119
|
+
(``None``), same as ``calc.figure``'s ``resolved_dpi`` convention; the
|
|
120
|
+
preset's box-tick convention (``xtick.top``/``ytick.right`` mirrored
|
|
121
|
+
when the preset draws a closed box) is honored too.
|
|
122
|
+
"""
|
|
123
|
+
if fmt not in _FORMATS:
|
|
124
|
+
raise ValueError(f"fmt must be one of {_FORMATS}")
|
|
125
|
+
if kind not in MAP_KINDS:
|
|
126
|
+
raise ValueError(f"kind must be one of {MAP_KINDS}")
|
|
127
|
+
if contour_source not in _CONTOUR_SOURCES:
|
|
128
|
+
raise ValueError(f"contour_source must be one of {_CONTOUR_SOURCES}")
|
|
129
|
+
# Rich-text labels (GOTO #5): de-math INVALID $...$ so savefig never raises.
|
|
130
|
+
title = safe_mathtext_label(title)
|
|
131
|
+
x_label = safe_mathtext_label(x_label)
|
|
132
|
+
y_label = safe_mathtext_label(y_label)
|
|
133
|
+
z_label = safe_mathtext_label(z_label)
|
|
134
|
+
st = figure_style(style)
|
|
135
|
+
resolved_dpi = int(dpi) if dpi is not None else int(st.dpi)
|
|
136
|
+
|
|
137
|
+
if contour_source == "points":
|
|
138
|
+
if kind not in _POINTS_KINDS:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"contour_source='points' only supports kind {_POINTS_KINDS}, got {kind!r}"
|
|
141
|
+
)
|
|
142
|
+
if z_values is None:
|
|
143
|
+
raise ValueError("z_values is required when contour_source='points'")
|
|
144
|
+
xr = np.asarray(x_axis, dtype=float).ravel()
|
|
145
|
+
yr = np.asarray(y_axis, dtype=float).ravel()
|
|
146
|
+
zr = np.asarray(z_values, dtype=float).ravel()
|
|
147
|
+
if not (xr.size == yr.size == zr.size):
|
|
148
|
+
raise ValueError(
|
|
149
|
+
"x_axis/y_axis/z_values must have the same length for "
|
|
150
|
+
f"contour_source='points', got {xr.size}/{yr.size}/{zr.size}"
|
|
151
|
+
)
|
|
152
|
+
finite = np.isfinite(xr) & np.isfinite(yr) & np.isfinite(zr)
|
|
153
|
+
x, y, z = xr[finite], yr[finite], zr[finite]
|
|
154
|
+
if x.size < 3:
|
|
155
|
+
raise ValueError("need at least 3 finite points to triangulate a scattered contour")
|
|
156
|
+
z_min, z_max = float(np.min(z)), float(np.max(z))
|
|
157
|
+
else:
|
|
158
|
+
if z_grid is None:
|
|
159
|
+
raise ValueError("z_grid is required when contour_source='grid'")
|
|
160
|
+
x = np.asarray(x_axis, dtype=float).ravel()
|
|
161
|
+
y = np.asarray(y_axis, dtype=float).ravel()
|
|
162
|
+
z = np.asarray(z_grid, dtype=float)
|
|
163
|
+
if z.ndim != 2 or z.shape != (y.size, x.size):
|
|
164
|
+
raise ValueError(f"z_grid must be (ny, nx) = ({y.size}, {x.size}), got {z.shape}")
|
|
165
|
+
if x.size < 2 or y.size < 2:
|
|
166
|
+
raise ValueError(f"a map needs at least a 2x2 grid, got ({y.size}, {x.size})")
|
|
167
|
+
|
|
168
|
+
if np.any(np.isfinite(z)):
|
|
169
|
+
z_min, z_max = float(np.nanmin(z)), float(np.nanmax(z))
|
|
170
|
+
else:
|
|
171
|
+
z_min = z_max = float("nan") # all-gaps map; contour kinds raise below
|
|
172
|
+
|
|
173
|
+
figsize = (width_in or st.fig_width_in, height_in or st.fig_height_in)
|
|
174
|
+
fallback = "DejaVu Serif" if st.font_generic == "serif" else "DejaVu Sans"
|
|
175
|
+
rc: dict[str, Any] = {
|
|
176
|
+
"font.family": st.font_generic,
|
|
177
|
+
f"font.{st.font_generic}": [st.font_name, fallback],
|
|
178
|
+
"font.size": st.font_size,
|
|
179
|
+
"axes.labelsize": st.font_size,
|
|
180
|
+
"axes.titlesize": st.title_font_size,
|
|
181
|
+
# Mirror ticks onto the top/right spines whenever the preset draws a
|
|
182
|
+
# closed box (matches calc.figure's convention; matplotlib's default
|
|
183
|
+
# leaves top/right bare even with the full rectangular border).
|
|
184
|
+
"xtick.top": st.box_on,
|
|
185
|
+
"ytick.right": st.box_on,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
with matplotlib.rc_context(rc): # type: ignore[arg-type]
|
|
189
|
+
if kind in _3D_KINDS:
|
|
190
|
+
fig = plt.figure(figsize=figsize)
|
|
191
|
+
ax = fig.add_subplot(projection="3d")
|
|
192
|
+
else:
|
|
193
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
194
|
+
try:
|
|
195
|
+
mappable = _draw(
|
|
196
|
+
ax, kind, x, y, z, z_min, z_max, cmap, levels, level_scale,
|
|
197
|
+
label_contours, view_elev, view_azim, contour_source=contour_source,
|
|
198
|
+
)
|
|
199
|
+
if title:
|
|
200
|
+
ax.set_title(title)
|
|
201
|
+
if x_label:
|
|
202
|
+
ax.set_xlabel(x_label)
|
|
203
|
+
if y_label:
|
|
204
|
+
ax.set_ylabel(y_label)
|
|
205
|
+
if kind in _3D_KINDS and z_label:
|
|
206
|
+
ax.set_zlabel(z_label)
|
|
207
|
+
if colorbar and mappable is not None:
|
|
208
|
+
fig.colorbar(mappable, ax=ax, label=z_label or None, shrink=0.8)
|
|
209
|
+
fig.tight_layout()
|
|
210
|
+
buf = BytesIO()
|
|
211
|
+
fig.savefig(buf, format=fmt, dpi=resolved_dpi)
|
|
212
|
+
return buf.getvalue()
|
|
213
|
+
finally:
|
|
214
|
+
plt.close(fig)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _draw(
|
|
218
|
+
ax: Any,
|
|
219
|
+
kind: str,
|
|
220
|
+
x: NDArray[np.float64],
|
|
221
|
+
y: NDArray[np.float64],
|
|
222
|
+
z: NDArray[np.float64],
|
|
223
|
+
z_min: float,
|
|
224
|
+
z_max: float,
|
|
225
|
+
cmap: str,
|
|
226
|
+
levels: int | list[float],
|
|
227
|
+
level_scale: str,
|
|
228
|
+
label_contours: bool,
|
|
229
|
+
view_elev: float,
|
|
230
|
+
view_azim: float,
|
|
231
|
+
*,
|
|
232
|
+
contour_source: str = "grid",
|
|
233
|
+
) -> Any:
|
|
234
|
+
"""Draw the requested mark; return the colorbar mappable (or None)."""
|
|
235
|
+
if contour_source == "points":
|
|
236
|
+
# x/y/z are flat, equal-length scattered arrays here (not a grid) --
|
|
237
|
+
# Delaunay-triangulate the cloud and contour straight off the
|
|
238
|
+
# triangulation, no regridding. Same level semantics as the grid path.
|
|
239
|
+
lv = _contour_levels(z_min, z_max, levels, level_scale)
|
|
240
|
+
try:
|
|
241
|
+
tri = mtri.Triangulation(x, y)
|
|
242
|
+
except RuntimeError as exc:
|
|
243
|
+
# qhull raises RuntimeError on degenerate input (e.g. all points
|
|
244
|
+
# collinear) -- surface it as a clean ValueError (-> 422), not a
|
|
245
|
+
# matplotlib internals leak.
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"points are degenerate (e.g. collinear) and cannot be triangulated"
|
|
248
|
+
) from exc
|
|
249
|
+
if kind == "contourf":
|
|
250
|
+
return ax.tricontourf(tri, z, levels=lv, cmap=cmap)
|
|
251
|
+
cs = ax.tricontour(tri, z, levels=lv, cmap=cmap)
|
|
252
|
+
if label_contours:
|
|
253
|
+
ax.clabel(cs, inline=True, fontsize=7, fmt="%.3g")
|
|
254
|
+
return cs
|
|
255
|
+
|
|
256
|
+
if kind == "heatmap":
|
|
257
|
+
return ax.pcolormesh(x, y, z, cmap=cmap, shading="auto")
|
|
258
|
+
if kind in ("contourf", "contour"):
|
|
259
|
+
lv = _contour_levels(z_min, z_max, levels, level_scale)
|
|
260
|
+
if kind == "contourf":
|
|
261
|
+
return ax.contourf(x, y, z, levels=lv, cmap=cmap)
|
|
262
|
+
cs = ax.contour(x, y, z, levels=lv, cmap=cmap)
|
|
263
|
+
if label_contours:
|
|
264
|
+
ax.clabel(cs, inline=True, fontsize=7, fmt="%.3g")
|
|
265
|
+
return cs
|
|
266
|
+
|
|
267
|
+
# 3-D kinds
|
|
268
|
+
ax.view_init(elev=view_elev, azim=view_azim)
|
|
269
|
+
xg, yg = np.meshgrid(x, y) # (ny, nx)
|
|
270
|
+
if kind == "surface":
|
|
271
|
+
return ax.plot_surface(xg, yg, z, cmap=cmap, linewidth=0, antialiased=True)
|
|
272
|
+
if kind == "scatter3d":
|
|
273
|
+
finite = np.isfinite(z)
|
|
274
|
+
sc = ax.scatter(
|
|
275
|
+
xg[finite], yg[finite], z[finite], c=z[finite], cmap=cmap, s=6, depthshade=True
|
|
276
|
+
)
|
|
277
|
+
return sc
|
|
278
|
+
# waterfall: one profile line per y-row, stacked in depth
|
|
279
|
+
line_cmap = matplotlib.colormaps[cmap]
|
|
280
|
+
for j in range(y.size):
|
|
281
|
+
row = z[j]
|
|
282
|
+
finite = np.isfinite(row)
|
|
283
|
+
if not finite.any():
|
|
284
|
+
continue
|
|
285
|
+
frac = j / max(1, y.size - 1)
|
|
286
|
+
ax.plot(x[finite], np.full(int(finite.sum()), y[j]), row[finite], color=line_cmap(frac))
|
|
287
|
+
return None
|