|
| 1 | +""" |
| 2 | +Draggable Point Widget |
| 3 | +====================== |
| 4 | +
|
| 5 | +Demonstrates the :class:`~anyplotlib.widgets.PointWidget` on a 1-D panel. |
| 6 | +
|
| 7 | +A smooth curve ``f(x) = sin(x) · e^(−x/6)`` is shown together with a |
| 8 | +cyan control point that the user can drag freely inside the plot area. |
| 9 | +
|
| 10 | +**Interaction** |
| 11 | +
|
| 12 | +* **Drag the point** anywhere inside the plot — the widget reports its |
| 13 | + data-space ``(x, y)`` position on every frame via the |
| 14 | + :meth:`~anyplotlib.widgets.Widget.on_changed` callback. |
| 15 | +* **Release** — the :meth:`~anyplotlib.widgets.Widget.on_release` callback |
| 16 | + snaps the point's y-coordinate to the curve value at the dragged x |
| 17 | + and draws the **tangent line** through that point. |
| 18 | +
|
| 19 | +**What is computed on release** |
| 20 | +
|
| 21 | +Given the dragged x position *xq*, the code evaluates: |
| 22 | +
|
| 23 | +* **Curve value**: ``yq = f(xq)`` |
| 24 | +* **Derivative** (central finite difference): ``dy/dx ≈ [f(xq+h) − f(xq−h)] / 2h`` |
| 25 | +* **Tangent line**: ``y_tan(x) = yq + slope · (x − xq)`` |
| 26 | +
|
| 27 | +The tangent line is added with :meth:`~anyplotlib.figure_plots.Plot1D.add_line` |
| 28 | +and the previous one is removed, so only one tangent is shown at a time. |
| 29 | +
|
| 30 | +.. note:: |
| 31 | + Move the point to an interesting part of the curve (e.g. a local maximum) |
| 32 | + and release — the tangent will be horizontal there. |
| 33 | +""" |
| 34 | + |
| 35 | +import numpy as np |
| 36 | +import anyplotlib as vw |
| 37 | + |
| 38 | +# ── Curve ────────────────────────────────────────────────────────────────── |
| 39 | +x = np.linspace(0.0, 4.0 * np.pi, 512) |
| 40 | + |
| 41 | +def f(t): |
| 42 | + return np.sin(t) * np.exp(-t / 6.0) |
| 43 | + |
| 44 | +def df(t, h=1e-5): |
| 45 | + """Central finite-difference derivative of f.""" |
| 46 | + return (f(t + h) - f(t - h)) / (2.0 * h) |
| 47 | + |
| 48 | +y = f(x) |
| 49 | + |
| 50 | +# ── Figure ───────────────────────────────────────────────────────────────── |
| 51 | +fig, ax = vw.subplots(figsize=(680, 340)) |
| 52 | +plot = ax.plot(y, axes=[x], units="rad", |
| 53 | + color="#4fc3f7", linewidth=2.0, label="f(x)") |
| 54 | + |
| 55 | +# ── Initial point widget — placed at the first local maximum ─────────────── |
| 56 | +x0_init = float(x[np.argmax(y)]) |
| 57 | +y0_init = float(np.max(y)) |
| 58 | +pt = plot.add_point_widget(x0_init, y0_init, color="#00e5ff") |
| 59 | + |
| 60 | +# Track the current tangent line handle so we can replace it |
| 61 | +_tangent_line: "vw.Line1D | None" = None # type: ignore[name-defined] |
| 62 | + |
| 63 | +def _draw_tangent(xq: float) -> None: |
| 64 | + """Snap point to curve, compute slope, draw tangent overlay.""" |
| 65 | + global _tangent_line |
| 66 | + |
| 67 | + # Evaluate curve and slope at xq |
| 68 | + yq = float(f(xq)) |
| 69 | + slope = float(df(xq)) |
| 70 | + |
| 71 | + # Snap the widget y to the curve (visual feedback) |
| 72 | + pt._data["y"] = yq |
| 73 | + pt._push_fn() |
| 74 | + |
| 75 | + # Tangent line spans the full visible x range |
| 76 | + x_tan = np.array([float(x[0]), float(x[-1])]) |
| 77 | + y_tan = yq + slope * (x_tan - xq) |
| 78 | + |
| 79 | + # Replace previous tangent |
| 80 | + if _tangent_line is not None: |
| 81 | + _tangent_line.remove() |
| 82 | + _tangent_line = plot.add_line( |
| 83 | + y_tan, x_axis=x_tan, |
| 84 | + color="#ff7043", linewidth=1.5, |
| 85 | + linestyle="dashed", |
| 86 | + label=f"slope = {slope:+.3f}", |
| 87 | + ) |
| 88 | + |
| 89 | +# Draw the tangent at the initial position |
| 90 | +_draw_tangent(x0_init) |
| 91 | + |
| 92 | + |
| 93 | +# ── Callbacks ────────────────────────────────────────────────────────────── |
| 94 | + |
| 95 | +@pt.on_changed |
| 96 | +def _live(event): |
| 97 | + """Every drag frame — print the current widget position.""" |
| 98 | + print(f" dragging x={event.x:.4f} y={event.y:.4f}", end="\r") |
| 99 | + |
| 100 | + |
| 101 | +@pt.on_release |
| 102 | +def _settled(event): |
| 103 | + """On mouse-up — snap y to the curve and refresh the tangent line.""" |
| 104 | + print(f" released x={event.x:.4f} ") |
| 105 | + _draw_tangent(event.x) |
| 106 | + |
| 107 | + |
| 108 | +fig |
| 109 | + |
0 commit comments