diff --git a/docs/gallery.md b/docs/gallery.md index f77adf7..45a8b01 100644 --- a/docs/gallery.md +++ b/docs/gallery.md @@ -80,6 +80,15 @@ ratios, and a figure suptitle — [![Mosaic layout with spanning panes and a suptitle](images/examples/36_mosaic_layout.png)](images/examples/36_mosaic_layout.png) +Named panes — the mosaic's list form names every subplot (`"price"`, +`"volume"`, `"depth"`), `link_x="col"` keeps a column time-aligned, and the +same names address the live render: `view.pane("price").set_range(x=…)` zooms +programmatically (the linked pane follows), `pane.export(...)` writes one +pane, `view.on(..., pane="price")` scopes events — +[`37_named_panes.py`](https://github.com/jawjay/qtviz/blob/main/examples/37_named_panes.py) + +[![Named panes: labeled mosaic, linked column, programmatic pane zoom](images/examples/37_named_panes.png)](images/examples/37_named_panes.png) + ## Getting started
diff --git a/docs/images/examples/37_named_panes.png b/docs/images/examples/37_named_panes.png new file mode 100644 index 0000000..7c19513 Binary files /dev/null and b/docs/images/examples/37_named_panes.png differ diff --git a/examples/37_named_panes.py b/examples/37_named_panes.py new file mode 100644 index 0000000..ad9e0d0 --- /dev/null +++ b/examples/37_named_panes.py @@ -0,0 +1,72 @@ +"""Named panes — address every subplot by name, before and after render +([D145]–[D151], design/pane-handles.md). + +The mosaic's **list form** names panes with real words (`"price"`, +`"volume"`, `"depth"`); repeated labels span cells like `subplot_mosaic`. +`link_x="col"` keeps each grid column time-aligned (a spanning pane joins +every column it covers). Downstream, the same names address the live render — +the "Axes of qtviz": + + view.pane("price").set_range(x=(18, 30)) # programmatic zoom; the + # linked "volume" pane follows + view.pane("depth").autorange() + view.pane("price").export("price.png") # just that pane + view.on(qv.RangeEvent, cb, pane="price") # pane-scoped events + view.set_root(view.root.with_pane("depth", other)) # declarative swap + +Run: + uv run python examples/37_named_panes.py +""" + +from __future__ import annotations + +import numpy as np + +import qtviz as qv + +rng = np.random.default_rng(7) +t = np.linspace(0.0, 30.0, 600) +price = 100.0 * np.exp(np.cumsum(rng.normal(0.0002, 0.01, t.size))) +volume = np.abs(rng.normal(1.0, 0.4, t.size)) * (1 + 0.5 * np.sin(t / 3)) +returns = {"r": np.diff(np.log(price)) * 100} + +panes = { + "price": (qv.Curve({"t": t, "p": price}, x="t", y="p", label="price") + * qv.HLine(float(price.mean()), line_style="dashed", label="mean") + ).opts(title="Price", x="t [s]"), + "volume": qv.Area({"t": t, "v": volume}, x="t", y="v", alpha=0.6) + .opts(title="Volume", x="t [s]"), + "depth": qv.Histogram(returns, value="r", bins="fd") + .opts(title="Returns (%)"), +} + +root = qv.Layout.mosaic( + [["price", "depth"], + ["volume", "depth"]], # multi-char labels; "depth" spans both rows + panes, + options=qv.LayoutOptions( + link_x="col", # the price/volume column pans together + width_ratios=[1.6, 1.0], + title="Named panes: mosaic labels drive layout, linking, and view.pane()", + ), +) + + +def build() -> qv.View: + view = qv.View(root, backend="pyqtgraph") + # Downstream use of a named pane: zoom "price" programmatically — the + # column link carries "volume" along; "depth" (its own column) stays put. + view.pane("price").set_range(x=(18.0, 30.0)) + return view + + +def main() -> None: + view = build() + view.on(qv.RangeEvent, + lambda e: print(f"[{e.pane}] x={e.x[0]:.2f}..{e.x[1]:.2f}"), + pane="price", throttle_ms=200) + qv.show(view, title="qtviz — named panes", size=(1100, 620)) + + +if __name__ == "__main__": + main() diff --git a/tests/qtviz/test_example_mains.py b/tests/qtviz/test_example_mains.py index 4281aef..d5af8ed 100644 --- a/tests/qtviz/test_example_mains.py +++ b/tests/qtviz/test_example_mains.py @@ -37,6 +37,7 @@ "examples/02_composition.py", "examples/31_axis_labels.py", "examples/35_everyday_figures.py", + "examples/37_named_panes.py", ]) def test_example_main_runs_without_a_preexisting_app(example): result = subprocess.run( diff --git a/tools/capture_screenshots.py b/tools/capture_screenshots.py index 10469fe..6bc2efd 100644 --- a/tools/capture_screenshots.py +++ b/tools/capture_screenshots.py @@ -66,6 +66,7 @@ "34_streaming_telemetry": {"settle": 1500, "size": (1200, 640), "splitter": (820, 380)}, "35_everyday_figures": {"size": (1500, 1040)}, "36_mosaic_layout": {"size": (1100, 620)}, + "37_named_panes": {"size": (1100, 620)}, "dashboard_native": {"size": (1100, 700)}, }