Skip to content

Commit 0fd3d80

Browse files
committed
Clean up syntax and remove unused code. Add regression tests for Plot2D methods and ensure top-level imports are accessible.
1 parent 1b3bd8b commit 0fd3d80

8 files changed

Lines changed: 385 additions & 48 deletions

File tree

Examples/Interactive/plot_key_bindings.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@
44
55
Demonstrates the ``on_key`` callback API: press a key while the plot is
66
focused to add an overlay widget centred on the current cursor position,
7-
or press **Delete** to remove the last widget you clicked.
7+
or press **Backspace / Delete** to remove the last widget you clicked.
88
99
**Key bindings**
1010
11-
+-------+---------------------------+
12-
| Key | Action |
13-
+=======+===========================+
14-
| ``q`` | Add a rectangle |
15-
+-------+---------------------------+
16-
| ``w`` | Add a circle |
17-
+-------+---------------------------+
18-
| ``e`` | Add an annulus |
19-
+-------+---------------------------+
20-
| ``Delete`` | Remove last-clicked |
21-
+-------+---------------------------+
11+
+-------------------------------+---------------------------+
12+
| Key | Action |
13+
+===============================+===========================+
14+
| ``q`` | Add a rectangle |
15+
+-------------------------------+---------------------------+
16+
| ``w`` | Add a circle |
17+
+-------------------------------+---------------------------+
18+
| ``e`` | Add an annulus |
19+
+-------------------------------+---------------------------+
20+
| ``Backspace`` (macOS ⌫) | Remove last-clicked |
21+
| ``Delete`` (Windows / Linux) | |
22+
+-------------------------------+---------------------------+
2223
2324
**Built-in 2-D shortcuts** (not overridden in this example):
2425
@@ -40,7 +41,8 @@
4041
4142
.. note::
4243
Move the mouse over the image first so the plot panel receives focus,
43-
then press a key.
44+
then press a key. On macOS the backspace key (⌫) is used for deletion;
45+
on Windows / Linux use the **Delete** key.
4446
"""
4547

4648
import numpy as np
@@ -95,19 +97,25 @@ def add_annulus(event):
9597
)
9698

9799

100+
# macOS sends 'Backspace' for the ⌫ key; Windows/Linux send 'Delete'.
101+
# Register both so the example works cross-platform.
102+
@plot.on_key('Backspace')
98103
@plot.on_key('Delete')
99104
def delete_last(event):
100-
"""Press Delete — remove the last widget that was clicked / dragged."""
105+
"""Press Backspace/Delete — remove the last widget that was clicked."""
101106
wid = event.last_widget_id
102107
if wid and wid in {w.id for w in plot.list_widgets()}:
103108
plot.remove_widget(wid)
104109

105110

106-
# ── Catch-all handler (optional) — print every registered key press ──────────
111+
# ── Catch-all handler (optional) — log every registered key press ─────────────
107112

108113
@plot.on_key
109114
def log_key(event):
110-
print(f"[on_key] key={event.key!r} img=({event.img_x:.1f}, {event.img_y:.1f})"
115+
img_x = getattr(event, 'img_x', None)
116+
img_y = getattr(event, 'img_y', None)
117+
pos = f"({img_x:.1f}, {img_y:.1f})" if img_x is not None else "n/a"
118+
print(f"[on_key] key={event.key!r} img={pos}"
111119
f" last_widget={event.last_widget_id!r}")
112120

113121
fig

anyplotlib/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from anyplotlib.figure import Figure, GridSpec, SubplotSpec, subplots
2-
from anyplotlib.figure_plots import PlotMesh, Plot3D, PlotBar
2+
from anyplotlib.figure_plots import Axes, Plot1D, Plot2D, PlotMesh, Plot3D, PlotBar
3+
from anyplotlib.callbacks import CallbackRegistry, Event
34
from anyplotlib.widgets import (
45
Widget, RectangleWidget, CircleWidget, AnnularWidget,
56
CrosshairWidget, PolygonWidget, LabelWidget,
@@ -8,7 +9,8 @@
89

910
__all__ = [
1011
"Figure", "GridSpec", "SubplotSpec", "subplots",
11-
"PlotMesh", "Plot3D", "PlotBar",
12+
"Axes", "Plot1D", "Plot2D", "PlotMesh", "Plot3D", "PlotBar",
13+
"CallbackRegistry", "Event",
1214
"Widget", "RectangleWidget", "CircleWidget", "AnnularWidget",
1315
"CrosshairWidget", "PolygonWidget", "LabelWidget",
1416
"VLineWidget", "HLineWidget", "RangeWidget",

anyplotlib/_repr_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
Strategy
99
--------
10-
and 1. Serialise every synced traitlet value to a plain JSON dict.
10+
1. Serialise every synced traitlet value to a plain JSON dict.
1111
2. Embed that dict and the widget's ``_esm`` source directly in the page.
1212
3. Provide a minimal model shim (get/set/on/save_changes) so the ESM's
1313
render() function works without any Jupyter comm infrastructure.

anyplotlib/figure.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ def _on_resize(self, change) -> None:
163163
@traitlets.observe("event_json")
164164
def _on_event(self, change) -> None:
165165
"""Dispatch a JS interaction event to the relevant plot and widget callbacks."""
166-
print("_on_event:", change["new"])
167166
raw = change["new"]
168167
if not raw or raw == "{}":
169168
return

anyplotlib/figure_esm.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function render({ model, el }) {
8484

8585
// ── outer DOM ────────────────────────────────────────────────────────────
8686
const outerDiv = document.createElement('div');
87-
outerDiv.style.cssText = 'position:relative;display:inline-block;user-select:none;';
87+
outerDiv.style.cssText = 'position:relative;display:inline-block;user-select:none;z-index:1;isolation:isolate;';
8888
el.appendChild(outerDiv);
8989

9090
const gridDiv = document.createElement('div');
@@ -95,7 +95,7 @@ function render({ model, el }) {
9595
const resizeHandle = document.createElement('div');
9696
resizeHandle.style.cssText =
9797
'position:absolute;bottom:2px;right:2px;width:16px;height:16px;cursor:nwse-resize;' +
98-
'background:linear-gradient(135deg,transparent 50%,#888 50%);border-radius:0 0 4px 0;z-index:20;';
98+
'background:linear-gradient(135deg,transparent 50%,#888 50%);border-radius:0 0 4px 0;z-index:100;';
9999
resizeHandle.title = 'Drag to resize figure';
100100
outerDiv.appendChild(resizeHandle);
101101

@@ -170,7 +170,7 @@ function render({ model, el }) {
170170

171171
function _createPanelDOM(id, kind, pw, ph, spec) {
172172
const cell = document.createElement('div');
173-
cell.style.cssText = 'position:relative;overflow:visible;line-height:0;';
173+
cell.style.cssText = 'position:relative;overflow:visible;line-height:0;display:flex;justify-content:center;align-items:flex-start;';
174174
cell.style.gridRow = `${spec.row_start+1} / ${spec.row_stop+1}`;
175175
cell.style.gridColumn = `${spec.col_start+1} / ${spec.col_stop+1}`;
176176
gridDiv.appendChild(cell);
@@ -1240,14 +1240,14 @@ function render({ model, el }) {
12401240
last_widget_id: p.lastWidgetId || null,
12411241
mouse_x: p.mouseX, mouse_y: p.mouseY,
12421242
});
1243-
e.preventDefault(); return;
1243+
e.stopPropagation(); e.preventDefault(); return;
12441244
}
12451245
if (e.key.toLowerCase() === 'r') {
12461246
p.state.azimuth = -60; p.state.elevation = 30; p.state.zoom = 1;
12471247
draw3d(p);
12481248
model.set(`panel_${p.id}_json`, JSON.stringify(p.state));
12491249
model.save_changes();
1250-
e.preventDefault();
1250+
e.stopPropagation(); e.preventDefault();
12511251
}
12521252
});
12531253
overlayCanvas.tabIndex = 0;
@@ -1820,26 +1820,26 @@ function render({ model, el }) {
18201820
img_x:imgX, img_y:imgY,
18211821
phys_x:physX, phys_y:physY,
18221822
});
1823-
e.preventDefault(); return;
1823+
e.stopPropagation(); e.preventDefault(); return;
18241824
}
18251825
const key=e.key.toLowerCase();
18261826
if(key==='r'){
18271827
st.zoom=1; st.center_x=0.5; st.center_y=0.5;
18281828
draw2d(p); model.set(`panel_${p.id}_json`,JSON.stringify(st)); model.save_changes();
1829-
e.preventDefault();
1829+
e.stopPropagation(); e.preventDefault();
18301830
} else if(key==='c'){
18311831
st.show_colorbar=!st.show_colorbar;
18321832
draw2d(p);
18331833
model.set(`panel_${p.id}_json`,JSON.stringify(st)); model.save_changes();
1834-
e.preventDefault();
1834+
e.stopPropagation(); e.preventDefault();
18351835
} else if(key==='l'){
18361836
st.scale_mode=st.scale_mode==='log'?'linear':'log';
18371837
draw2d(p); model.set(`panel_${p.id}_json`,JSON.stringify(st)); model.save_changes();
1838-
e.preventDefault();
1838+
e.stopPropagation(); e.preventDefault();
18391839
} else if(key==='s'){
18401840
st.scale_mode=st.scale_mode==='symlog'?'linear':'symlog';
18411841
draw2d(p); model.set(`panel_${p.id}_json`,JSON.stringify(st)); model.save_changes();
1842-
e.preventDefault();
1842+
e.stopPropagation(); e.preventDefault();
18431843
}
18441844
});
18451845
overlayCanvas.addEventListener('mouseenter',()=>overlayCanvas.focus());
@@ -1933,9 +1933,9 @@ function render({ model, el }) {
19331933
mouse_x:p.mouseX, mouse_y:p.mouseY,
19341934
phys_x:physX,
19351935
});
1936-
e.preventDefault(); return;
1936+
e.stopPropagation(); e.preventDefault(); return;
19371937
}
1938-
if(e.key.toLowerCase()==='r'){st.view_x0=0;st.view_x1=1;draw1d(p);model.set(`panel_${p.id}_json`,JSON.stringify(st));model.save_changes();e.preventDefault();}
1938+
if(e.key.toLowerCase()==='r'){st.view_x0=0;st.view_x1=1;draw1d(p);model.set(`panel_${p.id}_json`,JSON.stringify(st));model.save_changes();e.stopPropagation();e.preventDefault();}
19391939
});
19401940
overlayCanvas.tabIndex=0;overlayCanvas.style.outline='none';
19411941
overlayCanvas.addEventListener('mouseenter',()=>overlayCanvas.focus());
@@ -2767,7 +2767,7 @@ function render({ model, el }) {
27672767
last_widget_id: p.lastWidgetId || null,
27682768
mouse_x: p.mouseX, mouse_y: p.mouseY,
27692769
});
2770-
e.preventDefault();
2770+
e.stopPropagation(); e.preventDefault();
27712771
}
27722772
});
27732773
overlayCanvas.tabIndex = 0;

anyplotlib/figure_plots.py

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def _normalize_image(data: np.ndarray):
404404
"plasma": "fire", # warm sequential (dark→bright)
405405
"inferno": "kb", # dark→blue→white
406406
"magma": "kbc", # dark→blue→cyan sequential
407-
"cividis": "dimgray", # accessible, low-chroma sequential
407+
"cividis": "bgy", # accessible, blue→green→yellow sequential
408408
"hot": "fire",
409409
"afmhot": "fire",
410410
"jet": "rainbow4",
@@ -805,21 +805,58 @@ def disconnect(self, cid: int) -> None:
805805
# ------------------------------------------------------------------
806806
# View control
807807
# ------------------------------------------------------------------
808-
def set_view(self, x0: float | None = None, x1: float | None = None) -> None:
808+
def set_view(self,
809+
x0: float | None = None, x1: float | None = None,
810+
y0: float | None = None, y1: float | None = None) -> None:
811+
"""Set the viewport to a data-space rectangle.
812+
813+
Parameters
814+
----------
815+
x0, x1 : float, optional
816+
Horizontal data-space range to show. If omitted the full
817+
x-extent is used for zoom calculation.
818+
y0, y1 : float, optional
819+
Vertical data-space range to show. If omitted the full
820+
y-extent is used for zoom calculation.
821+
822+
Translates the requested rectangle into the ``zoom`` / ``center_x``
823+
/ ``center_y`` state values used by the 2-D JS renderer.
824+
"""
809825
xarr = np.asarray(self._state["x_axis"])
810-
if len(xarr) < 2:
826+
yarr = np.asarray(self._state["y_axis"])
827+
if len(xarr) < 2 or len(yarr) < 2:
811828
return
829+
812830
xmin, xmax = float(xarr[0]), float(xarr[-1])
813-
span = xmax - xmin or 1.0
814-
f0 = 0.0 if x0 is None else max(0.0, min(1.0, (float(x0)-xmin)/span))
815-
f1 = 1.0 if x1 is None else max(0.0, min(1.0, (float(x1)-xmin)/span))
816-
self._state["view_x0"] = f0
817-
self._state["view_x1"] = f1
831+
ymin, ymax = float(yarr[0]), float(yarr[-1])
832+
x_span = xmax - xmin or 1.0
833+
y_span = ymax - ymin or 1.0
834+
835+
zoom_candidates = []
836+
837+
if x0 is not None and x1 is not None:
838+
fx0 = max(0.0, min(1.0, (float(x0) - xmin) / x_span))
839+
fx1 = max(0.0, min(1.0, (float(x1) - xmin) / x_span))
840+
if fx1 > fx0:
841+
self._state["center_x"] = (fx0 + fx1) / 2.0
842+
zoom_candidates.append(1.0 / (fx1 - fx0))
843+
844+
if y0 is not None and y1 is not None:
845+
fy0 = max(0.0, min(1.0, (float(y0) - ymin) / y_span))
846+
fy1 = max(0.0, min(1.0, (float(y1) - ymin) / y_span))
847+
if fy1 > fy0:
848+
self._state["center_y"] = (fy0 + fy1) / 2.0
849+
zoom_candidates.append(1.0 / (fy1 - fy0))
850+
851+
if zoom_candidates:
852+
self._state["zoom"] = min(zoom_candidates)
818853
self._push()
819854

820855
def reset_view(self) -> None:
821-
self._state["view_x0"] = 0.0
822-
self._state["view_x1"] = 1.0
856+
"""Reset pan and zoom to show the full image."""
857+
self._state["zoom"] = 1.0
858+
self._state["center_x"] = 0.5
859+
self._state["center_y"] = 0.5
823860
self._push()
824861

825862
# ------------------------------------------------------------------
@@ -833,8 +870,8 @@ def add_circles(self, offsets, name=None, *, radius=5,
833870
linewidths=1.5, alpha=0.3,
834871
hover_edgecolors=None, hover_facecolors=None,
835872
labels=None, label=None) -> "MarkerGroup": # noqa: F821
836-
# On 1-D panels the native type is "points" (radius maps to sizes).
837-
return self._add_marker("points", name, offsets=offsets, sizes=radius,
873+
"""Add circle markers at (x, y) positions in data coordinates."""
874+
return self._add_marker("circles", name, offsets=offsets, radius=radius,
838875
facecolors=facecolors, edgecolors=edgecolors,
839876
linewidths=linewidths, alpha=alpha,
840877
hover_edgecolors=hover_edgecolors,
@@ -847,7 +884,7 @@ def add_points(self, offsets, name=None, *, sizes=5,
847884
hover_edgecolors=None, hover_facecolors=None,
848885
labels=None, label=None) -> "MarkerGroup": # noqa: F821
849886
"""Add point markers at (x, y) positions in data coordinates."""
850-
return self._add_marker("points", name, offsets=offsets, sizes=sizes,
887+
return self._add_marker("circles", name, offsets=offsets, radius=sizes,
851888
edgecolors=color, facecolors=facecolors,
852889
linewidths=linewidths, alpha=alpha,
853890
hover_edgecolors=hover_edgecolors,
@@ -965,6 +1002,12 @@ def list_markers(self) -> list:
9651002
out.append({"type": mtype, "name": name, "n": g._count()})
9661003
return out
9671004

1005+
def __repr__(self) -> str:
1006+
w = self._state.get("image_width", "?")
1007+
h = self._state.get("image_height", "?")
1008+
cmap = self._state.get("colormap_name", "?")
1009+
return f"Plot2D({w}\u00d7{h}, cmap={cmap!r})"
1010+
9681011

9691012
# ---------------------------------------------------------------------------
9701013
# PlotMesh (pcolormesh-style 2-D panel)
@@ -1323,6 +1366,11 @@ def update(self, x, y, z) -> None:
13231366
})
13241367
self._push()
13251368

1369+
def __repr__(self) -> str:
1370+
geom = self._state.get("geom_type", "?")
1371+
n = len(self._state.get("vertices", []))
1372+
return f"Plot3D(geom={geom!r}, n_vertices={n})"
1373+
13261374

13271375
# ---------------------------------------------------------------------------
13281376
# Plot1D
@@ -1779,6 +1827,11 @@ def list_markers(self) -> list:
17791827
out.append({"type": mtype, "name": name, "n": g._count()})
17801828
return out
17811829

1830+
def __repr__(self) -> str:
1831+
n = len(self._state.get("data", []))
1832+
color = self._state.get("line_color", "?")
1833+
return f"Plot1D(n={n}, color={color!r})"
1834+
17821835

17831836
# ---------------------------------------------------------------------------
17841837
# _bar_x_axis helper

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ requires-python = ">=3.10"
1313
dependencies = [
1414
"anywidget>=0.9.0",
1515
"colorcet>=3.0",
16-
"jupyterlab>=4.5.5",
1716
"numpy>=2.0.0",
18-
"pytest>=9.0.2",
1917
"traitlets>=5.0.0",
2018
]
2119

@@ -27,10 +25,14 @@ docs = [
2725
"pillow>=10.0",
2826
"matplotlib>=3.7",
2927
]
28+
jupyter = [
29+
"jupyterlab>=4.5.5",
30+
]
3031

3132
[dependency-groups]
3233
dev = [
3334
"playwright>=1.58.0",
35+
"pytest>=9.0.2",
3436
]
3537

3638

0 commit comments

Comments
 (0)