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.
@@ -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
+ ![bigraph-viz2 rendering a multi-cell tissue composite](docs/example-tissue.png)
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
+ ![bigraph-viz2 rendering a multi-cell tissue composite](docs/example-tissue.png)
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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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
+ ![bigraph-viz2 rendering a multi-cell tissue composite](docs/example-tissue.png)
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,4 @@
1
+
2
+ [test]
3
+ pytest>=8.0
4
+ playwright>=1.45
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)