|
| 1 | +""" |
| 2 | +Interactive figure annotations — double-click to add, drag to place |
| 3 | +=================================================================== |
| 4 | +
|
| 5 | +anyplotlib has a *figure-level* annotation layer that floats above the panels, |
| 6 | +positioned in **figure fractions** (0…1, origin top-left) rather than any |
| 7 | +panel's data coordinates. Set it with |
| 8 | +:meth:`~anyplotlib.Figure.set_figure_markers`; the supported kinds are |
| 9 | +``"text"``, ``"circle"``, ``"rect"`` and ``"arrow"``. |
| 10 | +
|
| 11 | +Turn on ``fig.edit_chrome`` and every annotation becomes draggable in the |
| 12 | +browser. Combine that with a ``double_click`` handler and you get a simple |
| 13 | +annotate-by-clicking workflow: double-click a feature to drop a labelled |
| 14 | +arrow, then drag it to line it up. Positions round-trip back to Python via |
| 15 | +:attr:`~anyplotlib.Figure.figure_markers`. |
| 16 | +""" |
| 17 | +import numpy as np |
| 18 | +import anyplotlib as apl |
| 19 | + |
| 20 | +rng = np.random.default_rng(7) |
| 21 | +data = rng.standard_normal((160, 160)).cumsum(0).cumsum(1) |
| 22 | +data = (data - data.min()) / (data.max() - data.min()) |
| 23 | + |
| 24 | +fig, ax = apl.subplots(1, 1, figsize=(520, 520)) |
| 25 | +v = ax.imshow(data, cmap="magma", units="px") |
| 26 | + |
| 27 | +# Enable the editable-annotation ("report builder") mode so figure markers are |
| 28 | +# hit-testable and draggable, and the figure emits background/marker events. |
| 29 | +fig.edit_chrome = True |
| 30 | + |
| 31 | +# %% |
| 32 | +# Seed a couple of annotations |
| 33 | +# ---------------------------- |
| 34 | +# Each marker is a dict with a ``kind`` and fraction-space geometry. A text |
| 35 | +# label and an arrow pointing into the image to start with. |
| 36 | + |
| 37 | +fig.set_figure_markers([ |
| 38 | + {"kind": "text", "x": 0.5, "y": 0.06, |
| 39 | + "text": "Double-click a feature to annotate it", |
| 40 | + "color": "#ffffff", "fontsize": 14}, |
| 41 | + {"kind": "arrow", "x": 0.20, "y": 0.30, "u": 0.12, "v": 0.12, |
| 42 | + "color": "#ffd54f", "linewidth": 2}, |
| 43 | +]) |
| 44 | + |
| 45 | +fig |
| 46 | + |
| 47 | +# %% |
| 48 | +# Double-click to drop a new annotation |
| 49 | +# ------------------------------------- |
| 50 | +# On a single-panel figure the panel nearly fills the canvas, so we turn the |
| 51 | +# click's device pixels into a figure fraction with the panel's |
| 52 | +# ``display_width`` / ``display_height`` and append an arrow + label there. |
| 53 | +# Because ``edit_chrome`` is on, the new marker is immediately draggable. |
| 54 | + |
| 55 | + |
| 56 | +def _on_double_click(event): |
| 57 | + if event.x is None or event.display_width is None: |
| 58 | + return |
| 59 | + fx = float(np.clip(event.x / event.display_width, 0.02, 0.98)) |
| 60 | + fy = float(np.clip(event.y / event.display_height, 0.02, 0.98)) |
| 61 | + markers = fig.figure_markers # current list (a copy) |
| 62 | + n = sum(1 for m in markers if m["kind"] == "text") |
| 63 | + markers.append({"kind": "arrow", "x": fx, "y": fy, |
| 64 | + "u": 0.08, "v": -0.08, "color": "#40c4ff", "linewidth": 2}) |
| 65 | + markers.append({"kind": "text", "x": fx + 0.08, "y": fy - 0.10, |
| 66 | + "text": f"mark {n}", "color": "#40c4ff", "fontsize": 13}) |
| 67 | + fig.set_figure_markers(markers) |
| 68 | + |
| 69 | + |
| 70 | +v.add_event_handler(_on_double_click, "double_click") |
| 71 | + |
| 72 | +fig.set_help( |
| 73 | + "Double-click on the image to drop a labelled arrow.\n" |
| 74 | + "Drag any annotation to reposition it (edit mode is on)." |
| 75 | +) |
| 76 | + |
| 77 | +fig # Interactive |
| 78 | + |
| 79 | +# %% |
| 80 | +# Read the placements back |
| 81 | +# ------------------------ |
| 82 | +# After the user drags things around, ``fig.figure_markers`` reflects the |
| 83 | +# current fraction positions — persist them, export them, or feed them into a |
| 84 | +# report. |
| 85 | + |
| 86 | +for m in fig.figure_markers: |
| 87 | + print(m["kind"], round(m["x"], 3), round(m["y"], 3)) |
0 commit comments