bigraph-viz2 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.
- bigraph_viz2-0.1.0/PKG-INFO +199 -0
- bigraph_viz2-0.1.0/README.md +172 -0
- bigraph_viz2-0.1.0/bigraph_viz2/__init__.py +11 -0
- bigraph_viz2-0.1.0/bigraph_viz2/_bundle/__init__.py +1 -0
- bigraph_viz2-0.1.0/bigraph_viz2/_bundle/bigraph-viz2.css +1 -0
- bigraph_viz2-0.1.0/bigraph_viz2/_bundle/bigraph-viz2.iife.min.js +1 -0
- bigraph_viz2-0.1.0/bigraph_viz2/emit.py +77 -0
- bigraph_viz2-0.1.0/bigraph_viz2/jupyter.py +11 -0
- bigraph_viz2-0.1.0/bigraph_viz2.egg-info/PKG-INFO +199 -0
- bigraph_viz2-0.1.0/bigraph_viz2.egg-info/SOURCES.txt +16 -0
- bigraph_viz2-0.1.0/bigraph_viz2.egg-info/dependency_links.txt +1 -0
- bigraph_viz2-0.1.0/bigraph_viz2.egg-info/requires.txt +4 -0
- bigraph_viz2-0.1.0/bigraph_viz2.egg-info/top_level.txt +1 -0
- bigraph_viz2-0.1.0/pyproject.toml +41 -0
- bigraph_viz2-0.1.0/setup.cfg +4 -0
- bigraph_viz2-0.1.0/tests/test_e2e.py +44 -0
- bigraph_viz2-0.1.0/tests/test_emit.py +43 -0
- bigraph_viz2-0.1.0/tests/test_jupyter.py +10 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bigraph-viz2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight read-only bigraph renderer with no graphviz dependency
|
|
5
|
+
Author: The Vivarium Collective
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/vivarium-collective/bigraph-viz2
|
|
8
|
+
Project-URL: Repository, https://github.com/vivarium-collective/bigraph-viz2
|
|
9
|
+
Project-URL: Issues, https://github.com/vivarium-collective/bigraph-viz2/issues
|
|
10
|
+
Keywords: bigraph,vivarium,process-bigraph,visualization,svg,jupyter
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
26
|
+
Requires-Dist: playwright>=1.45; extra == "test"
|
|
27
|
+
|
|
28
|
+
# bigraph-viz2
|
|
29
|
+
|
|
30
|
+
A lightweight, interactive, read-only renderer for
|
|
31
|
+
[process-bigraph](https://github.com/vivarium-collective/process-bigraph)
|
|
32
|
+
composites. Drop-in replacement for `bigraph-viz`'s static PNG output in
|
|
33
|
+
HTML reports — **no graphviz dependency**, pan / zoom / click-to-inspect /
|
|
34
|
+
double-click-to-collapse / Alt-drag-to-rearrange in the browser, and the
|
|
35
|
+
entire renderer inlines into a self-contained ~40 KB snippet.
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
The example above shows a two-cell tissue with nested substores (membrane,
|
|
40
|
+
cytoplasm, nucleus), processes living inside each, and wires reaching
|
|
41
|
+
across container boundaries. Orange = input port, teal = output port.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install bigraph-viz2
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
No graphviz. No node. No browser at install time. The JS bundle is
|
|
50
|
+
vendored inside the Python package; consumers need only Python ≥ 3.10.
|
|
51
|
+
|
|
52
|
+
## Use
|
|
53
|
+
|
|
54
|
+
The Python API has one main function and a Jupyter wrapper.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from bigraph_viz2 import emit_html
|
|
58
|
+
|
|
59
|
+
# one-shot: a self-contained HTML fragment you can paste anywhere
|
|
60
|
+
snippet = emit_html(composite_state, height="600px")
|
|
61
|
+
report_html = report_html.replace("{{BIGRAPH}}", snippet)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
In a Jupyter notebook:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from bigraph_viz2 import BigraphViz
|
|
68
|
+
BigraphViz(composite_state) # auto-displays via _repr_html_
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For pages with **multiple viz instances**, inline the bundle once and
|
|
72
|
+
dedupe the rest:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
snippets = [emit_html(specs[0], dedupe=False)]
|
|
76
|
+
for spec in specs[1:]:
|
|
77
|
+
snippets.append(emit_html(spec, dedupe=True))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### API
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
emit_html(state, *,
|
|
84
|
+
height="500px",
|
|
85
|
+
width="100%",
|
|
86
|
+
inspector=True, # show the right-side inspector panel
|
|
87
|
+
theme="light", # only "light" supported in v0.1
|
|
88
|
+
dedupe=False, # set True after the first call on a page
|
|
89
|
+
id=None, # explicit DOM id (auto-generated otherwise)
|
|
90
|
+
max_row_width=480 # auto-wrap threshold inside each container
|
|
91
|
+
) -> str
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Returns a self-contained HTML fragment.
|
|
95
|
+
|
|
96
|
+
### Interactions
|
|
97
|
+
|
|
98
|
+
| gesture | effect |
|
|
99
|
+
| ------------------------------------ | ------------------------------------------ |
|
|
100
|
+
| drag (anywhere) | pan |
|
|
101
|
+
| wheel | zoom centered on cursor (0.25× – 4×) |
|
|
102
|
+
| hover a port | tooltip with port name |
|
|
103
|
+
| click a node | populate the inspector |
|
|
104
|
+
| double-click a node | collapse / expand subtree |
|
|
105
|
+
| Alt + drag a node | reorder siblings; drop into row / new row |
|
|
106
|
+
| Esc (while dragging) | cancel the drag |
|
|
107
|
+
|
|
108
|
+
Collapse state persists in the URL hash and survives reload.
|
|
109
|
+
|
|
110
|
+
## Concepts
|
|
111
|
+
|
|
112
|
+
`bigraph-viz2` renders [compositional bigraph
|
|
113
|
+
schemas](https://github.com/vivarium-collective/process-bigraph) with
|
|
114
|
+
three primitive shapes:
|
|
115
|
+
|
|
116
|
+
| shape | meaning |
|
|
117
|
+
| ---------------------- | ------------------------------------------------------------- |
|
|
118
|
+
| **circle** | a *variable* — a leaf in the place graph |
|
|
119
|
+
| **rounded rectangle** | a *store* — a container nesting substores, processes, variables |
|
|
120
|
+
| **sharp rectangle** | a *process* — reads from / writes to its declared ports |
|
|
121
|
+
|
|
122
|
+
A composite is a tree of stores; processes live anywhere inside that
|
|
123
|
+
tree. Each process declares **ports**, which connect by **wire** to
|
|
124
|
+
variables — possibly across multiple container boundaries. Ports render
|
|
125
|
+
as small triangles on the edge of the process rectangle:
|
|
126
|
+
|
|
127
|
+
- **orange ▶** — input port (variable feeds the process)
|
|
128
|
+
- **teal ▶** — output port (process writes to the variable)
|
|
129
|
+
- **gray ·** — undirected (direction not declared in the spec)
|
|
130
|
+
|
|
131
|
+
Port direction is read from the spec's `inputs:` / `outputs:` blocks
|
|
132
|
+
(preferred), or from a single `ports:` block (treated as undirected for
|
|
133
|
+
back-compat with existing `bigraph-viz` specs).
|
|
134
|
+
|
|
135
|
+
### Example spec
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
spec = {
|
|
139
|
+
"name": "cell",
|
|
140
|
+
"stores": {
|
|
141
|
+
"membrane": {
|
|
142
|
+
"v": {"_type": "variable", "value": -70},
|
|
143
|
+
"channels": {"_type": "variable", "value": []},
|
|
144
|
+
},
|
|
145
|
+
"cytoplasm": {
|
|
146
|
+
"M": {"_type": "variable", "value": 1.0},
|
|
147
|
+
"ATP": {"_type": "variable", "value": 2.0},
|
|
148
|
+
"metabolism": {
|
|
149
|
+
"_type": "process",
|
|
150
|
+
"address": "fba.CobraStep",
|
|
151
|
+
"config": {"model": "iJO1366"},
|
|
152
|
+
"inputs": {"substrate": ["M"]},
|
|
153
|
+
"outputs": {"atp": ["ATP"]},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
"diffusion": {
|
|
157
|
+
"_type": "process",
|
|
158
|
+
"address": "ode.IonFlux",
|
|
159
|
+
"config": {},
|
|
160
|
+
"inputs": {"voltage": ["membrane", "v"]},
|
|
161
|
+
"outputs": {"channels": ["membrane", "channels"]},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
from bigraph_viz2 import emit_html
|
|
167
|
+
html = emit_html(spec, height="500px")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Wire paths are relative to the process's enclosing store (`["M"]` = the
|
|
171
|
+
sibling named `M`; `["..", "membrane", "v"]` = up to the enclosing store,
|
|
172
|
+
then into `membrane.v`).
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/vivarium-collective/bigraph-viz2
|
|
178
|
+
cd bigraph-viz2
|
|
179
|
+
|
|
180
|
+
# build the JS bundle and vendor it into py/
|
|
181
|
+
bash scripts/vendor.sh
|
|
182
|
+
|
|
183
|
+
# JS tests + typecheck
|
|
184
|
+
cd js && npm test && npm run typecheck
|
|
185
|
+
|
|
186
|
+
# JS end-to-end smoke (real browser)
|
|
187
|
+
cd js && npm run test:e2e
|
|
188
|
+
|
|
189
|
+
# Python tests (includes a Playwright round-trip)
|
|
190
|
+
cd py && pip install -e ".[test]" && pytest
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Status
|
|
194
|
+
|
|
195
|
+
v0.1 — initial release.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
Apache-2.0.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# bigraph-viz2
|
|
2
|
+
|
|
3
|
+
A lightweight, interactive, read-only renderer for
|
|
4
|
+
[process-bigraph](https://github.com/vivarium-collective/process-bigraph)
|
|
5
|
+
composites. Drop-in replacement for `bigraph-viz`'s static PNG output in
|
|
6
|
+
HTML reports — **no graphviz dependency**, pan / zoom / click-to-inspect /
|
|
7
|
+
double-click-to-collapse / Alt-drag-to-rearrange in the browser, and the
|
|
8
|
+
entire renderer inlines into a self-contained ~40 KB snippet.
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
The example above shows a two-cell tissue with nested substores (membrane,
|
|
13
|
+
cytoplasm, nucleus), processes living inside each, and wires reaching
|
|
14
|
+
across container boundaries. Orange = input port, teal = output port.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install bigraph-viz2
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No graphviz. No node. No browser at install time. The JS bundle is
|
|
23
|
+
vendored inside the Python package; consumers need only Python ≥ 3.10.
|
|
24
|
+
|
|
25
|
+
## Use
|
|
26
|
+
|
|
27
|
+
The Python API has one main function and a Jupyter wrapper.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from bigraph_viz2 import emit_html
|
|
31
|
+
|
|
32
|
+
# one-shot: a self-contained HTML fragment you can paste anywhere
|
|
33
|
+
snippet = emit_html(composite_state, height="600px")
|
|
34
|
+
report_html = report_html.replace("{{BIGRAPH}}", snippet)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
In a Jupyter notebook:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from bigraph_viz2 import BigraphViz
|
|
41
|
+
BigraphViz(composite_state) # auto-displays via _repr_html_
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For pages with **multiple viz instances**, inline the bundle once and
|
|
45
|
+
dedupe the rest:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
snippets = [emit_html(specs[0], dedupe=False)]
|
|
49
|
+
for spec in specs[1:]:
|
|
50
|
+
snippets.append(emit_html(spec, dedupe=True))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### API
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
emit_html(state, *,
|
|
57
|
+
height="500px",
|
|
58
|
+
width="100%",
|
|
59
|
+
inspector=True, # show the right-side inspector panel
|
|
60
|
+
theme="light", # only "light" supported in v0.1
|
|
61
|
+
dedupe=False, # set True after the first call on a page
|
|
62
|
+
id=None, # explicit DOM id (auto-generated otherwise)
|
|
63
|
+
max_row_width=480 # auto-wrap threshold inside each container
|
|
64
|
+
) -> str
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Returns a self-contained HTML fragment.
|
|
68
|
+
|
|
69
|
+
### Interactions
|
|
70
|
+
|
|
71
|
+
| gesture | effect |
|
|
72
|
+
| ------------------------------------ | ------------------------------------------ |
|
|
73
|
+
| drag (anywhere) | pan |
|
|
74
|
+
| wheel | zoom centered on cursor (0.25× – 4×) |
|
|
75
|
+
| hover a port | tooltip with port name |
|
|
76
|
+
| click a node | populate the inspector |
|
|
77
|
+
| double-click a node | collapse / expand subtree |
|
|
78
|
+
| Alt + drag a node | reorder siblings; drop into row / new row |
|
|
79
|
+
| Esc (while dragging) | cancel the drag |
|
|
80
|
+
|
|
81
|
+
Collapse state persists in the URL hash and survives reload.
|
|
82
|
+
|
|
83
|
+
## Concepts
|
|
84
|
+
|
|
85
|
+
`bigraph-viz2` renders [compositional bigraph
|
|
86
|
+
schemas](https://github.com/vivarium-collective/process-bigraph) with
|
|
87
|
+
three primitive shapes:
|
|
88
|
+
|
|
89
|
+
| shape | meaning |
|
|
90
|
+
| ---------------------- | ------------------------------------------------------------- |
|
|
91
|
+
| **circle** | a *variable* — a leaf in the place graph |
|
|
92
|
+
| **rounded rectangle** | a *store* — a container nesting substores, processes, variables |
|
|
93
|
+
| **sharp rectangle** | a *process* — reads from / writes to its declared ports |
|
|
94
|
+
|
|
95
|
+
A composite is a tree of stores; processes live anywhere inside that
|
|
96
|
+
tree. Each process declares **ports**, which connect by **wire** to
|
|
97
|
+
variables — possibly across multiple container boundaries. Ports render
|
|
98
|
+
as small triangles on the edge of the process rectangle:
|
|
99
|
+
|
|
100
|
+
- **orange ▶** — input port (variable feeds the process)
|
|
101
|
+
- **teal ▶** — output port (process writes to the variable)
|
|
102
|
+
- **gray ·** — undirected (direction not declared in the spec)
|
|
103
|
+
|
|
104
|
+
Port direction is read from the spec's `inputs:` / `outputs:` blocks
|
|
105
|
+
(preferred), or from a single `ports:` block (treated as undirected for
|
|
106
|
+
back-compat with existing `bigraph-viz` specs).
|
|
107
|
+
|
|
108
|
+
### Example spec
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
spec = {
|
|
112
|
+
"name": "cell",
|
|
113
|
+
"stores": {
|
|
114
|
+
"membrane": {
|
|
115
|
+
"v": {"_type": "variable", "value": -70},
|
|
116
|
+
"channels": {"_type": "variable", "value": []},
|
|
117
|
+
},
|
|
118
|
+
"cytoplasm": {
|
|
119
|
+
"M": {"_type": "variable", "value": 1.0},
|
|
120
|
+
"ATP": {"_type": "variable", "value": 2.0},
|
|
121
|
+
"metabolism": {
|
|
122
|
+
"_type": "process",
|
|
123
|
+
"address": "fba.CobraStep",
|
|
124
|
+
"config": {"model": "iJO1366"},
|
|
125
|
+
"inputs": {"substrate": ["M"]},
|
|
126
|
+
"outputs": {"atp": ["ATP"]},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
"diffusion": {
|
|
130
|
+
"_type": "process",
|
|
131
|
+
"address": "ode.IonFlux",
|
|
132
|
+
"config": {},
|
|
133
|
+
"inputs": {"voltage": ["membrane", "v"]},
|
|
134
|
+
"outputs": {"channels": ["membrane", "channels"]},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
from bigraph_viz2 import emit_html
|
|
140
|
+
html = emit_html(spec, height="500px")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Wire paths are relative to the process's enclosing store (`["M"]` = the
|
|
144
|
+
sibling named `M`; `["..", "membrane", "v"]` = up to the enclosing store,
|
|
145
|
+
then into `membrane.v`).
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git clone https://github.com/vivarium-collective/bigraph-viz2
|
|
151
|
+
cd bigraph-viz2
|
|
152
|
+
|
|
153
|
+
# build the JS bundle and vendor it into py/
|
|
154
|
+
bash scripts/vendor.sh
|
|
155
|
+
|
|
156
|
+
# JS tests + typecheck
|
|
157
|
+
cd js && npm test && npm run typecheck
|
|
158
|
+
|
|
159
|
+
# JS end-to-end smoke (real browser)
|
|
160
|
+
cd js && npm run test:e2e
|
|
161
|
+
|
|
162
|
+
# Python tests (includes a Playwright round-trip)
|
|
163
|
+
cd py && pip install -e ".[test]" && pytest
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Status
|
|
167
|
+
|
|
168
|
+
v0.1 — initial release.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
Apache-2.0.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""bigraph-viz2: lightweight read-only bigraph renderer.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
emit_html(state, **opts) -> str
|
|
5
|
+
BigraphViz(state, **opts) # Jupyter wrapper
|
|
6
|
+
"""
|
|
7
|
+
from .emit import emit_html
|
|
8
|
+
from .jupyter import BigraphViz
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["emit_html", "BigraphViz", "__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vendored JS bundle (built by scripts/vendor.sh)."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.bgv2{--bgv2-store-bg: #ffffff;--bgv2-store-stroke: #cbd5e1;--bgv2-process-bg: #f1f5f9;--bgv2-process-stroke: #475569;--bgv2-variable-bg: #ffffff;--bgv2-variable-stroke: #0284c7;--bgv2-wire-stroke: #94a3b8;--bgv2-selected: #2563eb;--bgv2-inspector-bg: #ffffff;--bgv2-font: system-ui, -apple-system, sans-serif;--bgv2-font-mono: ui-monospace, "SF Mono", Menlo, monospace;display:flex;background:#fafafa;font-family:var(--bgv2-font)}.bgv2-canvas{flex:1;position:relative;overflow:hidden}.bgv2-inspector{width:240px;padding:14px 16px;background:var(--bgv2-inspector-bg);border-left:1px solid #e5e7eb;font-size:12px}.bgv2-svg{width:100%;height:100%;display:block}.bgv2-store{fill:var(--bgv2-store-bg);stroke:var(--bgv2-store-stroke);stroke-width:1.5}.bgv2-proc{fill:var(--bgv2-process-bg);stroke:var(--bgv2-process-stroke);stroke-width:1.5}.bgv2-var{fill:var(--bgv2-variable-bg);stroke:var(--bgv2-variable-stroke);stroke-width:1.5}.bgv2-chip{fill:#fef3c7;stroke:#fbbf24;stroke-width:1.5}.bgv2-wire{stroke:var(--bgv2-wire-stroke);stroke-width:1.3;fill:none}.bgv2-port{fill:var(--bgv2-process-stroke)}.bgv2-var-icon,.bgv2-proc-name,.bgv2-store-label,.bgv2-chip-name{font-size:12px;fill:#0f172a;font-weight:600}.bgv2-var-label,.bgv2-proc-addr,.bgv2-chip-badge{font-size:10px;fill:#64748b;font-family:var(--bgv2-font-mono)}.bgv2-node.bgv2-selected .bgv2-proc,.bgv2-node.bgv2-selected .bgv2-store,.bgv2-node.bgv2-selected .bgv2-var{stroke:var(--bgv2-selected);stroke-width:2}.bgv2-insp-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#64748b;margin:12px 0 4px}.bgv2-insp-name{font-size:14px;font-weight:600;color:#0f172a}.bgv2-insp-p{font-size:12px;color:#475569;margin:2px 0}.bgv2-insp-p.mono{font-family:var(--bgv2-font-mono)}.bgv2-insp-p.small{font-size:10px}.bgv2-insp-p.muted{color:#94a3b8}.bgv2-insp-pre{background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:6px 8px;font-family:var(--bgv2-font-mono);font-size:11px;color:#1e293b;margin:0;overflow-x:auto}.bgv2-portmap{width:100%;font-size:11px;border-collapse:collapse}.bgv2-portmap td{padding:3px 0}.bgv2-portmap td.mono{font-family:var(--bgv2-font-mono);color:#0f172a}.bgv2-drag-ghost{filter:drop-shadow(0 4px 12px rgba(37,99,235,.35))}.bgv2-drag-slot{fill:#2563eb;opacity:.55;pointer-events:none}.bgv2-svg{isolation:isolate;will-change:transform}.bgv2-svg.bgv2-alt-mode .bgv2-node{cursor:grab}.bgv2-port-in{fill:#ea580c}.bgv2-port-out{fill:#0d9488}.bgv2-port-both{fill:var(--bgv2-process-stroke)}.bgv2-wire-in{stroke:#ea580c}.bgv2-wire-out{stroke:#0d9488}.bgv2-port-label{font-size:8.5px;font-family:var(--bgv2-font-mono);fill:#94a3b8;opacity:.85;pointer-events:none}.bgv2-port-label-in{fill:#c2410c}.bgv2-port-label-out{fill:#0f766e}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var BigraphViz=function(t){"use strict";const e=new Set(["_type","address","config","ports","inputs","outputs","type","value"]),n=new Set(["name","stores"]);function o(t,n,i,s){if("object"!=typeof n||null===n)return{id:s,name:t,kind:"variable",children:[],value:n};const c=n,d=c._type??i;if("variable"===d){const e="value"in c?c.value:c;return{id:s,name:t,kind:"variable",children:[],type:c.type,value:e}}if("process"===d){const e={},n={};for(const[t,o]of Object.entries(c.ports??{}))e[t]=o,n[t]="both";for(const[t,o]of Object.entries(c.inputs??{}))e[t]=o,n[t]="in";for(const[t,o]of Object.entries(c.outputs??{}))e[t]=o,n[t]="out";return{id:s,name:t,kind:"process",children:[],address:c.address,config:c.config,ports:e,portDirections:n}}const a=Object.entries(c).filter(([t])=>!e.has(t)).map(([t,n])=>{const r=function(t){if("object"!=typeof t||null===t)return"variable";const n=t._type;if("process"===n||"variable"===n||"store"===n)return n;const o=t,r=Object.keys(o).filter(t=>!e.has(t));return 0===r.length?"variable":"store"}(n);return o(t,n,r,`${s}/${t}`)});return a.sort((t,e)=>r(t.kind)-r(e.kind)),{id:s,name:t,kind:"store",children:a}}function r(t){return"store"===t?0:"process"===t?1:2}function i(t,e){const n=null==e?void 0:e.get(t.id);if(!n)return{rows:[t.children],explicit:!1};const o=new Map(t.children.map(t=>[t.id,t])),r=new Set,i=[];for(const c of n){const t=[];for(const e of c){const n=o.get(e);n&&(t.push(n),r.add(e))}t.length>0&&i.push(t)}const s=t.children.filter(t=>!r.has(t.id));return s.length>0&&i.push(s),{rows:i,explicit:!0}}function s(t,e,n,o){const r=new Map;return c(t,e,n,r,o),r}function c(t,e,n,o,r){if("variable"===t.kind){const e={w:56,h:46};return o.set(t.id,e),e}if("process"===t.kind){const e={w:180,h:52};return o.set(t.id,e),e}if(e.has(t.id)){const e={w:140,h:56};return o.set(t.id,e),e}for(const i of t.children)c(i,e,n,o,r);const{rows:s,explicit:l}=i(t,r);let u=0,p=0;if(l)for(const i of s){const t=d(i,o),e=a(i,o);u=Math.max(u,t),p+=(p>0?20:0)+e}else{let e=0,r=0;for(const i of t.children){const t=o.get(i.id),s=0===e?t.w:e+16+t.w;e>0&&s>n?(u=Math.max(u,e),p+=(p>0?20:0)+r,e=t.w,r=t.h):(e=s,r=Math.max(r,t.h))}e>0&&(u=Math.max(u,e),p+=(p>0?20:0)+r)}const f={w:u+48,h:p+30+48};return o.set(t.id,f),f}function d(t,e){if(0===t.length)return 0;let n=0;for(let o=0;o<t.length;o++)n+=e.get(t[o].id).w,o>0&&(n+=16);return n}function a(t,e){let n=0;for(const o of t)n=Math.max(n,e.get(o.id).h);return n}function l(t,e,n,o,r){const i=new Map,s=e.get(t.id);return u(t,0,0,s.w,s.h,e,n,o,i,r),i}function u(t,e,n,o,r,s,c,d,a,l){const p={x:e,y:n,w:o,h:r};if(a.set(t.id,{node:t,bbox:p,collapsed:c.has(t.id)}),"store"!==t.kind||c.has(t.id))return;const f=e+24,b=n+30+24,{rows:h,explicit:g}=i(t,l);if(g){let t=b;for(const e of h){let n=f,o=0;for(const r of e){const e=s.get(r.id);u(r,n,t,e.w,e.h,s,c,d,a,l),n+=e.w+16,o=Math.max(o,e.h)}t+=o+20}return}let m=f,x=b,v=0;for(const i of t.children){const t=s.get(i.id);m>f&&m+t.w>e+o-24&&(m=f,x+=v+20,v=0),u(i,m,x,t.w,t.h,s,c,d,a,l),m+=t.w+16,v=Math.max(v,t.h)}}function p(t,e){if(t.id===e)return t;for(const n of t.children){const t=p(n,e);if(t)return t}return null}function f(t){const e=t.lastIndexOf("/");return e<0?null:t.slice(0,e)}function b(t,e,n){const o=f(e.id);if(null===o)return null;let r=o;for(const i of n){if(null===r)return null;if(".."===i)r=f(r);else if("."===i);else{const e=`${r}/${i}`;if(!p(t,e))return null;r=e}}return r}function h(t,e,n){const o=e-(t.x+t.w/2),r=n-(t.y+t.h/2);return Math.abs(o)>Math.abs(r)?o>0?"right":"left":r>0?"bottom":"top"}function g(t,e,n,o){const{x:r,y:i,w:s,h:c}=t,d=(n+1)/(o+1);return"top"===e?{x:r+s*d,y:i}:"bottom"===e?{x:r+s*d,y:i+c}:"left"===e?{x:r,y:i+c*d}:{x:r+s,y:i+c*d}}function m(t,e,n){const o=[];return x(t,r=>{var i;const s=Object.entries(r.ports??{}),c=e.get(r.id);if(!c)return;const d=c.bbox,a=[];for(const[o,u]of s){const i=b(t,r,u);if(!i)continue;const{id:s,retargeted:c}=v(i,n),d=e.get(s);if(!d)continue;const l=d.bbox.x+d.bbox.w/2,p=d.bbox.y+d.bbox.h/2;a.push({portName:o,targetId:s,retargeted:c,tcx:l,tcy:p})}const l={top:[],bottom:[],left:[],right:[]};for(const t of a)l[h(d,t.tcx,t.tcy)].push(t);for(const t of["top","bottom","left","right"])for(const e of l[t])o.push({processId:r.id,portName:e.portName,targetId:e.targetId,retargetedToChip:e.retargeted,direction:(null==(i=r.portDirections)?void 0:i[e.portName])??"both"})}),o}function x(t,e){"process"===t.kind&&e(t);for(const n of t.children)x(n,e)}function v(t,e){let n=t;for(;;){const o=n.lastIndexOf("/");if(o<0)return{id:t,retargeted:!1};const r=n.slice(0,o);if(e.has(r))return{id:r,retargeted:r!==t};n=r}}const w="http://www.w3.org/2000/svg";function y(t,e,n,o,r,i){const s=document.createElementNS(w,"text");s.textContent=r,s.classList.add("bgv2-port-label",`bgv2-port-label-${i}`);"top"===n?(s.setAttribute("x",String(e.x)),s.setAttribute("y",String(o.y+6+6)),s.setAttribute("text-anchor","middle")):"bottom"===n?(s.setAttribute("x",String(e.x)),s.setAttribute("y",String(o.y+o.h-6)),s.setAttribute("text-anchor","middle")):"left"===n?(s.setAttribute("x",String(o.x+6)),s.setAttribute("y",String(e.y+3)),s.setAttribute("text-anchor","start")):(s.setAttribute("x",String(o.x+o.w-6)),s.setAttribute("y",String(e.y+3)),s.setAttribute("text-anchor","end")),t.appendChild(s)}function E(t,e,n,o,r){const i=o.x-e.x,s=o.y-e.y,c=Math.hypot(i,s),d=Math.max(28,Math.min(140,.45*c)),a=S(n),l=S(o.approachEdge),u=e.x+a.dx*d,p=e.y+a.dy*d,f=o.x+l.dx*d,b=o.y+l.dy*d,h=document.createElementNS(w,"path");h.setAttribute("d",`M ${e.x} ${e.y} C ${u} ${p} ${f} ${b} ${o.x} ${o.y}`),h.setAttribute("stroke-dasharray","4,3"),h.classList.add("bgv2-wire",`bgv2-wire-${r.direction}`),r.retargetedToChip&&h.classList.add("bgv2-wire-retargeted"),t.appendChild(h);const g=function(t,e,n){if("both"===n){const e=document.createElementNS(w,"circle");return e.setAttribute("cx",String(t.x)),e.setAttribute("cy",String(t.y)),e.setAttribute("r","3"),e.classList.add("bgv2-port","bgv2-port-both"),e}const o="in"===n,r=S(e),i=o?-r.dx:r.dx,s=o?-r.dy:r.dy,c=5,d=t.x+i*c,a=t.y+s*c,l=-s,u=i,p=t.x+l*(.7*c),f=t.y+u*(.7*c),b=t.x-l*(.7*c),h=t.y-u*(.7*c),g=document.createElementNS(w,"polygon");return g.setAttribute("points",`${d},${a} ${p},${f} ${b},${h}`),g.classList.add("bgv2-port",`bgv2-port-${n}`),g}(e,n,r.direction),m=document.createElementNS(w,"title");m.textContent=r.portName,g.appendChild(m),t.appendChild(g)}function A(t,e){const n=t.bbox;if("variable"===t.node.kind&&!t.collapsed){const t=n.x+n.w/2,o=n.y+n.h/2+-6,r=t-e.x,i=o-e.y,s=Math.hypot(r,i)||1;return{x:t-r/s*16,y:o-i/s*16,approachEdge:Math.abs(r)>Math.abs(i)?r>0?"left":"right":i>0?"top":"bottom"}}const o=h(n,e.x,e.y),r=g(n,o,0,1);return{x:r.x,y:r.y,approachEdge:o}}function S(t){switch(t){case"top":return{dx:0,dy:-1};case"bottom":return{dx:0,dy:1};case"left":return{dx:-1,dy:0};case"right":return{dx:1,dy:0}}}const L="http://www.w3.org/2000/svg";function C(t){const e=document.createElementNS(L,"svg"),{w:n,h:o}=t.root.bbox;e.setAttribute("viewBox",`0 0 ${n} ${o}`),e.setAttribute("xmlns",L),e.classList.add("bgv2-svg");const r=document.createElementNS(L,"g");r.classList.add("bgv2-root"),e.appendChild(r);const i=document.createElementNS(L,"g"),s=document.createElementNS(L,"g");r.appendChild(i),r.appendChild(s);for(const c of t.byId.values())$(i,c);return function(t,e,n){const o=new Map;for(const r of e){let t=o.get(r.processId);t||(t=[],o.set(r.processId,t)),t.push(r)}for(const[r,i]of o){const e=n.get(r);if(!e)continue;const o=e.bbox,s=i.map(t=>{const e=n.get(t.targetId),r=e.bbox.x+e.bbox.w/2,i=e.bbox.y+e.bbox.h/2,s=h(o,r,i);return{w:t,target:e,portEdge:s,endpoint:A(e,g(o,s,0,1))}}),c={top:[],bottom:[],left:[],right:[]};for(const t of s)c[t.portEdge].push(t);for(const n of["top","bottom","left","right"]){const e=c[n];0!==e.length&&("top"===n||"bottom"===n?e.sort((t,e)=>t.endpoint.x-e.endpoint.x):e.sort((t,e)=>t.endpoint.y-e.endpoint.y),e.forEach((r,i)=>{const s=g(o,n,i,e.length);E(t,s,n,r.endpoint,r.w),y(t,s,n,o,r.w.portName,r.w.direction)}))}}}(s,t.wires,t.byId),e}function $(t,e){const{node:n,bbox:o,collapsed:r}=e,i=document.createElementNS(L,"g");if(i.setAttribute("data-bgv2-id",n.id),i.classList.add("bgv2-node",`bgv2-node-${n.kind}`),t.appendChild(i),"variable"===n.kind){const t=o.x+o.w/2,e=o.y+o.h/2-6,r=document.createElementNS(L,"circle");return r.setAttribute("cx",String(t)),r.setAttribute("cy",String(e)),r.setAttribute("r","16"),r.classList.add("bgv2-var"),i.appendChild(r),I(i,t,e+4,n.name.slice(0,2),"bgv2-var-icon"),void I(i,t,e+28,n.name,"bgv2-var-label")}if("process"===n.kind){const t=document.createElementNS(L,"rect");return t.setAttribute("x",String(o.x)),t.setAttribute("y",String(o.y)),t.setAttribute("width",String(o.w)),t.setAttribute("height",String(o.h)),t.classList.add("bgv2-proc"),i.appendChild(t),I(i,o.x+o.w/2,o.y+22,n.name,"bgv2-proc-name"),void(n.address&&I(i,o.x+o.w/2,o.y+40,n.address,"bgv2-proc-addr"))}if(r){const t=document.createElementNS(L,"rect");t.setAttribute("x",String(o.x)),t.setAttribute("y",String(o.y)),t.setAttribute("width",String(o.w)),t.setAttribute("height",String(o.h)),t.setAttribute("rx","28"),t.setAttribute("stroke-dasharray","6,3"),t.classList.add("bgv2-chip"),i.appendChild(t),I(i,o.x+o.w/2,o.y+24,n.name,"bgv2-chip-name");const e=k(n);return void I(i,o.x+o.w/2,o.y+42,`▸ ${e} hidden`,"bgv2-chip-badge")}const s=document.createElementNS(L,"rect");s.setAttribute("x",String(o.x)),s.setAttribute("y",String(o.y)),s.setAttribute("width",String(o.w)),s.setAttribute("height",String(o.h)),s.setAttribute("rx","12"),s.classList.add("bgv2-store"),i.appendChild(s),I(i,o.x+16,o.y+22,n.name,"bgv2-store-label","start")}function I(t,e,n,o,r,i="middle"){const s=document.createElementNS(L,"text");return s.setAttribute("x",String(e)),s.setAttribute("y",String(n)),s.setAttribute("text-anchor",i),s.classList.add(r),s.textContent=o,t.appendChild(s),s}function k(t){let e=0;for(const n of t.children)e+=1+k(n);return e}function N(t){return t.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""}[t]))}function M(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function O(t,e){const n=function(t,e){return 0===e.size?`#bgv2-${t}=collapse:`:`#bgv2-${t}=collapse:${Array.from(e).sort().join(",")}`}(t,e).replace(/^#/,""),o="#"+[...(window.location.hash.replace(/^#/,"").split("&")??[]).filter(e=>e&&!e.startsWith(`bgv2-${t}=`)),n].filter(Boolean).join("&");history.replaceState(null,"",o)}const R="http://www.w3.org/2000/svg";function j(t,e,n,o){let r=!1,i=0,s=null,c=null,d=null,a=null,l=null,u=[],p=0,f=0,b=0,h={x:0,y:0};function g(o){if(0!==o.button||!o.altKey)return;const i=o.target.closest(".bgv2-node");if(!i)return;const g=i.getAttribute("data-bgv2-id");g&&function(t){return t.includes("/")}(g)&&(o.preventDefault(),f=o.clientX,b=o.clientY,function(o,i,g){const m=e.byId.get(o);if(!m)return;r=!0,s=o,c=function(t){const e=t.lastIndexOf("/");return e<0?null:t.slice(0,e)}(o),d=i,i.setAttribute("opacity","0.25"),a=i.cloneNode(!0),a.classList.add("bgv2-drag-ghost"),a.setAttribute("opacity","0.75"),a.setAttribute("pointer-events","none"),i.parentNode.appendChild(a);const x=Y(t,f,b);h=x;const v=Y(t,g.clientX,g.clientY);a.setAttribute("transform",`translate(${v.x-x.x},${v.y-x.y})`),u=function(t,e,n,o){const r=T(t,e,o),i=function(t,e){for(let n=0;n<t.length;n++){const o=t[n].indexOf(e);if(o>=0){return{rowIndex:n,posInRow:o,newRow:1===t[n].length}}}return{rowIndex:0,posInRow:0,newRow:!1}}(r,n),s=r.map(t=>t.filter(t=>t!==n)).filter(t=>t.length>0),c=[],d=s.map(e=>function(t,e){let n=1/0,o=1/0,r=-1/0,i=-1/0;for(const s of t){const t=e.byId.get(s).bbox;n=Math.min(n,t.x),o=Math.min(o,t.y),r=Math.max(r,t.x+t.w),i=Math.max(i,t.y+t.h)}return{x:n,y:o,w:r-n,h:i-o}}(e,t));if(0===d.length)return c;{const t=d[0];c.push({x:t.x,y:t.y-16,w:t.w,h:8,rowIndex:0,posInRow:0,newRow:!0,isOriginal:i.newRow&&0===i.rowIndex})}for(let a=0;a<s.length;a++){const e=s[a],n=d[a];for(let s=0;s<=e.length;s++){let n,o,r;if(0===s){const i=t.byId.get(e[0]);n=i.bbox.x-14,o=i.bbox.y,r=i.bbox.h}else if(s===e.length){const i=t.byId.get(e[e.length-1]);n=i.bbox.x+i.bbox.w+6,o=i.bbox.y,r=i.bbox.h}else{const i=t.byId.get(e[s-1]);n=i.bbox.x+i.bbox.w+4,o=i.bbox.y,r=i.bbox.h}c.push({x:n,y:o,w:8,h:r,rowIndex:a,posInRow:s,newRow:!1,isOriginal:!i.newRow&&i.rowIndex===a&&i.posInRow===s})}const o=d[a+1],r=o?(n.y+n.h+o.y)/2-4:n.y+n.h+8;c.push({x:n.x,y:r,w:n.w,h:8,rowIndex:a+1,posInRow:0,newRow:!0,isOriginal:i.newRow&&i.rowIndex===a+1})}return c}(e,c,o,n),p=u.findIndex(t=>t.isOriginal),p<0&&(p=0);l=document.createElementNS(R,"rect"),l.classList.add("bgv2-drag-slot"),l.setAttribute("rx","3"),i.parentNode.appendChild(l),w(),document.body.style.cursor="grabbing"}(g,i,o))}function m(e){if(!r||!a)return;const n=Y(t,e.clientX,e.clientY);a.setAttribute("transform",`translate(${n.x-h.x},${n.y-h.y})`),function(t,e){let n=0,o=1/0;for(let r=0;r<u.length;r++){const i=u[r],s=i.x+i.w/2,c=i.y+i.h/2,d=Math.hypot(s-t,c-e);d<o&&(o=d,n=r)}n!==p&&(p=n,w())}(n.x,n.y)}function x(){r&&function(){var t;if(!s||!c)return void y();const r=null==(t=e.byId.get(c))?void 0:t.node,i=u[p];if(r&&i&&!i.isOriginal){const t=T(e,r.id,n);for(const e of t){const t=e.indexOf(s);t>=0&&e.splice(t,1)}const c=t.filter(t=>t.length>0);if(i.newRow)c.splice(i.rowIndex,0,[s]);else{const t=c[i.rowIndex]??[],e=Math.max(0,Math.min(i.posInRow,t.length));c[i.rowIndex]?c[i.rowIndex].splice(e,0,s):c.push([s])}const d=new Map(n);d.set(r.id,c),o(d)}y()}()}function v(t){"Escape"===t.key&&r&&y()}function w(){if(!l)return;const t=u[p];t&&(l.setAttribute("x",String(t.x)),l.setAttribute("y",String(t.y)),l.setAttribute("width",String(t.w)),l.setAttribute("height",String(t.h)))}function y(){const t=r;r=!1,d&&(d.setAttribute("opacity",""),d=null),a&&a.parentNode&&a.parentNode.removeChild(a),a=null,l&&l.parentNode&&l.parentNode.removeChild(l),l=null,s=null,c=null,u=[],document.body.style.cursor="",t&&(i=performance.now()+100)}function E(t){(r||performance.now()<i)&&(t.stopImmediatePropagation(),t.preventDefault())}return t.addEventListener("mousedown",g),window.addEventListener("mousemove",m),window.addEventListener("mouseup",x),window.addEventListener("keydown",v),t.addEventListener("click",E,!0),{isActive:()=>r||performance.now()<i,detach:()=>{y(),t.removeEventListener("mousedown",g),window.removeEventListener("mousemove",m),window.removeEventListener("mouseup",x),window.removeEventListener("keydown",v),t.removeEventListener("click",E,!0)}}}function T(t,e,n){var o;const r=n.get(e);if(r)return r.map(t=>[...t]);const i=null==(o=t.byId.get(e))?void 0:o.node;return i?function(t){const e=[];for(const n of t){const t=e.find(t=>Math.abs(t[0].bbox.y-n.bbox.y)<4);t?t.push(n):e.push([n])}return e.forEach(t=>t.sort((t,e)=>t.bbox.x-e.bbox.x)),e.sort((t,e)=>t[0].bbox.y-e[0].bbox.y),e.map(t=>t.map(t=>t.node.id))}(i.children.map(e=>t.byId.get(e.id))):[]}function Y(t,e,n){const o=t.createSVGPoint();o.x=e,o.y=n;const r=t.getScreenCTM();if(!r)return{x:e,y:n};const i=o.matrixTransform(r.inverse());return{x:i.x,y:i.y}}function X(t,e,n){if(t.innerHTML="",!n)return t.appendChild(D("Inspector")),void t.appendChild(P("Nothing selected.","muted"));const o=e.byId.get(n);if(!o)return;const r=o.node;if(t.appendChild(D(`Inspector · ${r.kind}`)),t.appendChild(function(t){const e=document.createElement("div");return e.className="bgv2-insp-name",e.textContent=t,e}(r.name)),"process"===r.kind){t.appendChild(P(r.address??"(no address)","mono")),t.appendChild(P(`at: ${H(n)}`,"mono small")),t.appendChild(D("Config")),t.appendChild(B(JSON.stringify(r.config??{},null,2))),t.appendChild(D("Ports → wires"));const e=document.createElement("table");e.className="bgv2-portmap";for(const[t,n]of Object.entries(r.ports??{})){const o=document.createElement("tr"),r=document.createElement("td");r.textContent=t;const i=document.createElement("td");i.textContent=n.join("/"),i.className="mono",o.appendChild(r),o.appendChild(i),e.appendChild(o)}t.appendChild(e)}else"variable"===r.kind?(t.appendChild(P(r.type??"(no declared type)","mono")),t.appendChild(P(`at: ${H(n)}`,"mono small")),t.appendChild(D("Value")),t.appendChild(B(JSON.stringify(r.value,null,2)))):(t.appendChild(P(`at: ${H(n)}`,"mono small")),t.appendChild(P(`${r.children.length} children`,"muted")))}function D(t){const e=document.createElement("div");return e.className="bgv2-insp-label",e.textContent=t,e}function P(t,e=""){const n=document.createElement("div");return n.className=`bgv2-insp-p ${e}`,n.textContent=t,n}function B(t){const e=document.createElement("pre");return e.className="bgv2-insp-pre",e.textContent=t,e}function H(t){const e=t.lastIndexOf("/");return e<0?"(root)":t.slice(0,e)}const W=new WeakMap;let q=0;function z(t){const e=W.get(t);e&&(e.detachers.forEach(t=>t()),W.delete(t)),t.innerHTML="",t.classList.remove("bgv2")}return t.mount=function(t,e,r={}){z(t),t.classList.add("bgv2");const i=r.id??"auto-"+ ++q,c=!1!==r.inspector,d=r.maxRowWidth??480,a=document.createElement("div");a.className="bgv2-canvas",t.appendChild(a);const u=c?document.createElement("div"):null;u&&(u.className="bgv2-inspector",t.appendChild(u));const p=function(t){const e=t.name??"root";if(void 0!==t.stores)return o(e,t.stores,"store",e);const r={};for(const[o,i]of Object.entries(t))n.has(o)||(r[o]=i);return o(e,r,"store",e)}(e);let f=function(t,e){const n=t.replace(/^#/,"");for(const o of n.split("&")){const t=o.match(new RegExp(`^bgv2-${M(e)}=collapse:(.*)$`));if(t)return t[1]?new Set(t[1].split(",")):new Set}return new Set}(window.location.hash,i),b=new Map;const h=[];!function t(){h.forEach(t=>t()),h.length=0,a.innerHTML="";const e=function(t,e,n,o){const r=l(t,s(t,e,n,o),e,n,o),i=m(t,r,e);return{byId:r,root:r.get(t.id),wires:i}}(p,f,d,b),n=C(e);a.appendChild(n);const o=j(n,e,b,e=>{b=e,t()});h.push(o.detach),h.push(function(t,e,n={}){let o=0,r=0,i=1,s=!1,c=!1,d=0,a=0,l=0,u=0;function p(){e.setAttribute("transform",`translate(${o},${r}) scale(${i})`)}function f(e){e.preventDefault();const n=e.deltaY<0?1.1:1/1.1,s=Math.max(.25,Math.min(4,i*n)),c=t.getBoundingClientRect(),d=e.clientX-c.left,a=e.clientY-c.top;o=d-s/i*(d-o),r=a-s/i*(a-r),i=s,p()}function b(t){0===t.button&&(n.isLocked&&n.isLocked()||(s=!0,c=!1,d=t.clientX,a=t.clientY,l=o,u=r))}function h(e){if(!s)return;if(n.isLocked&&n.isLocked())return s=!1,c=!1,void(t.style.cursor="grab");const i=e.clientX-d,f=e.clientY-a;!c&&Math.hypot(i,f)>=4&&(c=!0,t.style.cursor="grabbing"),c&&(o=l+i,r=u+f,p())}function g(){s&&(s=!1,t.style.cursor="grab")}function m(t){c&&(t.stopImmediatePropagation(),t.preventDefault(),c=!1)}return p(),t.style.cursor="grab",t.addEventListener("wheel",f,{passive:!1}),t.addEventListener("mousedown",b),window.addEventListener("mousemove",h),window.addEventListener("mouseup",g),t.addEventListener("click",m,!0),()=>{t.removeEventListener("wheel",f),t.removeEventListener("mousedown",b),window.removeEventListener("mousemove",h),window.removeEventListener("mouseup",g),t.removeEventListener("click",m,!0),t.style.cursor=""}}(n,n.querySelector(".bgv2-root"),{isLocked:o.isActive})),h.push(function(t,e){const n=document.createElement("div");function o(t){const o=t.target.closest(".bgv2-node");if(!o)return void(n.style.display="none");const r=o.getAttribute("data-bgv2-id");if(!r)return;const i=e.byId.get(r);if(!i)return;const s=i.node.name,c="process"===i.node.kind?i.node.address??"":"variable"===i.node.kind?i.node.type??"variable":`store · ${i.node.children.length} children`;n.innerHTML=`<div>${N(s)}</div><div style="opacity:.7">${N(c)}</div>`,n.style.left=`${t.clientX+12}px`,n.style.top=`${t.clientY+12}px`,n.style.display="block"}function r(){n.style.display="none"}return n.className="bgv2-tooltip",n.style.cssText="position:fixed;pointer-events:none;background:#0f172a;color:#fff;padding:4px 8px;border-radius:3px;font:11px/1.3 ui-monospace,monospace;display:none;z-index:9999;",document.body.appendChild(n),t.addEventListener("mousemove",o),t.addEventListener("mouseleave",r),()=>{t.removeEventListener("mousemove",o),t.removeEventListener("mouseleave",r),n.remove()}}(n,e)),h.push(function(t,e,n,o={}){function r(e){if(o.isLocked&&o.isLocked())return;const r=e.target.closest(".bgv2-node");if(t.querySelectorAll(".bgv2-selected").forEach(t=>t.classList.remove("bgv2-selected")),!r)return void n(null);const i=r.getAttribute("data-bgv2-id");i&&(r.classList.add("bgv2-selected"),n(i))}return n(null),t.addEventListener("click",r),()=>t.removeEventListener("click",r)}(n,0,t=>{u&&X(u,e,t)},{isLocked:o.isActive})),h.push(function(t,e,n,o,r){function i(t){var e;const i=t.target.closest(".bgv2-node");if(!i)return;const s=i.getAttribute("data-bgv2-id");if(!s)return;if("variable"===(null==(e=Array.from(i.classList).find(t=>t.startsWith("bgv2-node-")))?void 0:e.replace("bgv2-node-","")))return;const c=new Set(o);c.has(s)?c.delete(s):c.add(s),O(n,c),r(c)}return t.addEventListener("dblclick",i),()=>t.removeEventListener("dblclick",i)}(n,0,i,f,e=>{f=e,t()})),u&&X(u,e,null)}(),W.set(t,{detachers:h})},t.unmount=z,t.version="0.1.0",Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),t}({});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""emit_html — produce a self-contained bigraph viz HTML snippet."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import html as _html
|
|
5
|
+
import json
|
|
6
|
+
import secrets
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_BUNDLE_PKG = "bigraph_viz2._bundle"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _read_bundle_file(name: str) -> str:
|
|
14
|
+
return resources.files(_BUNDLE_PKG).joinpath(name).read_text(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def emit_html(
|
|
18
|
+
state: dict[str, Any],
|
|
19
|
+
*,
|
|
20
|
+
height: str = "500px",
|
|
21
|
+
width: str = "100%",
|
|
22
|
+
inspector: bool = True,
|
|
23
|
+
theme: str = "light",
|
|
24
|
+
dedupe: bool = False,
|
|
25
|
+
id: str | None = None,
|
|
26
|
+
max_row_width: int = 480,
|
|
27
|
+
) -> str:
|
|
28
|
+
if theme != "light":
|
|
29
|
+
raise ValueError("only theme='light' is supported in v1")
|
|
30
|
+
viz_id = id or _new_id()
|
|
31
|
+
div_id = f"bgv2-{viz_id}"
|
|
32
|
+
data_id = f"bgv2-{viz_id}-data"
|
|
33
|
+
|
|
34
|
+
state_json = json.dumps(state, default=str, ensure_ascii=False)
|
|
35
|
+
|
|
36
|
+
bundle = ""
|
|
37
|
+
if not dedupe:
|
|
38
|
+
css = _read_bundle_file("bigraph-viz2.css")
|
|
39
|
+
js = _read_bundle_file("bigraph-viz2.iife.min.js")
|
|
40
|
+
bundle = (
|
|
41
|
+
f"<style data-bgv2-bundle>{css}</style>"
|
|
42
|
+
f"<script data-bgv2-bundle>{js}</script>"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
opts_json = json.dumps({
|
|
46
|
+
"inspector": inspector,
|
|
47
|
+
"maxRowWidth": max_row_width,
|
|
48
|
+
"id": viz_id,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
style = f"height:{height};width:{width}"
|
|
52
|
+
|
|
53
|
+
bootstrap = (
|
|
54
|
+
"<script>(function(){"
|
|
55
|
+
f"var el=document.getElementById({json.dumps(div_id)});"
|
|
56
|
+
f"var raw=document.getElementById({json.dumps(data_id)}).textContent;"
|
|
57
|
+
"if(!window.BigraphViz){"
|
|
58
|
+
" console.error('bigraph-viz2: bundle not loaded — call emit_html with dedupe=False first');"
|
|
59
|
+
" return;"
|
|
60
|
+
"}"
|
|
61
|
+
f"window.BigraphViz.mount(el, JSON.parse(raw), {opts_json});"
|
|
62
|
+
"})();</script>"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
f"{bundle}"
|
|
67
|
+
f"<div id=\"{_html.escape(div_id, quote=True)}\" "
|
|
68
|
+
f"class=\"bgv2-host\" style=\"{_html.escape(style, quote=True)}\"></div>"
|
|
69
|
+
f"<script type=\"application/json\" id=\"{_html.escape(data_id, quote=True)}\">"
|
|
70
|
+
f"{state_json}"
|
|
71
|
+
f"</script>"
|
|
72
|
+
f"{bootstrap}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _new_id() -> str:
|
|
77
|
+
return secrets.token_hex(4)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Jupyter wrapper. display(BigraphViz(spec)) in a notebook."""
|
|
2
|
+
from .emit import emit_html
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BigraphViz:
|
|
6
|
+
def __init__(self, state, **opts):
|
|
7
|
+
self._state = state
|
|
8
|
+
self._opts = opts
|
|
9
|
+
|
|
10
|
+
def _repr_html_(self):
|
|
11
|
+
return emit_html(self._state, **self._opts)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bigraph-viz2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight read-only bigraph renderer with no graphviz dependency
|
|
5
|
+
Author: The Vivarium Collective
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/vivarium-collective/bigraph-viz2
|
|
8
|
+
Project-URL: Repository, https://github.com/vivarium-collective/bigraph-viz2
|
|
9
|
+
Project-URL: Issues, https://github.com/vivarium-collective/bigraph-viz2/issues
|
|
10
|
+
Keywords: bigraph,vivarium,process-bigraph,visualization,svg,jupyter
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
26
|
+
Requires-Dist: playwright>=1.45; extra == "test"
|
|
27
|
+
|
|
28
|
+
# bigraph-viz2
|
|
29
|
+
|
|
30
|
+
A lightweight, interactive, read-only renderer for
|
|
31
|
+
[process-bigraph](https://github.com/vivarium-collective/process-bigraph)
|
|
32
|
+
composites. Drop-in replacement for `bigraph-viz`'s static PNG output in
|
|
33
|
+
HTML reports — **no graphviz dependency**, pan / zoom / click-to-inspect /
|
|
34
|
+
double-click-to-collapse / Alt-drag-to-rearrange in the browser, and the
|
|
35
|
+
entire renderer inlines into a self-contained ~40 KB snippet.
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
The example above shows a two-cell tissue with nested substores (membrane,
|
|
40
|
+
cytoplasm, nucleus), processes living inside each, and wires reaching
|
|
41
|
+
across container boundaries. Orange = input port, teal = output port.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install bigraph-viz2
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
No graphviz. No node. No browser at install time. The JS bundle is
|
|
50
|
+
vendored inside the Python package; consumers need only Python ≥ 3.10.
|
|
51
|
+
|
|
52
|
+
## Use
|
|
53
|
+
|
|
54
|
+
The Python API has one main function and a Jupyter wrapper.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from bigraph_viz2 import emit_html
|
|
58
|
+
|
|
59
|
+
# one-shot: a self-contained HTML fragment you can paste anywhere
|
|
60
|
+
snippet = emit_html(composite_state, height="600px")
|
|
61
|
+
report_html = report_html.replace("{{BIGRAPH}}", snippet)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
In a Jupyter notebook:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from bigraph_viz2 import BigraphViz
|
|
68
|
+
BigraphViz(composite_state) # auto-displays via _repr_html_
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For pages with **multiple viz instances**, inline the bundle once and
|
|
72
|
+
dedupe the rest:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
snippets = [emit_html(specs[0], dedupe=False)]
|
|
76
|
+
for spec in specs[1:]:
|
|
77
|
+
snippets.append(emit_html(spec, dedupe=True))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### API
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
emit_html(state, *,
|
|
84
|
+
height="500px",
|
|
85
|
+
width="100%",
|
|
86
|
+
inspector=True, # show the right-side inspector panel
|
|
87
|
+
theme="light", # only "light" supported in v0.1
|
|
88
|
+
dedupe=False, # set True after the first call on a page
|
|
89
|
+
id=None, # explicit DOM id (auto-generated otherwise)
|
|
90
|
+
max_row_width=480 # auto-wrap threshold inside each container
|
|
91
|
+
) -> str
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Returns a self-contained HTML fragment.
|
|
95
|
+
|
|
96
|
+
### Interactions
|
|
97
|
+
|
|
98
|
+
| gesture | effect |
|
|
99
|
+
| ------------------------------------ | ------------------------------------------ |
|
|
100
|
+
| drag (anywhere) | pan |
|
|
101
|
+
| wheel | zoom centered on cursor (0.25× – 4×) |
|
|
102
|
+
| hover a port | tooltip with port name |
|
|
103
|
+
| click a node | populate the inspector |
|
|
104
|
+
| double-click a node | collapse / expand subtree |
|
|
105
|
+
| Alt + drag a node | reorder siblings; drop into row / new row |
|
|
106
|
+
| Esc (while dragging) | cancel the drag |
|
|
107
|
+
|
|
108
|
+
Collapse state persists in the URL hash and survives reload.
|
|
109
|
+
|
|
110
|
+
## Concepts
|
|
111
|
+
|
|
112
|
+
`bigraph-viz2` renders [compositional bigraph
|
|
113
|
+
schemas](https://github.com/vivarium-collective/process-bigraph) with
|
|
114
|
+
three primitive shapes:
|
|
115
|
+
|
|
116
|
+
| shape | meaning |
|
|
117
|
+
| ---------------------- | ------------------------------------------------------------- |
|
|
118
|
+
| **circle** | a *variable* — a leaf in the place graph |
|
|
119
|
+
| **rounded rectangle** | a *store* — a container nesting substores, processes, variables |
|
|
120
|
+
| **sharp rectangle** | a *process* — reads from / writes to its declared ports |
|
|
121
|
+
|
|
122
|
+
A composite is a tree of stores; processes live anywhere inside that
|
|
123
|
+
tree. Each process declares **ports**, which connect by **wire** to
|
|
124
|
+
variables — possibly across multiple container boundaries. Ports render
|
|
125
|
+
as small triangles on the edge of the process rectangle:
|
|
126
|
+
|
|
127
|
+
- **orange ▶** — input port (variable feeds the process)
|
|
128
|
+
- **teal ▶** — output port (process writes to the variable)
|
|
129
|
+
- **gray ·** — undirected (direction not declared in the spec)
|
|
130
|
+
|
|
131
|
+
Port direction is read from the spec's `inputs:` / `outputs:` blocks
|
|
132
|
+
(preferred), or from a single `ports:` block (treated as undirected for
|
|
133
|
+
back-compat with existing `bigraph-viz` specs).
|
|
134
|
+
|
|
135
|
+
### Example spec
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
spec = {
|
|
139
|
+
"name": "cell",
|
|
140
|
+
"stores": {
|
|
141
|
+
"membrane": {
|
|
142
|
+
"v": {"_type": "variable", "value": -70},
|
|
143
|
+
"channels": {"_type": "variable", "value": []},
|
|
144
|
+
},
|
|
145
|
+
"cytoplasm": {
|
|
146
|
+
"M": {"_type": "variable", "value": 1.0},
|
|
147
|
+
"ATP": {"_type": "variable", "value": 2.0},
|
|
148
|
+
"metabolism": {
|
|
149
|
+
"_type": "process",
|
|
150
|
+
"address": "fba.CobraStep",
|
|
151
|
+
"config": {"model": "iJO1366"},
|
|
152
|
+
"inputs": {"substrate": ["M"]},
|
|
153
|
+
"outputs": {"atp": ["ATP"]},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
"diffusion": {
|
|
157
|
+
"_type": "process",
|
|
158
|
+
"address": "ode.IonFlux",
|
|
159
|
+
"config": {},
|
|
160
|
+
"inputs": {"voltage": ["membrane", "v"]},
|
|
161
|
+
"outputs": {"channels": ["membrane", "channels"]},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
from bigraph_viz2 import emit_html
|
|
167
|
+
html = emit_html(spec, height="500px")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Wire paths are relative to the process's enclosing store (`["M"]` = the
|
|
171
|
+
sibling named `M`; `["..", "membrane", "v"]` = up to the enclosing store,
|
|
172
|
+
then into `membrane.v`).
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/vivarium-collective/bigraph-viz2
|
|
178
|
+
cd bigraph-viz2
|
|
179
|
+
|
|
180
|
+
# build the JS bundle and vendor it into py/
|
|
181
|
+
bash scripts/vendor.sh
|
|
182
|
+
|
|
183
|
+
# JS tests + typecheck
|
|
184
|
+
cd js && npm test && npm run typecheck
|
|
185
|
+
|
|
186
|
+
# JS end-to-end smoke (real browser)
|
|
187
|
+
cd js && npm run test:e2e
|
|
188
|
+
|
|
189
|
+
# Python tests (includes a Playwright round-trip)
|
|
190
|
+
cd py && pip install -e ".[test]" && pytest
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Status
|
|
194
|
+
|
|
195
|
+
v0.1 — initial release.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
Apache-2.0.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
bigraph_viz2/__init__.py
|
|
4
|
+
bigraph_viz2/emit.py
|
|
5
|
+
bigraph_viz2/jupyter.py
|
|
6
|
+
bigraph_viz2.egg-info/PKG-INFO
|
|
7
|
+
bigraph_viz2.egg-info/SOURCES.txt
|
|
8
|
+
bigraph_viz2.egg-info/dependency_links.txt
|
|
9
|
+
bigraph_viz2.egg-info/requires.txt
|
|
10
|
+
bigraph_viz2.egg-info/top_level.txt
|
|
11
|
+
bigraph_viz2/_bundle/__init__.py
|
|
12
|
+
bigraph_viz2/_bundle/bigraph-viz2.css
|
|
13
|
+
bigraph_viz2/_bundle/bigraph-viz2.iife.min.js
|
|
14
|
+
tests/test_e2e.py
|
|
15
|
+
tests/test_emit.py
|
|
16
|
+
tests/test_jupyter.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bigraph_viz2
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bigraph-viz2"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight read-only bigraph renderer with no graphviz dependency"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "The Vivarium Collective" }]
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = ["bigraph", "vivarium", "process-bigraph", "visualization", "svg", "jupyter"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/vivarium-collective/bigraph-viz2"
|
|
31
|
+
Repository = "https://github.com/vivarium-collective/bigraph-viz2"
|
|
32
|
+
Issues = "https://github.com/vivarium-collective/bigraph-viz2/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
test = ["pytest>=8.0", "playwright>=1.45"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
packages = ["bigraph_viz2", "bigraph_viz2._bundle"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
"bigraph_viz2._bundle" = ["*.js", "*.css"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
playwright = pytest.importorskip("playwright.sync_api")
|
|
8
|
+
from playwright.sync_api import sync_playwright
|
|
9
|
+
|
|
10
|
+
from bigraph_viz2 import emit_html
|
|
11
|
+
|
|
12
|
+
SPEC = json.loads((Path(__file__).parent / "fixtures" / "cell.json").read_text())
|
|
13
|
+
|
|
14
|
+
def _playwright_browsers_installed() -> bool:
|
|
15
|
+
# Linux: ~/.cache/ms-playwright, macOS: ~/Library/Caches/ms-playwright
|
|
16
|
+
candidates = [
|
|
17
|
+
Path.home() / ".cache" / "ms-playwright",
|
|
18
|
+
Path.home() / "Library" / "Caches" / "ms-playwright",
|
|
19
|
+
]
|
|
20
|
+
return shutil.which("chromium") is not None or any(p.exists() for p in candidates)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.skipif(
|
|
24
|
+
not _playwright_browsers_installed(),
|
|
25
|
+
reason="playwright browsers not installed",
|
|
26
|
+
)
|
|
27
|
+
def test_emit_html_renders_in_browser(tmp_path):
|
|
28
|
+
page_html = (
|
|
29
|
+
"<!doctype html><html><body>"
|
|
30
|
+
+ emit_html(SPEC, id="e2e", dedupe=False)
|
|
31
|
+
+ "</body></html>"
|
|
32
|
+
)
|
|
33
|
+
f = tmp_path / "page.html"
|
|
34
|
+
f.write_text(page_html)
|
|
35
|
+
with sync_playwright() as p:
|
|
36
|
+
browser = p.chromium.launch()
|
|
37
|
+
page = browser.new_page()
|
|
38
|
+
errors = []
|
|
39
|
+
page.on("pageerror", lambda e: errors.append(str(e)))
|
|
40
|
+
page.goto(f.as_uri())
|
|
41
|
+
page.wait_for_selector(".bgv2-svg")
|
|
42
|
+
assert page.locator(".bgv2-proc").count() == 2
|
|
43
|
+
browser.close()
|
|
44
|
+
assert errors == []
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from bigraph_viz2 import emit_html
|
|
6
|
+
|
|
7
|
+
FIX = Path(__file__).parent / "fixtures" / "cell.json"
|
|
8
|
+
SPEC = json.loads(FIX.read_text())
|
|
9
|
+
|
|
10
|
+
def test_emit_returns_str():
|
|
11
|
+
html = emit_html(SPEC)
|
|
12
|
+
assert isinstance(html, str)
|
|
13
|
+
assert len(html) > 0
|
|
14
|
+
|
|
15
|
+
def test_emit_contains_div_with_data_attr():
|
|
16
|
+
html = emit_html(SPEC, id="viz1")
|
|
17
|
+
assert 'id="bgv2-viz1"' in html
|
|
18
|
+
assert "<div" in html
|
|
19
|
+
assert "<script" in html
|
|
20
|
+
|
|
21
|
+
def test_emit_embeds_bundle_when_dedupe_false():
|
|
22
|
+
html = emit_html(SPEC, dedupe=False)
|
|
23
|
+
assert "BigraphViz" in html # bundle global is named BigraphViz
|
|
24
|
+
assert "<style" in html
|
|
25
|
+
|
|
26
|
+
def test_emit_skips_bundle_when_dedupe_true():
|
|
27
|
+
html = emit_html(SPEC, dedupe=True)
|
|
28
|
+
# bootstrap script still present, but no embedded bundle definition
|
|
29
|
+
assert "<style" not in html
|
|
30
|
+
# the dedupe path emits a small bootstrap, not the whole IIFE
|
|
31
|
+
assert len(html) < 5_000 # bundle is ~12KB; dedupe path << that
|
|
32
|
+
|
|
33
|
+
def test_emit_json_round_trips_spec():
|
|
34
|
+
html = emit_html(SPEC, id="viz2")
|
|
35
|
+
m = re.search(r'id="bgv2-viz2-data"[^>]*>(.*?)</script>', html, re.S)
|
|
36
|
+
assert m is not None
|
|
37
|
+
embedded = json.loads(m.group(1))
|
|
38
|
+
assert embedded == SPEC
|
|
39
|
+
|
|
40
|
+
def test_emit_height_and_width():
|
|
41
|
+
html = emit_html(SPEC, height="600px", width="80%")
|
|
42
|
+
assert "height:600px" in html or "height: 600px" in html
|
|
43
|
+
assert "width:80%" in html or "width: 80%" in html
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from bigraph_viz2 import BigraphViz, emit_html
|
|
5
|
+
|
|
6
|
+
SPEC = json.loads((Path(__file__).parent / "fixtures" / "cell.json").read_text())
|
|
7
|
+
|
|
8
|
+
def test_repr_html_matches_emit_html():
|
|
9
|
+
bv = BigraphViz(SPEC, id="viz1", dedupe=True)
|
|
10
|
+
assert bv._repr_html_() == emit_html(SPEC, id="viz1", dedupe=True)
|