diff --git a/Examples/PlotTypes/README.rst b/Examples/PlotTypes/README.rst new file mode 100644 index 0000000..8c02b0e --- /dev/null +++ b/Examples/PlotTypes/README.rst @@ -0,0 +1,3 @@ +Plot Types +---------- +A collection of short examples showing different plot types. \ No newline at end of file diff --git a/Examples/PlotTypes/plot_3d.py b/Examples/PlotTypes/plot_3d.py new file mode 100644 index 0000000..2565fc0 --- /dev/null +++ b/Examples/PlotTypes/plot_3d.py @@ -0,0 +1,74 @@ +""" +3D Plotting +=========== + +Demonstrate the three 3-D geometry types supported by +:meth:`~anyplotlib.Axes.plot_surface`, +:meth:`~anyplotlib.Axes.scatter3d`, and +:meth:`~anyplotlib.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.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/PlotTypes/plot_bar.py b/Examples/PlotTypes/plot_bar.py new file mode 100644 index 0000000..e0385ff --- /dev/null +++ b/Examples/PlotTypes/plot_bar.py @@ -0,0 +1,151 @@ +""" +Bar Chart +========= + +Demonstrate :meth:`~anyplotlib.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.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.PlotBar` panels in one figure. +# Call :meth:`~anyplotlib.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.PlotBar.set_color` repaints all bars, +# :meth:`~anyplotlib.PlotBar.set_show_values` toggles labels, +# :meth:`~anyplotlib.PlotBar.set_log_scale` switches the +# value-axis between linear and logarithmic. + +bar1.set_color("#ff7043") +bar1.set_show_values(False) +fig1 diff --git a/Examples/PlotTypes/plot_image2d.py b/Examples/PlotTypes/plot_image2d.py new file mode 100644 index 0000000..bdf9703 --- /dev/null +++ b/Examples/PlotTypes/plot_image2d.py @@ -0,0 +1,130 @@ +""" +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.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.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 in the same physical coordinates used by imshow +spot_nm = np.array([[ 2.1, 0.0], [-2.1, 0.0], + [ 0.0, 2.1], [ 0.0, -2.1]]) +v.add_circles(spot_nm, 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 # Interactive + +# %% +# Adjust colour map and display range +# ------------------------------------ +# :meth:`~anyplotlib.Plot2D.set_colormap` switches the palette; +# :meth:`~anyplotlib.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 # Interactive + diff --git a/Examples/PlotTypes/plot_inset.py b/Examples/PlotTypes/plot_inset.py new file mode 100644 index 0000000..0f88a4f --- /dev/null +++ b/Examples/PlotTypes/plot_inset.py @@ -0,0 +1,90 @@ +""" +Inset Plots +=========== + +Floating informational sub-plots that overlay the main figure — useful for +displaying supplementary data alongside a primary image, as seen in orientation +mapping, phase analysis, and similar workflows. + +Each inset has a **title bar** with two buttons: + +* **−** (minimize) — collapses the inset to its title bar only. +* **⤢** (maximize) — expands the inset to ~72 % of the figure, centred. + Click **⤡** to restore. + +Multiple insets sharing the same ``corner`` auto-stack so they never overlap +in the minimised or normal state. + +Python-side state can also be set programmatically:: + + inset.minimize() + inset.maximize() + inset.restore() + print(inset.inset_state) # "normal" | "minimized" | "maximized" +""" + +import numpy as np +import anyplotlib as apl + +rng = np.random.default_rng(42) + +# ── Helpers — synthetic data ────────────────────────────────────────────────── + +def _diffraction(N=256): + """Simulated diffraction pattern (Gaussian rings).""" + y, x = np.ogrid[-N//2:N//2, -N//2:N//2] + r = np.hypot(x, y) + img = np.zeros((N, N)) + for r0, sigma, amp in [(40, 6, 1.0), (80, 8, 0.6), (120, 10, 0.3)]: + img += amp * np.exp(-((r - r0) ** 2) / (2 * sigma ** 2)) + img += rng.normal(0, 0.04, img.shape) + return img + +def _phase_map(N=128): + """Fake two-phase orientation map.""" + img = rng.integers(0, 4, (N, N), dtype=np.uint8) + # blob of phase 2 in the centre + cy, cx = N // 2, N // 2 + yy, xx = np.ogrid[:N, :N] + img[((yy - cy)**2 + (xx - cx)**2) < (N // 4)**2] = np.uint8(5) + return img.astype(float) + +def _pole_figure(N=96): + """Simulated pole-figure intensity (radial Gaussian blob).""" + y, x = np.ogrid[-N//2:N//2, -N//2:N//2] + r = np.hypot(x, y) + return np.exp(-(r ** 2) / (2 * (N // 6) ** 2)) + rng.normal(0, 0.02, (N, N)) + +def _virtual_adf(N=128): + """Annular dark-field signal for a simple lattice.""" + y, x = np.mgrid[:N, :N] + return (np.sin(y * 0.4) * np.cos(x * 0.4)) ** 2 + rng.normal(0, 0.05, (N, N)) + +# ── Build figure ────────────────────────────────────────────────────────────── + +fig, ax = apl.subplots(1, 1, figsize=(660, 500)) + +# Primary large image: diffraction pattern +main = ax.imshow(_diffraction(256), cmap="inferno") + +# ── Inset 1: phase map (top-right) ─────────────────────────────────────────── +inset_phase = fig.add_inset(0.27, 0.27, corner="top-right", title="Phase Map") +inset_phase.imshow(_phase_map(128), cmap="tab10") + +# ── Inset 2: pole figure — stacks below inset 1 in the same corner ──────────── +inset_pole = fig.add_inset(0.27, 0.27, corner="top-right", title="Pole Figure") +inset_pole.imshow(_pole_figure(96), cmap="hot") + +# ── Inset 3: virtual ADF (bottom-left) ──────────────────────────────────────── +inset_adf = fig.add_inset(0.27, 0.27, corner="bottom-left", title="Virtual ADF") +inset_adf.imshow(_virtual_adf(128), cmap="gray") + +# ── Inset 4: 1-D line profile (bottom-right) ───────────────────────────────── +x_nm = np.linspace(0, 10, 256) +profile = np.sin(x_nm * 3.5) * np.exp(-x_nm * 0.18) + rng.normal(0, 0.05, 256) + +inset_line = fig.add_inset(0.30, 0.22, corner="bottom-right", title="Line Profile") +inset_line.plot(profile, axes=[x_nm], units="nm", color="#4fc3f7", linewidth=1.5) + +fig + diff --git a/Examples/PlotTypes/plot_line_styles.py b/Examples/PlotTypes/plot_line_styles.py new file mode 100644 index 0000000..42faeea --- /dev/null +++ b/Examples/PlotTypes/plot_line_styles.py @@ -0,0 +1,159 @@ +""" +1D Line Styles +============== + +Demonstrates the line-style, opacity, and per-point marker parameters +available on :meth:`~anyplotlib.Axes.plot` and +:meth:`~anyplotlib.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.Axes.plot` +# and :meth:`~anyplotlib.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/PlotTypes/plot_pcolormesh.py b/Examples/PlotTypes/plot_pcolormesh.py new file mode 100644 index 0000000..a85fb75 --- /dev/null +++ b/Examples/PlotTypes/plot_pcolormesh.py @@ -0,0 +1,65 @@ +""" +pcolormesh — non-linear axes +============================ + +Demonstrate :meth:`~anyplotlib.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.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/PlotTypes/plot_spectra1d.py b/Examples/PlotTypes/plot_spectra1d.py new file mode 100644 index 0000000..d0630fb --- /dev/null +++ b/Examples/PlotTypes/plot_spectra1d.py @@ -0,0 +1,112 @@ +""" +1D Spectra +========== + +Plot a 1-D spectrum with a physical x-axis (energy in eV) using +:meth:`~anyplotlib.Axes.plot`. + +The spectrum contains a broad background and three Gaussian peaks. +Circle markers highlight the peak positions using +:meth:`~anyplotlib.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.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/docs/api/figure_plots.rst b/docs/api/figure_plots.rst index 1a9a14d..6e48db6 100644 --- a/docs/api/figure_plots.rst +++ b/docs/api/figure_plots.rst @@ -1,6 +1,6 @@ -============ -Figure Plots -============ +==================== +Axes, Plots & Layout +==================== .. currentmodule:: anyplotlib diff --git a/docs/api/index.rst b/docs/api/index.rst index ecf2a45..8cdb2ad 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -2,7 +2,7 @@ API Reference ============= -The anyplotlib public API is organized into five modules below. +The anyplotlib public API is organized into the sections below. Click a card to browse the module page, or use the summary tables to jump directly to a class or function. @@ -34,23 +34,14 @@ directly to a class or function. :link: figure_plots :link-type: doc - :octicon:`graph;2em;sd-text-info` Axes & Plots + :octicon:`graph;2em;sd-text-info` Axes, Plots & Layout ^^^ - :class:`~anyplotlib.Axes` and the five plot classes: - :class:`~anyplotlib.Plot1D`, :class:`~anyplotlib.Plot2D`, + :class:`~anyplotlib.Axes` and the five plot classes + (:class:`~anyplotlib.Plot1D`, :class:`~anyplotlib.Plot2D`, :class:`~anyplotlib.PlotMesh`, :class:`~anyplotlib.Plot3D`, - :class:`~anyplotlib.PlotBar`. - - .. grid-item-card:: - :link: figure_plots - :link-type: doc - - :octicon:`rows;2em;sd-text-info` Layout - ^^^ - - :class:`~anyplotlib.GridSpec` and :class:`~anyplotlib.SubplotSpec` - for building flexible multi-panel figure layouts. + :class:`~anyplotlib.PlotBar`), plus :class:`~anyplotlib.GridSpec` + and :class:`~anyplotlib.SubplotSpec` for multi-panel layouts. .. grid-item-card:: :link: markers diff --git a/docs/dev/index.rst b/docs/dev/index.rst index ebfb987..4087d17 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -26,11 +26,7 @@ management. uv sync # Run the full test suite - uv run pytest tests/ - - # Quick smoke tests (no pytest overhead) - uv run python test_figure.py - uv run python test_pcolormesh.py + uv run pytest anyplotlib/tests/ The ``dev`` dependency group (declared in ``pyproject.toml``) pulls in ``pytest``, ``playwright``, ``sphinx``, ``docutils``, and other tools @@ -41,30 +37,36 @@ needed for both tests and docs builds. Architecture Overview ===================== -The library is split into a small number of focused modules. +The library is split into focused subpackages. .. list-table:: :header-rows: 1 :widths: 25 75 - * - File + * - Module - Purpose - * - ``figure.py`` + * - ``anyplotlib/figure/`` - ``Figure`` — the only ``anywidget.AnyWidget`` subclass. Owns all traitlets and is the Python ↔ JS bridge. - * - ``figure_plots.py`` - - ``Plot2D``, ``Plot1D``, ``PlotMesh``, ``Plot3D``, ``Axes``, - ``GridSpec``, ``subplots()``. Plain Python classes — *no* traitlets. + Also contains ``GridSpec``, ``SubplotSpec``, and ``subplots()``. + * - ``anyplotlib/axes/`` + - ``Axes`` and ``InsetAxes``. Plain Python classes — *no* traitlets. + * - ``anyplotlib/plot1d/`` + - ``Plot1D`` and ``PlotBar``. Plain Python classes — *no* traitlets. + * - ``anyplotlib/plot2d/`` + - ``Plot2D`` and ``PlotMesh``. Plain Python classes — *no* traitlets. + * - ``anyplotlib/plot3d/`` + - ``Plot3D``. Plain Python class — *no* traitlets. * - ``figure_esm.js`` - Pure-JS canvas renderer (≈ 4 000 lines). - * - ``markers.py`` + * - ``anyplotlib/markers.py`` - Static visual overlays (circles, arrows, lines, etc.). - * - ``widgets.py`` + * - ``anyplotlib/widgets/`` - Interactive draggable overlays (``RectangleWidget``, ``CrosshairWidget``, etc.). - * - ``callbacks.py`` - - Multi-tier event system (``on_change`` / ``on_release``). - * - ``sphinx_anywidget/`` + * - ``anyplotlib/callbacks.py`` + - Multi-tier event system (``on_changed`` / ``on_release``). + * - ``anyplotlib/sphinx_anywidget/`` - Sphinx extension for interactive docs via Pyodide. **Python → JS flow:** ``plot._push()`` → ``figure._push(panel_id)`` → @@ -79,15 +81,15 @@ Python observer calls ``Widget._update_from_js()`` and fires callbacks. Running & Writing Tests ======================= -Tests live in ``tests/`` +Tests live in ``anyplotlib/tests/`` Run the full suite:: - uv run pytest tests/ + uv run pytest anyplotlib/tests/ Run a specific module:: - uv run pytest tests/test_sphinx_anywidget.py -v + uv run pytest anyplotlib/tests/test_documentation/test_sphinx_anywidget.py -v The Playwright end-to-end tests (``test_pyodide_e2e.py``) require the Playwright browsers. Install them once with:: @@ -231,7 +233,7 @@ separate code block with its own prose cell:: # %% # Adjusting the colour map # ------------------------- - # :meth:`~anyplotlib.figure_plots.Plot2D.set_colormap` switches the palette. + # :meth:`~anyplotlib.Plot2D.set_colormap` switches the palette. v.set_colormap("viridis") fig diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 36cbb85..8b3e1c0 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -134,7 +134,7 @@ All line properties can be changed after creation without recreating the panel:: ``v.add_vlines(x_values)`` / ``v.add_hlines(y_values)`` / … — static marker collections at explicit data coordinates. -See :class:`~anyplotlib.figure_plots.Plot1D` for the full API reference, and +See :class:`~anyplotlib.Plot1D` for the full API reference, and the :doc:`auto_examples/index` gallery (e.g. *1D Line Styles* or *1D Spectra*) for worked examples. diff --git a/pyproject.toml b/pyproject.toml index 8142e4c..36f3482 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,21 @@ packages = ["anyplotlib"] name = "anyplotlib" version = "0.1.0" description = "A plotting library using python, javascript and anywidget for performant in browser plotting." +readme = "README.md" +license = { text = "MIT" } +authors = [ + { name = "Carter Francis", email = "cartsfrancis@gmail.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Visualization", +] requires-python = ">=3.10" dependencies = [ "anywidget>=0.9.0", @@ -34,6 +49,11 @@ jupyter = [ "jupyterlab>=4.5.5", ] +[project.urls] +"Homepage" = "https://cssfrancis.github.io/anyplotlib/" +"Repository" = "https://github.com/CSSFrancis/anyplotlib" +"Bug Tracker" = "https://github.com/CSSFrancis/anyplotlib/issues" + [dependency-groups] dev = [ "docutils>=0.19", diff --git a/upcoming_changes/11.maintenance.rst b/upcoming_changes/11.maintenance.rst index 642100c..b7606d5 100644 --- a/upcoming_changes/11.maintenance.rst +++ b/upcoming_changes/11.maintenance.rst @@ -1,2 +1,2 @@ -Refactored the testssuite. Moved to a new directory, combined liked -t1ests into single files, added a couple new tests and removed some redundant tests. \ No newline at end of file +Refactored the test suite. Moved to a new directory, combined like +tests into single files, added a couple new tests and removed some redundant tests.