Skip to content

Commit 96f03c7

Browse files
author
root
committed
Add notebook cell renderer support
1 parent 64d6043 commit 96f03c7

7 files changed

Lines changed: 143 additions & 0 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ h.set_data((0.8 * y).astype(np.float32), x=x)
8282
p.render()
8383
```
8484

85+
## Notebook Cell Renderer
86+
87+
`Plot`, `Subplots`, `AlignedPlots`, and `Dashboard` implement a Jupyter widget
88+
MIME renderer. Put the object as the last expression in a notebook cell to
89+
render it directly:
90+
91+
```python
92+
p = ip.Plot(width=900, height=450, title="Cell renderer")
93+
p.line("mid", y, x=x)
94+
p
95+
```
96+
97+
Use `p.show()` when you want to display the plot before the last line of a cell,
98+
or when the final expression is a printout, table, or another object.
99+
85100
## Direct Web App Usage
86101

87102
Use `@nbimplot/web` when you want ImPlot/WASM rendering in a normal browser

docs/EXAMPLES.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ import numpy as np
1212
import nbimplot as ip
1313
```
1414

15+
## Notebook Cell Rendering
16+
17+
`nbimplot` plots are Jupyter widgets. If a plot-like object is the last
18+
expression in a notebook cell, Jupyter renders the ImPlot/WASM canvas directly:
19+
20+
```python
21+
t = np.linspace(0, 20, 20_000, dtype=np.float32)
22+
y = np.sin(t).astype(np.float32)
23+
24+
p = ip.Plot(width=900, height=360, title="Last Expression Render")
25+
p.line("signal", y, x=t)
26+
p
27+
```
28+
29+
Use `p.show()` when the cell continues after the display call. `Subplots`,
30+
`AlignedPlots`, and `Dashboard` support the same last-expression display path.
31+
1532
## Line and In-Place Updates
1633

1734
```python

docs/FAST_JUPYTER_PLOTTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ h = p.line("signal", y, x=x)
3333
p.show()
3434
```
3535

36+
You can also render by leaving the plot object as the final expression in the
37+
cell:
38+
39+
```python
40+
p = ip.Plot(width=900, height=450, title="Signal")
41+
p.line("signal", y, x=x)
42+
p
43+
```
44+
45+
Use `p.show()` when displaying before additional code or printed output.
46+
3647
## Update Existing Data
3748

3849
```python

nbimplot/_plot.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,32 @@ def _get_wasm_assets() -> tuple[str | None, bytes | None, str | None]:
122122
return _WASM_ASSET_CACHE
123123

124124

125+
def _mime_requested(mimetype: str, kwargs: dict[str, Any]) -> bool:
126+
include = kwargs.get("include")
127+
exclude = kwargs.get("exclude")
128+
if include is not None and mimetype not in include:
129+
return False
130+
if exclude is not None and mimetype in exclude:
131+
return False
132+
return True
133+
134+
135+
def _with_text_plain(
136+
bundle: tuple[dict[str, Any], dict[str, Any]] | None,
137+
text: str,
138+
kwargs: dict[str, Any],
139+
) -> tuple[dict[str, Any], dict[str, Any]] | None:
140+
if bundle is None:
141+
return None
142+
data, metadata = bundle
143+
data = dict(data)
144+
if _mime_requested("text/plain", kwargs):
145+
data["text/plain"] = text
146+
else:
147+
data.pop("text/plain", None)
148+
return data, metadata
149+
150+
125151
def _to_float32_1d(data: Any, *, arg_name: str) -> np.ndarray:
126152
arr = np.asarray(data)
127153
if arr.ndim != 1:
@@ -2326,6 +2352,16 @@ def show(self) -> None:
23262352
display(self)
23272353
return None
23282354

2355+
def __repr__(self) -> str:
2356+
title = f", title={self.title!r}" if self.title else ""
2357+
return (
2358+
f"nbimplot.Plot(width={self.width}, height={self.height}{title}, "
2359+
f"series={len(self._series)}, primitives={len(self._primitives)}, renderer='wasm-implot')"
2360+
)
2361+
2362+
def _repr_mimebundle_(self, **kwargs: Any) -> tuple[dict[str, Any], dict[str, Any]] | None:
2363+
return _with_text_plain(super()._repr_mimebundle_(**kwargs), repr(self), kwargs)
2364+
23292365
def close(self) -> None:
23302366
if not getattr(self, "_closed", True):
23312367
self.send({"type": "dispose"})
@@ -3065,6 +3101,16 @@ def show(self) -> None:
30653101
self._plot.show()
30663102
return None
30673103

3104+
def __repr__(self) -> str:
3105+
cls_name = type(self).__name__
3106+
return (
3107+
f"nbimplot.{cls_name}(rows={self.rows}, cols={self.cols}, title={self.title!r}, "
3108+
f"renderer='wasm-implot')"
3109+
)
3110+
3111+
def _repr_mimebundle_(self, **kwargs: Any) -> tuple[dict[str, Any], dict[str, Any]] | None:
3112+
return _with_text_plain(self._plot._repr_mimebundle_(**kwargs), repr(self), kwargs)
3113+
30683114
def render(self) -> None:
30693115
self._plot.render()
30703116

public/llms-full.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ h = p.line("mid", y, x=x)
7878
p.show()
7979
```
8080

