diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1442bb8..17a97da 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -53,6 +53,20 @@ jobs: - name: Install Playwright browser run: uv run playwright install chromium --with-deps + # ── Build Pyodide wheel ─────────────────────────────────────────────── + # Produces docs/_static/wheels/anyplotlib-0.0.0-py3-none-any.whl so the + # in-browser Pyodide bridge can install the exact source tree that built + # these docs — no PyPI release required. + - name: Build Pyodide wheel + run: | + mkdir -p docs/_static/wheels + uv build --wheel --out-dir docs/_static/wheels/ + # Rename to the stable sentinel name micropip expects for URL installs. + cd docs/_static/wheels + for f in anyplotlib-*.whl; do + [ "$f" != "anyplotlib-0.0.0-py3-none-any.whl" ] && mv "$f" anyplotlib-0.0.0-py3-none-any.whl + done + # ── Sphinx build ───────────────────────────────────────────────────── # -W turns warnings into errors; --keep-going collects all of them. - name: Build HTML documentation @@ -110,6 +124,16 @@ jobs: echo "dest_dir=dev" >> "$GITHUB_OUTPUT" fi + # ── Build Pyodide wheel ─────────────────────────────────────────────── + - name: Build Pyodide wheel + run: | + mkdir -p docs/_static/wheels + uv build --wheel --out-dir docs/_static/wheels/ + cd docs/_static/wheels + for f in anyplotlib-*.whl; do + [ "$f" != "anyplotlib-0.0.0-py3-none-any.whl" ] && mv "$f" anyplotlib-0.0.0-py3-none-any.whl + done + # ── Sphinx build ───────────────────────────────────────────────────── - name: Build HTML documentation env: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c0a1cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Python bytecode / caches +__pycache__/ +*.py[cod] +*$py.class +*.pyo + +# Distribution / packaging +dist/ +build/ +*.egg-info/ +*.egg +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# Test / coverage artefacts +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ + +# Jupyter notebooks checkpoints +.ipynb_checkpoints/ + +# Sphinx build output +docs/_build/ +build/html/ +build/doctrees/ + +# Generated Pyodide wheel (built by workflow / make html — never commit) +docs/_static/wheels/ + +# Editor / IDE +.idea/ +.vscode/ +*.swp +*.swo + +# macOS +.DS_Store + + diff --git a/Examples/Interactive/plot_interactive_fft.py b/Examples/Interactive/plot_interactive_fft.py index 20e2773..f274cc4 100644 --- a/Examples/Interactive/plot_interactive_fft.py +++ b/Examples/Interactive/plot_interactive_fft.py @@ -178,7 +178,4 @@ def _roi_released(event): v_fft.set_data(log_mag, x_axis=freq_x, y_axis=freq_y, units="1/\u00c5") -fig - - - +fig # Interactive diff --git a/Examples/Interactive/plot_interactive_fitting.py b/Examples/Interactive/plot_interactive_fitting.py index 06edc41..8acd7b6 100644 --- a/Examples/Interactive/plot_interactive_fitting.py +++ b/Examples/Interactive/plot_interactive_fitting.py @@ -19,6 +19,9 @@ and the sum curve will jump to the optimal fit. Click a component line again to hide its widgets. """ +# Packages required when running interactively in Pyodide (docs live mode). +_PYODIDE_PACKAGES = ["scipy"] + import numpy as np from scipy.optimize import curve_fit import anyplotlib as apl @@ -289,4 +292,4 @@ def _on_fit(event): def _clicked(event, c=comp): c.toggle() -fig \ No newline at end of file +fig # Interactive \ No newline at end of file diff --git a/Examples/Interactive/plot_key_bindings.py b/Examples/Interactive/plot_key_bindings.py index d3c1f22..3ec8505 100644 --- a/Examples/Interactive/plot_key_bindings.py +++ b/Examples/Interactive/plot_key_bindings.py @@ -118,5 +118,4 @@ def log_key(event): print(f"[on_key] key={event.key!r} img={pos}" f" last_widget={event.last_widget_id!r}") -fig - +fig # Interactive diff --git a/Examples/Interactive/plot_point_widget.py b/Examples/Interactive/plot_point_widget.py index 7f86f8a..c614b90 100644 --- a/Examples/Interactive/plot_point_widget.py +++ b/Examples/Interactive/plot_point_widget.py @@ -105,5 +105,5 @@ def _settled(event): _draw_tangent(event.x) -fig +fig # Interactive diff --git a/Examples/Interactive/plot_segment_by_contrast.py b/Examples/Interactive/plot_segment_by_contrast.py index fe559c3..e195dcc 100644 --- a/Examples/Interactive/plot_segment_by_contrast.py +++ b/Examples/Interactive/plot_segment_by_contrast.py @@ -232,6 +232,6 @@ def _delete_nearest(event): _refresh() -fig +fig # Interactive diff --git a/Examples/plot_3d.py b/Examples/plot_3d.py deleted file mode 100644 index fb52745..0000000 --- a/Examples/plot_3d.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -3D Plotting -=========== - -Demonstrate the three 3-D geometry types supported by -:meth:`~anyplotlib.figure_plots.Axes.plot_surface`, -:meth:`~anyplotlib.figure_plots.Axes.scatter3d`, and -:meth:`~anyplotlib.figure_plots.Axes.plot3d`. -Drag to rotate, scroll to zoom, press **R** to reset the view. -""" -import numpy as np -import anyplotlib as vw - -# ── Surface ─────────────────────────────────────────────────────────────────── -x = np.linspace(-3, 3, 60) -y = np.linspace(-3, 3, 60) -XX, YY = np.meshgrid(x, y) -ZZ = np.sin(np.sqrt(XX ** 2 + YY ** 2)) - -fig, ax = vw.subplots(1, 1, figsize=(520, 480)) -surf = ax.plot_surface(XX, YY, ZZ, - colormap="viridis", - x_label="x", y_label="y", z_label="sin(r)") - -fig - -# %% -# Scatter plot -# ------------ - -rng = np.random.default_rng(1) -n = 300 -theta = rng.uniform(0, 2 * np.pi, n) -phi = rng.uniform(0, np.pi, n) -r = rng.uniform(0.6, 1.0, n) -xs = r * np.sin(phi) * np.cos(theta) -ys = r * np.sin(phi) * np.sin(theta) -zs = r * np.cos(phi) - -fig2, ax2 = vw.subplots(1, 1, figsize=(480, 480)) -sc = ax2.scatter3d(xs, ys, zs, - color="#4fc3f7", point_size=3, - x_label="x", y_label="y", z_label="z") - -fig2 - -# %% -# 3-D line — parametric helix -# ---------------------------- - -t = np.linspace(0, 4 * np.pi, 300) -hx = np.cos(t) -hy = np.sin(t) -hz = t / (4 * np.pi) - -fig3, ax3 = vw.subplots(1, 1, figsize=(480, 480)) -ln = ax3.plot3d(hx, hy, hz, - color="#ff7043", linewidth=2, - x_label="cos t", y_label="sin t", z_label="t") - -fig3 - -# %% -# Update the surface data live -# ---------------------------- -# Call :meth:`~anyplotlib.figure_plots.Plot3D.set_data` to replace the geometry -# without recreating the panel. - -ZZ2 = np.cos(np.sqrt(XX ** 2 + YY ** 2)) -surf.set_data(XX, YY, ZZ2) -surf.set_colormap("plasma") -surf.set_view(azimuth=30, elevation=40) - -fig diff --git a/Examples/plot_bar.py b/Examples/plot_bar.py deleted file mode 100644 index 05772d0..0000000 --- a/Examples/plot_bar.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -Bar Chart -========= - -Demonstrate :meth:`~anyplotlib.figure_plots.Axes.bar` with: - -* **Matplotlib-aligned API** — ``ax.bar(x, height, width, bottom, …)`` -* Vertical and horizontal orientations, per-bar colours, category labels -* **Grouped bars** — pass a 2-D *height* array ``(N, G)`` -* **Log-scale value axis** — ``log_scale=True`` -* Live data updates via :meth:`~anyplotlib.figure_plots.PlotBar.set_data` -""" -import numpy as np -import anyplotlib as vw - -rng = np.random.default_rng(7) - -# ── 1. Vertical bar chart — monthly sales ──────────────────────────────────── -# The first positional argument is now *x* (positions or labels), matching -# ``matplotlib.pyplot.bar(x, height, width=0.8, bottom=0.0, ...)``. -months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] -sales = np.array([42, 55, 48, 63, 71, 68, 74, 81, 66, 59, 52, 78], - dtype=float) - -fig1, ax1 = vw.subplots(1, 1, figsize=(640, 340)) -bar1 = ax1.bar( - months, # x — category strings become x_labels automatically - sales, # height - width=0.6, - color="#4fc3f7", - show_values=True, - units="Month", - y_units="Units sold", -) -fig1 - -# %% -# Horizontal bar chart — ranked items -# ------------------------------------- -# Set ``orient="h"`` for a horizontal layout. Pass a list of CSS colours -# to ``colors`` to give each bar its own colour. - -categories = ["NumPy", "SciPy", "Matplotlib", "Pandas", "Scikit-learn", - "PyTorch", "TensorFlow", "JAX", "Polars", "Dask"] -scores = np.array([95, 88, 91, 87, 83, 79, 76, 72, 68, 65], dtype=float) - -palette = [ - "#ef5350", "#ec407a", "#ab47bc", "#7e57c2", "#42a5f5", - "#26c6da", "#26a69a", "#66bb6a", "#d4e157", "#ffa726", -] - -fig2, ax2 = vw.subplots(1, 1, figsize=(540, 400)) -bar2 = ax2.bar( - categories, - scores, - orient="h", - colors=palette, - width=0.65, - show_values=True, - y_units="Popularity score", -) -fig2 - -# %% -# Grouped bar chart — quarterly comparison -# ----------------------------------------- -# Pass a 2-D *height* array of shape ``(N, G)`` to draw *G* bars side by -# side for each category. Provide ``group_labels`` to show a legend and -# ``group_colors`` to customise each group's colour. - -quarters = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"] -q_data = np.array([ - [42, 58, 51], # Jan — Q1, Q2, Q3 - [55, 61, 59], # Feb - [48, 70, 65], # Mar - [63, 75, 71], # Apr - [71, 69, 80], # May - [68, 83, 77], # Jun -], dtype=float) # shape (6, 3) → 6 categories, 3 groups - -fig3, ax3 = vw.subplots(1, 1, figsize=(680, 340)) -bar3 = ax3.bar( - quarters, - q_data, - width=0.8, - group_labels=["Q1", "Q2", "Q3"], - group_colors=["#4fc3f7", "#ff7043", "#66bb6a"], - show_values=False, - y_units="Sales", -) -fig3 - -# %% -# Log-scale value axis -# --------------------- -# Set ``log_scale=True`` for a logarithmic value axis. Non-positive values -# are clamped to ``1e-10`` — no error is raised. Tick marks are placed at -# each decade (10⁰, 10¹, 10², …) with faint minor gridlines at 2×, 3×, 5× -# multiples. - -log_labels = ["A", "B", "C", "D", "E"] -log_vals = np.array([1, 10, 100, 1_000, 10_000], dtype=float) - -fig4, ax4 = vw.subplots(1, 1, figsize=(500, 340)) -bar4 = ax4.bar( - log_labels, - log_vals, - log_scale=True, - color="#ab47bc", - show_values=True, - y_units="Count (log scale)", -) -fig4 - -# %% -# Side-by-side comparison — update data live -# ------------------------------------------- -# Place two :class:`~anyplotlib.figure_plots.PlotBar` panels in one figure. -# Call :meth:`~anyplotlib.figure_plots.PlotBar.set_data` to swap in Q2 data — -# the value-axis range recalculates automatically. - -q1 = np.array([42, 55, 48, 63, 71, 68, 74, 81, 66, 59, 52, 78], dtype=float) -q2 = np.array([58, 61, 70, 75, 69, 83, 90, 88, 77, 64, 71, 95], dtype=float) -all_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - -fig5, (ax_left, ax_right) = vw.subplots(1, 2, figsize=(820, 320)) -bar_left = ax_left.bar( - all_months, q1, width=0.6, - color="#4fc3f7", show_values=False, y_units="Q1 sales", -) -bar_right = ax_right.bar( - all_months, q1, width=0.6, - color="#ff7043", show_values=False, y_units="Q2 sales", -) -bar_right.set_data(q2) # swap in Q2 — axis range recalculates automatically - -fig5 - -# %% -# Mutate colours, annotations, and scale at runtime -# -------------------------------------------------- -# :meth:`~anyplotlib.figure_plots.PlotBar.set_color` repaints all bars, -# :meth:`~anyplotlib.figure_plots.PlotBar.set_show_values` toggles labels, -# :meth:`~anyplotlib.figure_plots.PlotBar.set_log_scale` switches the -# value-axis between linear and logarithmic. - -bar1.set_color("#ff7043") -bar1.set_show_values(False) -fig1 - -import numpy as np -import anyplotlib as vw - -rng = np.random.default_rng(7) - -# ── 1. Vertical bar chart — monthly sales ──────────────────────────────────── -months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] -sales = np.array([42, 55, 48, 63, 71, 68, 74, 81, 66, 59, 52, 78], - dtype=float) - -fig1, ax1 = vw.subplots(1, 1, figsize=(640, 340)) -bar1 = ax1.bar( - sales, - x_labels=months, - color="#4fc3f7", - bar_width=0.6, - show_values=True, - units="Month", - y_units="Units sold", -) -fig1 - -# %% -# Horizontal bar chart — ranked items -# ------------------------------------- -# Set ``orient="h"`` for a horizontal layout. Pass a list of CSS colours to -# ``colors`` to give each bar its own colour, and use ``show_values=True`` to -# annotate each bar with its numeric value. - -categories = ["NumPy", "SciPy", "Matplotlib", "Pandas", "Scikit-learn", - "PyTorch", "TensorFlow", "JAX", "Polars", "Dask"] -scores = np.array([95, 88, 91, 87, 83, 79, 76, 72, 68, 65], dtype=float) - -palette = [ - "#ef5350", "#ec407a", "#ab47bc", "#7e57c2", "#42a5f5", - "#26c6da", "#26a69a", "#66bb6a", "#d4e157", "#ffa726", -] - -fig2, ax2 = vw.subplots(1, 1, figsize=(540, 400)) -bar2 = ax2.bar( - scores, - x_labels=categories, - orient="h", - colors=palette, - bar_width=0.65, - show_values=True, - y_units="Popularity score", -) -fig2 - -# %% -# Side-by-side comparison — update data live -# ------------------------------------------- -# Place two :class:`~anyplotlib.figure_plots.PlotBar` panels in one -# :func:`~anyplotlib.figure_plots.subplots` figure. Call -# :meth:`~anyplotlib.figure_plots.PlotBar.set_data` to swap in Q2 data for the -# right panel, demonstrating how the axis range re-calculates automatically. - -quarters = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - -q1 = np.array([42, 55, 48, 63, 71, 68, 74, 81, 66, 59, 52, 78], dtype=float) -q2 = np.array([58, 61, 70, 75, 69, 83, 90, 88, 77, 64, 71, 95], dtype=float) - -fig3, (ax_left, ax_right) = vw.subplots(1, 2, figsize=(820, 320)) - -bar_left = ax_left.bar( - q1, - x_labels=quarters, - color="#4fc3f7", - bar_width=0.6, - show_values=False, - y_units="Q1 sales", -) - -bar_right = ax_right.bar( - q1, # start with Q1 … - x_labels=quarters, - color="#ff7043", - bar_width=0.6, - show_values=False, - y_units="Q2 sales", -) - -# Swap in Q2 data — range is recalculated automatically -bar_right.set_data(q2) - -fig3 - -# %% -# Mutate colours and annotations at runtime -# ------------------------------------------ -# :meth:`~anyplotlib.figure_plots.PlotBar.set_color` repaints all bars with a -# single CSS colour. -# :meth:`~anyplotlib.figure_plots.PlotBar.set_show_values` toggles the -# in-bar value annotations. - -bar1.set_color("#ff7043") -bar1.set_show_values(False) -fig1 - diff --git a/Examples/plot_image2d.py b/Examples/plot_image2d.py deleted file mode 100644 index ed0e168..0000000 --- a/Examples/plot_image2d.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -2D Image with Histogram -======================= - -Display a 2-D image with physical axes, a colourmap, and an interactive -histogram below — all wired together with draggable threshold widgets. - -Layout ------- -A :class:`~anyplotlib.figure_plots.GridSpec` with two rows puts the image -on top and a bar-chart histogram below. Two -:class:`~anyplotlib.widgets.VLineWidget` handles on the histogram mark the -``display_min`` / ``display_max`` thresholds; dragging them updates the -image colour scale in real time. - -Key bindings on the image panel: **R** reset view · **C** toggle colorbar · -**L** / **S** cycle colour-scale modes. - -New ``imshow`` parameters -------------------------- -``cmap`` - Colormap name passed directly to :meth:`~anyplotlib.figure_plots.Axes.imshow` - (e.g. ``"viridis"``, ``"inferno"``). Defaults to ``"gray"``. -``vmin`` / ``vmax`` - Colormap clipping limits in data units. Values outside the range are - clamped to the colormap endpoints. Defaults to the data min/max. -``origin`` - ``"upper"`` (default) places row 0 at the top (image convention). - ``"lower"`` places row 0 at the bottom (scientific / matrix convention) - and automatically reverses the y-axis so tick values increase upward. -""" -import numpy as np -import anyplotlib as apl - - -rng = np.random.default_rng(1) - -# ── Synthetic diffraction pattern ───────────────────────────────────────────── -N = 256 -x = np.linspace(-5, 5, N) # physical axis in nm -y = np.linspace(-5, 5, N) -XX, YY = np.meshgrid(x, y) -R = np.sqrt(XX ** 2 + YY ** 2) - - -def _ring(r, r0, width, amp): - return amp * np.exp(-0.5 * ((r - r0) / width) ** 2) - - -image = ( - _ring(R, 0.0, 0.30, 1.00) # central spot - + _ring(R, 2.1, 0.15, 0.55) # first-order ring - + _ring(R, 4.2, 0.15, 0.25) # second-order ring - + rng.normal(scale=0.04, size=(N, N)) -) - -# ── Layout: image (top, 3×) + histogram bar chart (bottom, 1×) ──────────────── -gs = apl.GridSpec(2, 1, height_ratios=[3, 1]) -fig = apl.Figure(figsize=(500, 640)) -ax_img = fig.add_subplot(gs[0, 0]) -ax_hist = fig.add_subplot(gs[1, 0]) - -# ── Image panel — cmap, vmin, vmax supplied directly to imshow ──────────────── -vmin_init = float(image.min()) -vmax_init = float(image.max()) - -# Pass cmap, vmin, and vmax directly — no separate set_colormap / set_clim call -# needed for the initial display. -v = ax_img.imshow(image, axes=[x, y], units="nm", - cmap="inferno", vmin=vmin_init, vmax=vmax_init) - -# First-order spot markers -dx = x[1] - x[0] - - -def phys_to_px(val): - return (np.asarray(val) - x[0]) / dx - - -spot_nm = np.array([[ 2.1, 0.0], [-2.1, 0.0], - [ 0.0, 2.1], [ 0.0, -2.1]]) -spot_px = np.column_stack([phys_to_px(spot_nm[:, 0]), - phys_to_px(spot_nm[:, 1])]) -v.add_circles(spot_px, name="spots", radius=7, - edgecolors="#00e5ff", facecolors="#00e5ff22", - labels=["g1", "g1_bar", "g2", "g2_bar"]) - -# ── Histogram bar chart ──────────────────────────────────────────────────────── -counts, edges = np.histogram(image.ravel(), bins=64) -bin_centers = 0.5 * (edges[:-1] + edges[1:]) - -h = ax_hist.bar(counts, x_centers=bin_centers, orient="v", - color="#4fc3f7", y_units="count") - -# ── Draggable threshold handles on the histogram ────────────────────────────── -wlo = h.add_vline_widget(vmin_init, color="#ff6e40") # low-threshold handle -whi = h.add_vline_widget(vmax_init, color="#ffffff") # high-threshold handle - - -@wlo.on_release -def _apply_low(event): - """Update image display_min when the low handle is released.""" - v.set_clim(vmin=event.x) - - -@whi.on_release -def _apply_high(event): - """Update image display_max when the high handle is released.""" - v.set_clim(vmax=event.x) - - -fig - -# %% -# Adjust colour map and display range -# ------------------------------------ -# :meth:`~anyplotlib.figure_plots.Plot2D.set_colormap` switches the palette; -# :meth:`~anyplotlib.figure_plots.Plot2D.set_clim` adjusts the display range. -# Both are equivalent to passing ``cmap`` / ``vmin`` / ``vmax`` at construction. - -v.set_colormap("viridis") -v.set_clim(vmin=0.0, vmax=0.8) - -fig - -# %% -# origin='lower' — scientific / matrix convention -# ------------------------------------------------ -# Passing ``origin='lower'`` places row 0 of the data at the *bottom* of the -# image, matching the matplotlib / scientific convention. The y-axis is -# automatically reversed so tick values still increase upward. - -mat = np.arange(64, dtype=float).reshape(8, 8) # row 0 = small values - -fig2, ax2 = apl.subplots() -v2 = ax2.imshow(mat, cmap="plasma", origin="lower") - -fig2 - diff --git a/Examples/plot_line_styles.py b/Examples/plot_line_styles.py deleted file mode 100644 index dafef5a..0000000 --- a/Examples/plot_line_styles.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -1D Line Styles -============== - -Demonstrates the line-style, opacity, and per-point marker parameters -available on :meth:`~anyplotlib.figure_plots.Axes.plot` and -:meth:`~anyplotlib.figure_plots.Plot1D.add_line`. - -Four separate figures are shown: - -1. **Linestyles** – all four dash patterns on one panel with a legend. -2. **Alpha (transparency)** – two overlapping sine waves, each at 40 % opacity. -3. **Marker symbols** – all seven supported symbols, each on its own offset - curve. -4. **Combined** – dashed + semi-transparent + circle-marker overlay on a solid - primary line; demonstrates post-construction setters. -""" -import numpy as np -import anyplotlib as vw - -t256 = np.linspace(0.0, 2.0 * np.pi, 256) # dense — good for dashes / alpha -t24 = np.linspace(0.0, 2.0 * np.pi, 24) # sparse — makes markers visible - -# ── 1. Linestyles ───────────────────────────────────────────────────────────── -fig1, ax1 = vw.subplots(1, 1, figsize=(580, 300)) - -plot1 = ax1.plot(np.sin(t256), color="#4fc3f7", linewidth=2, - linestyle="solid", label="solid") -plot1.add_line(np.sin(t256) + 0.6, color="#ff7043", linewidth=2, - linestyle="dashed", label="dashed (\"--\")") -plot1.add_line(np.sin(t256) + 1.2, color="#aed581", linewidth=2, - linestyle="dotted", label="dotted (\":\")") -plot1.add_line(np.sin(t256) + 1.8, color="#ce93d8", linewidth=2, - linestyle="dashdot", label="dashdot (\"-.\")") - -fig1 - -# %% -# The ``ls`` shorthand -# -------------------- -# Each linestyle has a single-character (or two-character) shorthand that -# matches the matplotlib convention: -# -# * ``"-"`` → ``"solid"`` -# * ``"--"`` → ``"dashed"`` -# * ``":"`` → ``"dotted"`` -# * ``"-."`` → ``"dashdot"`` -# -# The shorthands work on both :meth:`~anyplotlib.figure_plots.Axes.plot` -# and :meth:`~anyplotlib.figure_plots.Plot1D.add_line`: - -fig2a, ax2a = vw.subplots(1, 1, figsize=(440, 220)) -p = ax2a.plot(np.sin(t256), ls="-", color="#4fc3f7", label='ls="-"') -p.add_line(np.sin(t256) + 0.8, ls="--", color="#ff7043", label='ls="--"') -p.add_line(np.sin(t256) + 1.6, ls=":", color="#aed581", label='ls=":"') -fig2a - -# %% -# Alpha (opacity) -# --------------- -# ``alpha`` controls line opacity on a 0–1 scale. Values below 1 let -# overlapping curves show through each other — useful for comparing signals -# that share the same amplitude range. - -fig2, ax2 = vw.subplots(1, 1, figsize=(580, 300)) - -plot2 = ax2.plot(np.sin(t256), color="#4fc3f7", alpha=0.4, linewidth=3, - label="sin α=0.4") -plot2.add_line(np.cos(t256), color="#ff7043", alpha=0.4, linewidth=3, - label="cos α=0.4") - -fig2 - -# %% -# Marker symbols -# -------------- -# Set ``marker`` to place a symbol at every data point. Use a **sparse** -# x-axis (few points) so the individual markers are legible. -# ``markersize`` is the radius (circles / diamonds) or half-side-length -# (squares, triangles) in canvas pixels. -# -# Supported symbols: -# -# * ``"o"`` — circle -# * ``"s"`` — square -# * ``"^"`` — triangle-up -# * ``"v"`` — triangle-down -# * ``"D"`` — diamond -# * ``"+"`` — plus (stroke-only) -# * ``"x"`` — cross (stroke-only) -# * ``"none"`` — no marker (default) - -SYMBOLS = [ - ("o", "#4fc3f7"), - ("s", "#ff7043"), - ("^", "#aed581"), - ("v", "#ce93d8"), - ("D", "#ffcc02"), - ("+", "#80cbc4"), - ("x", "#ef9a9a"), -] - -fig3, ax3 = vw.subplots(1, 1, figsize=(580, 380)) - -plot3 = ax3.plot( - np.sin(t24) + (0 - 3) * 0.9, - color=SYMBOLS[0][1], linewidth=1.5, - marker=SYMBOLS[0][0], markersize=5, - label=f'marker="{SYMBOLS[0][0]}"', -) -for i, (sym, col) in enumerate(SYMBOLS[1:], 1): - plot3.add_line( - np.sin(t24) + (i - 3) * 0.9, - color=col, linewidth=1.5, - marker=sym, markersize=5, - label=f'marker="{sym}"', - ) - -fig3 - -# %% -# Combined — linestyle + alpha + marker -# -------------------------------------- -# All three style parameters can be combined freely on the same line or on -# separate overlay lines. - -fig4, ax4 = vw.subplots(1, 1, figsize=(580, 300)) - -# Dense solid primary line -plot4 = ax4.plot(np.sin(t256), color="#4fc3f7", linewidth=2, - label="sin (solid)") - -# Sparse dashed overlay with circle markers and reduced opacity -plot4.add_line(np.cos(t24), color="#ff7043", linewidth=2, - linestyle="dashed", alpha=0.75, - marker="o", markersize=5, - label="cos (dashed, α=0.75, marker='o')") - -fig4 - -# %% -# Post-construction setters -# ------------------------- -# Every primary-line style property has a matching setter method. These -# mutate ``_state`` and push the change to the canvas immediately — no -# need to recreate the panel. - -fig5, ax5 = vw.subplots(1, 1, figsize=(440, 220)) -plot5 = ax5.plot(np.sin(t256), color="#4fc3f7", linewidth=1.5) - -# Change style via setters -plot5.set_color("#ff7043") -plot5.set_linewidth(2.5) -plot5.set_linestyle("dashdot") # equivalent: plot5.set_linestyle("-.") -plot5.set_alpha(0.8) -plot5.set_marker("o", markersize=5) - -fig5 - diff --git a/Examples/plot_pcolormesh.py b/Examples/plot_pcolormesh.py deleted file mode 100644 index 41147bd..0000000 --- a/Examples/plot_pcolormesh.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -pcolormesh — non-linear axes -============================ - -Demonstrate :meth:`~anyplotlib.figure_plots.Axes.pcolormesh` with non-uniform -(log-spaced) x-edges and irregularly-spaced y-edges, mirroring -``matplotlib.axes.Axes.pcolormesh``. - -The key difference from :meth:`~anyplotlib.figure_plots.Axes.imshow` is that -``pcolormesh`` takes **edge** arrays (length N+1 and M+1 for an (M, N) data -array) rather than center arrays. This enables fully non-linear axes where -each cell can have a different width/height in data coordinates. -""" -import numpy as np -import anyplotlib as vw - -rng = np.random.default_rng(42) - -# ── Data: 32 rows × 48 columns ─────────────────────────────────────────────── -M, N = 32, 48 -data = np.sin(np.linspace(0, 3 * np.pi, N)) + np.cos(np.linspace(0, 2 * np.pi, M))[:, None] -data += rng.normal(scale=0.15, size=(M, N)) - -# ── Non-uniform edges ───────────────────────────────────────────────────────── -# x: log-spaced between 0.1 and 100 (N+1 edges) -x_edges = np.logspace(-1, 2, N + 1) - -# y: irregular spacing — dense in the middle, coarse at the ends (M+1 edges) -y_centres = np.concatenate([ - np.linspace(0, 40, M // 4, endpoint=False), - np.linspace(40, 60, M // 2, endpoint=False), - np.linspace(60, 100, M // 4), -]) -y_edges = np.concatenate([[y_centres[0] - (y_centres[1] - y_centres[0]) / 2], - (y_centres[:-1] + y_centres[1:]) / 2, - [y_centres[-1] + (y_centres[-1] - y_centres[-2]) / 2]]) - -# ── Plot ────────────────────────────────────────────────────────────────────── -fig, ax = vw.subplots(1, 1, figsize=(560, 460)) -mesh = ax.pcolormesh(data, x_edges=x_edges, y_edges=y_edges, units="arb.") -mesh.set_colormap("viridis") -fig - -# %% -# Add point markers in physical coordinates -# ----------------------------------------- -# Marker coordinates are in the same physical (data) space as the edges. -# Only ``add_circles`` and ``add_lines`` are available on a pcolormesh panel. - -pts = np.array([[1.0, 20.0], [10.0, 50.0], [50.0, 80.0], [90.0, 45.0]]) -mesh.add_circles(pts, name="peaks", radius=3, - edgecolors="#ff1744", facecolors="#ff174433", - labels=["A", "B", "C", "D"]) -fig - -# %% -# Add line-segment markers -# ------------------------ -segs = [ - [[1.0, 20.0], [10.0, 50.0]], - [[10.0, 50.0], [50.0, 80.0]], -] -mesh.add_lines(segs, name="path", edgecolors="#00e5ff", linewidths=2.0) -fig - diff --git a/Examples/plot_spectra1d.py b/Examples/plot_spectra1d.py deleted file mode 100644 index 320ad22..0000000 --- a/Examples/plot_spectra1d.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -1D Spectra -========== - -Plot a 1-D spectrum with a physical x-axis (energy in eV) using -:meth:`~anyplotlib.figure_plots.Axes.plot`. - -The spectrum contains a broad background and three Gaussian peaks. -Circle markers highlight the peak positions using -:meth:`~anyplotlib.figure_plots.Plot1D.add_points`, and a range widget -selects a region of interest. A model fit is overlaid with a dashed line, -and the background component is shown as a semi-transparent dotted curve with -diamond markers. - -Pan and zoom with the mouse; press **R** to reset the view. -""" -import numpy as np -import anyplotlib as vw - -rng = np.random.default_rng(0) - -# ── Synthetic XPS-style spectrum ────────────────────────────────────────────── -energy = np.linspace(280, 295, 512) # binding energy axis (eV) - -def gaussian(x, mu, sigma, amp): - return amp * np.exp(-0.5 * ((x - mu) / sigma) ** 2) - -background = 0.4 * np.exp(-0.08 * (energy - 280)) - -# Background + three peaks (C 1s region) -spectrum = ( - background - + gaussian(energy, 284.8, 0.4, 1.0) # C–C / C–H - + gaussian(energy, 286.2, 0.4, 0.35) # C–O - + gaussian(energy, 288.0, 0.4, 0.18) # C=O - + rng.normal(scale=0.015, size=len(energy)) -) - -# ── Plot ────────────────────────────────────────────────────────────────────── -fig, ax = vw.subplots(1, 1, figsize=(620, 340)) -v = ax.plot(spectrum, axes=[energy], units="eV", y_units="Intensity (a.u.)", - color="#4fc3f7", linewidth=1.5) - -# ── Peak markers (add_points collection) ────────────────────────────────────── -peak_energies = np.array([284.8, 286.2, 288.0]) -peak_offsets = np.column_stack([ - peak_energies, - np.interp(peak_energies, energy, spectrum), -]) -v.add_points(peak_offsets, name="peaks", - sizes=7, color="#ff1744", facecolors="#ff174433", - labels=["C\u2013C", "C\u2013O", "C=O"]) - -# ── Region-of-interest widget ───────────────────────────────────────────────── -v.add_range_widget(x0=285.8, x1=288.8, color="#00e5ff") - -fig - -# %% -# Overlay a model fit — linestyle and alpha -# ----------------------------------------- -# Use :meth:`~anyplotlib.figure_plots.Plot1D.add_line` to overlay additional -# curves. Here the noiseless model fit is drawn as a **dashed** line so it -# is visually distinct from the noisy measured spectrum. The ``alpha`` -# parameter makes the fit semi-transparent so the data underneath remains -# readable. -# -# The y-axis range is expanded automatically to accommodate any overlay line -# whose values fall outside the current bounds. - -fit = ( - background - + gaussian(energy, 284.8, 0.4, 1.0) - + gaussian(energy, 286.2, 0.4, 0.35) - + gaussian(energy, 288.0, 0.4, 0.18) -) -v.add_line(fit, x_axis=energy, - color="#ffcc00", linewidth=2.0, - linestyle="dashed", alpha=0.85, - label="fit") - -fig - -# %% -# Background component — dotted line with markers -# ------------------------------------------------ -# Draw the exponential background component as a **dotted** curve. Passing -# ``marker="D"`` places a diamond at every data point (useful when the line -# is sparse or when you want to emphasise individual sample positions). -# ``markersize`` controls the half-size of the symbol in pixels. - -# Sub-sample to keep the marker plot readable -step = 32 -v.add_line(background[::step], x_axis=energy[::step], - color="#ce93d8", linewidth=1.2, - linestyle="dotted", alpha=0.9, - marker="D", markersize=3, - label="background") - -fig - -# %% -# Post-construction setters -# ------------------------- -# All primary-line style properties can be changed after the panel is created -# without rebuilding it. This is useful in interactive notebooks where you -# want to tweak the appearance of the main trace. - -v.set_alpha(0.9) # slightly reduce primary-line opacity -v.set_linewidth(2.0) # thicker stroke for the main spectrum - -fig diff --git a/anyplotlib/_repr_utils.py b/anyplotlib/_repr_utils.py index c65063b..f8e5762 100644 --- a/anyplotlib/_repr_utils.py +++ b/anyplotlib/_repr_utils.py @@ -129,23 +129,51 @@ def _widget_px(widget) -> tuple[int, int]: