jswidget 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jswidget-0.1.0/.gitignore +17 -0
- jswidget-0.1.0/LICENSE +28 -0
- jswidget-0.1.0/PKG-INFO +149 -0
- jswidget-0.1.0/README.md +97 -0
- jswidget-0.1.0/pyproject.toml +46 -0
- jswidget-0.1.0/src/jswidget/__init__.py +21 -0
- jswidget-0.1.0/src/jswidget/jswidget.py +233 -0
jswidget-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DannyRuijters
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
jswidget-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jswidget
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight anywidget-based IPython widget that renders arbitrary JavaScript inside Jupyter notebooks
|
|
5
|
+
Project-URL: Homepage, https://github.com/DannyRuijters/JSWidget
|
|
6
|
+
Project-URL: Repository, https://github.com/DannyRuijters/JSWidget
|
|
7
|
+
Author: DannyRuijters
|
|
8
|
+
License: BSD 3-Clause License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026, DannyRuijters
|
|
11
|
+
|
|
12
|
+
Redistribution and use in source and binary forms, with or without
|
|
13
|
+
modification, are permitted provided that the following conditions are met:
|
|
14
|
+
|
|
15
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
16
|
+
list of conditions and the following disclaimer.
|
|
17
|
+
|
|
18
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
19
|
+
this list of conditions and the following disclaimer in the documentation
|
|
20
|
+
and/or other materials provided with the distribution.
|
|
21
|
+
|
|
22
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
23
|
+
contributors may be used to endorse or promote products derived from
|
|
24
|
+
this software without specific prior written permission.
|
|
25
|
+
|
|
26
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
27
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
28
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
29
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
30
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
31
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
32
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
33
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
34
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
35
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Keywords: anywidget,ipython,javascript,jupyter,widget
|
|
38
|
+
Classifier: Development Status :: 4 - Beta
|
|
39
|
+
Classifier: Framework :: Jupyter
|
|
40
|
+
Classifier: Framework :: Jupyter :: JupyterLab
|
|
41
|
+
Classifier: Intended Audience :: Developers
|
|
42
|
+
Classifier: Intended Audience :: Science/Research
|
|
43
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
44
|
+
Classifier: Programming Language :: JavaScript
|
|
45
|
+
Classifier: Programming Language :: Python :: 3
|
|
46
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
47
|
+
Requires-Python: >=3.8
|
|
48
|
+
Requires-Dist: anywidget
|
|
49
|
+
Requires-Dist: numpy
|
|
50
|
+
Requires-Dist: traitlets
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
# JSWidget
|
|
54
|
+
|
|
55
|
+
A lightweight [anywidget](https://anywidget.dev/)-based IPython widget that renders arbitrary JavaScript (Canvas 2D, WebGL, etc.) inside Jupyter notebooks. It passes JSON data and binary buffers (mesh vertices, normals, indices) over the ipywidgets comm API, with live-update callbacks so Python can push new data without re-executing the cell.
|
|
56
|
+
|
|
57
|
+
Works in **VS Code**, **JupyterLab**, and **classic Jupyter notebooks** — no extra frontend build step required.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install jswidget
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or install from source in development mode:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/DannyRuijters/JSWidget.git
|
|
69
|
+
cd JSWidget
|
|
70
|
+
pip install -e .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from jswidget import JSWidget
|
|
77
|
+
|
|
78
|
+
w = JSWidget(width=600, height=400)
|
|
79
|
+
w.js_code = '''
|
|
80
|
+
const canvas = document.createElement("canvas");
|
|
81
|
+
canvas.width = opts.width;
|
|
82
|
+
canvas.height = opts.height;
|
|
83
|
+
el.appendChild(canvas);
|
|
84
|
+
const ctx = canvas.getContext("2d");
|
|
85
|
+
ctx.fillStyle = "#e94560";
|
|
86
|
+
ctx.font = "24px sans-serif";
|
|
87
|
+
ctx.fillText("Hello from JSWidget!", 20, 50);
|
|
88
|
+
'''
|
|
89
|
+
w.show()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## JavaScript API
|
|
93
|
+
|
|
94
|
+
The JavaScript code passed to `js_code` receives the following variables:
|
|
95
|
+
|
|
96
|
+
| Variable | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `el` | Container DOM element to render into |
|
|
99
|
+
| `data` | JSON data dict set from Python via `w.data` |
|
|
100
|
+
| `getBuffer(name)` | Returns a named binary buffer as an `ArrayBuffer` |
|
|
101
|
+
| `opts` | `{width, height}` of the widget |
|
|
102
|
+
| `setState(obj)` | Save state that persists across re-renders |
|
|
103
|
+
| `getState()` | Retrieve previously saved state |
|
|
104
|
+
| `onData(fn)` | Register a callback for live `data` updates from Python |
|
|
105
|
+
| `onBuffers(fn)` | Register a callback for live buffer updates from Python |
|
|
106
|
+
|
|
107
|
+
## Passing Data
|
|
108
|
+
|
|
109
|
+
### JSON data
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
w.data = {'values': [10, 40, 80], 'color': '#4ecdc4'}
|
|
113
|
+
# Update later (triggers onData callbacks in JS):
|
|
114
|
+
w.send_data({'values': [90, 20, 55], 'color': '#ff6b6b'})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Binary buffers
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import numpy as np
|
|
121
|
+
|
|
122
|
+
vertices = np.array([[0,0,0],[1,0,0],[0,1,0]], dtype=np.float32)
|
|
123
|
+
normals = np.array([[0,0,1],[0,0,1],[0,0,1]], dtype=np.float32)
|
|
124
|
+
indices = np.array([0, 1, 2], dtype=np.uint32)
|
|
125
|
+
|
|
126
|
+
w.set_buffers(vertices=vertices, normals=normals, indices=indices)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Buffers are accessible in JavaScript via `getBuffer('vertices')`, etc., and return an `ArrayBuffer` that can be wrapped in a typed array (e.g. `new Float32Array(getBuffer('vertices'))`).
|
|
130
|
+
|
|
131
|
+
## Demo Notebook
|
|
132
|
+
|
|
133
|
+
See [JSWidget_demo.ipynb](JSWidget_demo.ipynb) for full examples including:
|
|
134
|
+
|
|
135
|
+
1. **Canvas 2D drawing** — gradient backgrounds and text rendering
|
|
136
|
+
2. **Bar chart with live updates** — pass data from Python via `send_data()` and re-draw with `onData()`
|
|
137
|
+
3. **Interactive WebGL mesh viewer** — sphere and torus rendering with mouse-drag rotation and scroll zoom, using binary buffers for mesh data
|
|
138
|
+
|
|
139
|
+
## Files
|
|
140
|
+
|
|
141
|
+
| File | Description |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `jswidget.py` | The `JSWidget` class (anywidget-based DOMWidget with ESM frontend) |
|
|
144
|
+
| `JSWidget_demo.ipynb` | Demo notebook with Canvas 2D, bar chart, and WebGL examples |
|
|
145
|
+
| `LICENSE` | BSD 3-Clause License |
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
BSD 3-Clause — see [LICENSE](LICENSE).
|
jswidget-0.1.0/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# JSWidget
|
|
2
|
+
|
|
3
|
+
A lightweight [anywidget](https://anywidget.dev/)-based IPython widget that renders arbitrary JavaScript (Canvas 2D, WebGL, etc.) inside Jupyter notebooks. It passes JSON data and binary buffers (mesh vertices, normals, indices) over the ipywidgets comm API, with live-update callbacks so Python can push new data without re-executing the cell.
|
|
4
|
+
|
|
5
|
+
Works in **VS Code**, **JupyterLab**, and **classic Jupyter notebooks** — no extra frontend build step required.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install jswidget
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install from source in development mode:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/DannyRuijters/JSWidget.git
|
|
17
|
+
cd JSWidget
|
|
18
|
+
pip install -e .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from jswidget import JSWidget
|
|
25
|
+
|
|
26
|
+
w = JSWidget(width=600, height=400)
|
|
27
|
+
w.js_code = '''
|
|
28
|
+
const canvas = document.createElement("canvas");
|
|
29
|
+
canvas.width = opts.width;
|
|
30
|
+
canvas.height = opts.height;
|
|
31
|
+
el.appendChild(canvas);
|
|
32
|
+
const ctx = canvas.getContext("2d");
|
|
33
|
+
ctx.fillStyle = "#e94560";
|
|
34
|
+
ctx.font = "24px sans-serif";
|
|
35
|
+
ctx.fillText("Hello from JSWidget!", 20, 50);
|
|
36
|
+
'''
|
|
37
|
+
w.show()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## JavaScript API
|
|
41
|
+
|
|
42
|
+
The JavaScript code passed to `js_code` receives the following variables:
|
|
43
|
+
|
|
44
|
+
| Variable | Description |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `el` | Container DOM element to render into |
|
|
47
|
+
| `data` | JSON data dict set from Python via `w.data` |
|
|
48
|
+
| `getBuffer(name)` | Returns a named binary buffer as an `ArrayBuffer` |
|
|
49
|
+
| `opts` | `{width, height}` of the widget |
|
|
50
|
+
| `setState(obj)` | Save state that persists across re-renders |
|
|
51
|
+
| `getState()` | Retrieve previously saved state |
|
|
52
|
+
| `onData(fn)` | Register a callback for live `data` updates from Python |
|
|
53
|
+
| `onBuffers(fn)` | Register a callback for live buffer updates from Python |
|
|
54
|
+
|
|
55
|
+
## Passing Data
|
|
56
|
+
|
|
57
|
+
### JSON data
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
w.data = {'values': [10, 40, 80], 'color': '#4ecdc4'}
|
|
61
|
+
# Update later (triggers onData callbacks in JS):
|
|
62
|
+
w.send_data({'values': [90, 20, 55], 'color': '#ff6b6b'})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Binary buffers
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import numpy as np
|
|
69
|
+
|
|
70
|
+
vertices = np.array([[0,0,0],[1,0,0],[0,1,0]], dtype=np.float32)
|
|
71
|
+
normals = np.array([[0,0,1],[0,0,1],[0,0,1]], dtype=np.float32)
|
|
72
|
+
indices = np.array([0, 1, 2], dtype=np.uint32)
|
|
73
|
+
|
|
74
|
+
w.set_buffers(vertices=vertices, normals=normals, indices=indices)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Buffers are accessible in JavaScript via `getBuffer('vertices')`, etc., and return an `ArrayBuffer` that can be wrapped in a typed array (e.g. `new Float32Array(getBuffer('vertices'))`).
|
|
78
|
+
|
|
79
|
+
## Demo Notebook
|
|
80
|
+
|
|
81
|
+
See [JSWidget_demo.ipynb](JSWidget_demo.ipynb) for full examples including:
|
|
82
|
+
|
|
83
|
+
1. **Canvas 2D drawing** — gradient backgrounds and text rendering
|
|
84
|
+
2. **Bar chart with live updates** — pass data from Python via `send_data()` and re-draw with `onData()`
|
|
85
|
+
3. **Interactive WebGL mesh viewer** — sphere and torus rendering with mouse-drag rotation and scroll zoom, using binary buffers for mesh data
|
|
86
|
+
|
|
87
|
+
## Files
|
|
88
|
+
|
|
89
|
+
| File | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `jswidget.py` | The `JSWidget` class (anywidget-based DOMWidget with ESM frontend) |
|
|
92
|
+
| `JSWidget_demo.ipynb` | Demo notebook with Canvas 2D, bar chart, and WebGL examples |
|
|
93
|
+
| `LICENSE` | BSD 3-Clause License |
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
BSD 3-Clause — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jswidget"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight anywidget-based IPython widget that renders arbitrary JavaScript inside Jupyter notebooks"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {file = "LICENSE"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "DannyRuijters"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["jupyter", "widget", "javascript", "anywidget", "ipython"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Framework :: Jupyter",
|
|
19
|
+
"Framework :: Jupyter :: JupyterLab",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"License :: OSI Approved :: BSD License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: JavaScript",
|
|
25
|
+
"Topic :: Multimedia :: Graphics",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"anywidget",
|
|
29
|
+
"numpy",
|
|
30
|
+
"traitlets",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/DannyRuijters/JSWidget"
|
|
35
|
+
Repository = "https://github.com/DannyRuijters/JSWidget"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/jswidget"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = [
|
|
42
|
+
"src/jswidget/",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"pyproject.toml",
|
|
46
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSWidget - A lightweight anywidget-based IPython widget that renders
|
|
3
|
+
arbitrary JavaScript inside Jupyter notebooks.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from jswidget import JSWidget
|
|
7
|
+
|
|
8
|
+
w = JSWidget(width=800, height=600)
|
|
9
|
+
w.js_code = '''
|
|
10
|
+
const canvas = document.createElement("canvas");
|
|
11
|
+
canvas.width = opts.width;
|
|
12
|
+
canvas.height = opts.height;
|
|
13
|
+
el.appendChild(canvas);
|
|
14
|
+
'''
|
|
15
|
+
w.show()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from jswidget.jswidget import JSWidget
|
|
19
|
+
|
|
20
|
+
__all__ = ["JSWidget"]
|
|
21
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom DOMWidget that renders arbitrary JavaScript and passes data
|
|
3
|
+
(including user-defined binary buffers) over the ipywidgets comm API.
|
|
4
|
+
|
|
5
|
+
Uses anywidget for reliable rendering in VS Code, JupyterLab, and classic notebooks.
|
|
6
|
+
No external dependencies beyond anywidget and numpy.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from jswidget import JSWidget
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
w = JSWidget(width=800, height=600)
|
|
13
|
+
|
|
14
|
+
# Set binary buffers (any name, created dynamically)
|
|
15
|
+
vertices = np.array([[0,0,0],[1,0,0],[0,1,0]], dtype=np.float32)
|
|
16
|
+
indices = np.array([0,1,2], dtype=np.uint32)
|
|
17
|
+
w.set_buffers(vertices=vertices, normals=normals, indices=indices)
|
|
18
|
+
|
|
19
|
+
# Set JSON data
|
|
20
|
+
w.data = {'opacity': 0.8, 'color': [1, 0, 0]}
|
|
21
|
+
|
|
22
|
+
# Set JavaScript code and display
|
|
23
|
+
w.js_code = '''
|
|
24
|
+
const canvas = document.createElement('canvas');
|
|
25
|
+
canvas.width = opts.width;
|
|
26
|
+
canvas.height = opts.height;
|
|
27
|
+
el.appendChild(canvas);
|
|
28
|
+
const ctx = canvas.getContext('2d');
|
|
29
|
+
|
|
30
|
+
// Access binary buffers
|
|
31
|
+
const verts = new Float32Array(getBuffer('vertices'));
|
|
32
|
+
|
|
33
|
+
// Access JSON data
|
|
34
|
+
const opacity = data.opacity;
|
|
35
|
+
|
|
36
|
+
// Register for live updates from Python
|
|
37
|
+
onData((newData) => { ... });
|
|
38
|
+
onBuffers((newMeta) => { ... });
|
|
39
|
+
|
|
40
|
+
// Save state to survive re-renders
|
|
41
|
+
setState({rotX: 0.3, rotY: 0.5});
|
|
42
|
+
'''
|
|
43
|
+
w.show()
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
import traitlets
|
|
48
|
+
import anywidget
|
|
49
|
+
|
|
50
|
+
# The ESM frontend module - provides the same JS API as jswidget.py
|
|
51
|
+
_ESM = """
|
|
52
|
+
function render({ model, el }) {
|
|
53
|
+
// Create container
|
|
54
|
+
const container = document.createElement('div');
|
|
55
|
+
container.style.width = model.get('width') + 'px';
|
|
56
|
+
container.style.height = model.get('height') + 'px';
|
|
57
|
+
container.style.overflow = 'hidden';
|
|
58
|
+
container.style.position = 'relative';
|
|
59
|
+
el.appendChild(container);
|
|
60
|
+
|
|
61
|
+
let cleanupFn = null;
|
|
62
|
+
const _id = Math.random().toString(36).slice(2, 14);
|
|
63
|
+
|
|
64
|
+
function executeCode() {
|
|
65
|
+
const code = model.get('js_code');
|
|
66
|
+
if (!code) return;
|
|
67
|
+
|
|
68
|
+
// Cleanup previous execution
|
|
69
|
+
if (cleanupFn) {
|
|
70
|
+
try { cleanupFn(); } catch(e) {}
|
|
71
|
+
cleanupFn = null;
|
|
72
|
+
}
|
|
73
|
+
container.innerHTML = '';
|
|
74
|
+
|
|
75
|
+
// State management
|
|
76
|
+
window.__jsw_state = window.__jsw_state || {};
|
|
77
|
+
function setState(obj) { window.__jsw_state[_id] = Object.assign(window.__jsw_state[_id] || {}, obj); }
|
|
78
|
+
function getState() { return window.__jsw_state[_id] || {}; }
|
|
79
|
+
|
|
80
|
+
// opts
|
|
81
|
+
const opts = { width: model.get('width'), height: model.get('height') };
|
|
82
|
+
|
|
83
|
+
// data
|
|
84
|
+
let currentData = model.get('data') || {};
|
|
85
|
+
|
|
86
|
+
// getBuffer
|
|
87
|
+
function getBuffer(name) {
|
|
88
|
+
const raw = model.get('_buf_' + name);
|
|
89
|
+
if (!raw) return null;
|
|
90
|
+
if (raw instanceof DataView) {
|
|
91
|
+
if (raw.byteLength === 0) return null;
|
|
92
|
+
return raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength);
|
|
93
|
+
}
|
|
94
|
+
if (raw instanceof ArrayBuffer) {
|
|
95
|
+
return raw.byteLength === 0 ? null : raw;
|
|
96
|
+
}
|
|
97
|
+
if (raw.buffer instanceof ArrayBuffer) {
|
|
98
|
+
if (raw.byteLength === 0) return null;
|
|
99
|
+
return raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Callback registries
|
|
105
|
+
const _dataCallbacks = [];
|
|
106
|
+
const _bufferCallbacks = [];
|
|
107
|
+
function onData(fn) { _dataCallbacks.push(fn); }
|
|
108
|
+
function onBuffers(fn) { _bufferCallbacks.push(fn); }
|
|
109
|
+
|
|
110
|
+
// Wire up model change events to callbacks
|
|
111
|
+
function _onDataChange() {
|
|
112
|
+
currentData = model.get('data') || {};
|
|
113
|
+
_dataCallbacks.forEach(fn => fn(currentData));
|
|
114
|
+
}
|
|
115
|
+
function _onBuffersChange() {
|
|
116
|
+
const meta = model.get('_buffers_metadata') || [];
|
|
117
|
+
_bufferCallbacks.forEach(fn => fn(meta));
|
|
118
|
+
}
|
|
119
|
+
model.on('change:data', _onDataChange);
|
|
120
|
+
model.on('change:_buffers_metadata', _onBuffersChange);
|
|
121
|
+
|
|
122
|
+
// Register cleanup to remove listeners
|
|
123
|
+
cleanupFn = () => {
|
|
124
|
+
model.off('change:data', _onDataChange);
|
|
125
|
+
model.off('change:_buffers_metadata', _onBuffersChange);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const fn = new Function('el', 'data', 'getBuffer', 'opts', 'setState', 'getState', 'onData', 'onBuffers', code);
|
|
130
|
+
fn(container, currentData, getBuffer, opts, setState, getState, onData, onBuffers);
|
|
131
|
+
} catch(e) {
|
|
132
|
+
const errDiv = document.createElement('pre');
|
|
133
|
+
errDiv.style.color = 'red';
|
|
134
|
+
errDiv.style.padding = '10px';
|
|
135
|
+
errDiv.textContent = 'JS Error: ' + e.message + '\\n' + e.stack;
|
|
136
|
+
container.appendChild(errDiv);
|
|
137
|
+
console.error('JSWidget execution error:', e);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Execute initial code
|
|
142
|
+
executeCode();
|
|
143
|
+
|
|
144
|
+
// Re-execute when js_code changes
|
|
145
|
+
model.on('change:js_code', executeCode);
|
|
146
|
+
|
|
147
|
+
// Update container size
|
|
148
|
+
model.on('change:width', () => {
|
|
149
|
+
container.style.width = model.get('width') + 'px';
|
|
150
|
+
});
|
|
151
|
+
model.on('change:height', () => {
|
|
152
|
+
container.style.height = model.get('height') + 'px';
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
if (cleanupFn) {
|
|
157
|
+
try { cleanupFn(); } catch(e) {}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export default { render };
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class JSWidget(anywidget.AnyWidget):
|
|
166
|
+
"""A DOMWidget that renders arbitrary JavaScript with binary data.
|
|
167
|
+
Uses anywidget for reliable comm-based rendering in all environments.
|
|
168
|
+
|
|
169
|
+
The JavaScript code receives:
|
|
170
|
+
el - the container DOM element
|
|
171
|
+
data - the JSON data dict
|
|
172
|
+
getBuffer(n) - get named buffer as ArrayBuffer
|
|
173
|
+
opts - {width, height}
|
|
174
|
+
setState(obj) - save state that persists across re-renders
|
|
175
|
+
getState() - retrieve previously saved state
|
|
176
|
+
onData(fn) - register callback for data updates
|
|
177
|
+
onBuffers(fn) - register callback for buffer updates
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
_esm = _ESM
|
|
181
|
+
|
|
182
|
+
# User-provided JavaScript code to execute in the widget
|
|
183
|
+
js_code = traitlets.Unicode('').tag(sync=True)
|
|
184
|
+
|
|
185
|
+
# JSON-serializable data dict passed to JavaScript
|
|
186
|
+
data = traitlets.Dict({}).tag(sync=True)
|
|
187
|
+
|
|
188
|
+
# Widget dimensions
|
|
189
|
+
width = traitlets.Int(800).tag(sync=True)
|
|
190
|
+
height = traitlets.Int(600).tag(sync=True)
|
|
191
|
+
|
|
192
|
+
# Metadata about binary buffers (triggers onBuffers callback in JS)
|
|
193
|
+
# Binary buffer traits (_buf_<name>) are created dynamically by set_buffers().
|
|
194
|
+
_buffers_metadata = traitlets.List([]).tag(sync=True)
|
|
195
|
+
|
|
196
|
+
def set_buffers(self, **named_buffers):
|
|
197
|
+
"""Set binary data buffers for the JavaScript frontend.
|
|
198
|
+
|
|
199
|
+
Each keyword argument should be a numpy array, bytes, or bytearray.
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
w.set_buffers(vertices=vertices, normals=normals, indices=indices)
|
|
203
|
+
"""
|
|
204
|
+
metadata = []
|
|
205
|
+
with self.hold_sync():
|
|
206
|
+
for name, buf in named_buffers.items():
|
|
207
|
+
trait_name = f'_buf_{name}'
|
|
208
|
+
if not self.has_trait(trait_name):
|
|
209
|
+
self.add_traits(**{trait_name: traitlets.Bytes(b'').tag(sync=True)})
|
|
210
|
+
|
|
211
|
+
if isinstance(buf, np.ndarray):
|
|
212
|
+
metadata.append({'name': name, 'dtype': str(buf.dtype), 'shape': list(buf.shape)})
|
|
213
|
+
setattr(self, trait_name, buf.tobytes())
|
|
214
|
+
elif isinstance(buf, (bytes, bytearray, memoryview)):
|
|
215
|
+
metadata.append({'name': name, 'dtype': 'bytes', 'shape': [len(buf)]})
|
|
216
|
+
setattr(self, trait_name, bytes(buf))
|
|
217
|
+
else:
|
|
218
|
+
raise TypeError(f"Buffer '{name}' must be numpy array, bytes, or bytearray")
|
|
219
|
+
|
|
220
|
+
self._buffers_metadata = metadata
|
|
221
|
+
|
|
222
|
+
def send_data(self, data_dict):
|
|
223
|
+
"""Update the data dict and push to JavaScript."""
|
|
224
|
+
self.data = data_dict
|
|
225
|
+
|
|
226
|
+
def execute(self, js_code):
|
|
227
|
+
"""Update the JavaScript code and re-render."""
|
|
228
|
+
self.js_code = js_code
|
|
229
|
+
|
|
230
|
+
def show(self):
|
|
231
|
+
"""Display the widget."""
|
|
232
|
+
from IPython.display import display
|
|
233
|
+
display(self)
|