81+
Notebook cell rendering:
82+
83+
```python
84+
p = ip.Plot(width=900, height=450, title="Signal")
85+
p.line("mid", y, x=x)
86+
p
87+
```
88+
89+
`Plot`, `Subplots`, `AlignedPlots`, and `Dashboard` provide Jupyter widget MIME
90+
bundles for last-expression cell rendering. Use `p.show()` when displaying
91+
before later code, print output, tables, or another final expression.
92+
8193
Update existing data:
8294

8395
```python

public/llms.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ p.on_select(lambda plot, event: print(plot.indices_for_selection(event)))
4040
p.highlight_selection({"x_min": 0, "x_max": 1, "y_min": -1, "y_max": 1})
4141
```
4242

43+
Notebook cell rendering:
44+
- `Plot`, `Subplots`, `AlignedPlots`, and `Dashboard` render as Jupyter widget MIME bundles when they are the final expression in a cell.
45+
- Use `p` as the last line for direct cell rendering.
46+
- Use `p.show()` when displaying before later code, prints, tables, or another final expression.
47+
4348
Web example:
4449
```js
4550
import { createPlot } from "@nbimplot/web";

tests/test_plot.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,3 +993,40 @@ def test_show_returns_none_to_prevent_double_display():
993993
result = sub.show()
994994
assert result is None
995995
assert mocked_display.call_count == 1
996+
997+
998+
def test_plot_has_notebook_cell_renderer_mimebundle():
999+
plot = ip.Plot(width=640, height=320, title="Cell Renderer")
1000+
_capture_messages(plot)
1001+
plot.line("mid", np.arange(5, dtype=np.float32))
1002+
1003+
bundle = plot._repr_mimebundle_()
1004+
assert bundle is not None
1005+
data, metadata = bundle
1006+
1007+
assert metadata == {}
1008+
assert "application/vnd.jupyter.widget-view+json" in data
1009+
assert data["application/vnd.jupyter.widget-view+json"]["version_major"] == 2
1010+
assert data["text/plain"] == (
1011+
"nbimplot.Plot(width=640, height=320, title='Cell Renderer', "
1012+
"series=1, primitives=0, renderer='wasm-implot')"
1013+
)
1014+
1015+
data, _ = plot._repr_mimebundle_(exclude=["text/plain"])
1016+
assert "text/plain" not in data
1017+
1018+
1019+
def test_subplot_wrappers_delegate_notebook_cell_renderer():
1020+
sub = ip.Subplots(2, 2, title="Grid")
1021+
bundle = sub._repr_mimebundle_()
1022+
assert bundle is not None
1023+
data, _ = bundle
1024+
assert "application/vnd.jupyter.widget-view+json" in data
1025+
assert data["text/plain"] == "nbimplot.Subplots(rows=2, cols=2, title='Grid', renderer='wasm-implot')"
1026+
1027+
dash = ip.Dashboard(1, 2, title="Desk")
1028+
bundle = dash._repr_mimebundle_()
1029+
assert bundle is not None
1030+
data, _ = bundle
1031+
assert "application/vnd.jupyter.widget-view+json" in data
1032+
assert data["text/plain"] == "nbimplot.Dashboard(rows=1, cols=2, title='Desk', renderer='wasm-implot')"

0 commit comments

Comments
 (0)