panel-reactflow 0.3.0rc0__tar.gz → 0.3.0rc1__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.
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/workflows/docs.yml +17 -1
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/PKG-INFO +1 -1
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/advanced.png +0 -0
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/custom_editor.png +0 -0
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/edge_editors.png +0 -0
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/node_edge_instances.png +0 -0
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/schema_types.png +0 -0
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/simple.png +0 -0
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/threejs_viewer.png +0 -0
- panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/threejs_viewer_instances.png +0 -0
- panel_reactflow-0.3.0rc1/docs/examples/advanced.md +12 -0
- panel_reactflow-0.3.0rc1/docs/examples/custom-editor.md +12 -0
- panel_reactflow-0.3.0rc1/docs/examples/edge-editors.md +12 -0
- panel_reactflow-0.3.0rc1/docs/examples/index.md +64 -0
- panel_reactflow-0.3.0rc1/docs/examples/node-edge-instances.md +12 -0
- panel_reactflow-0.3.0rc1/docs/examples/schema-types.md +12 -0
- panel_reactflow-0.3.0rc1/docs/examples/simple.md +11 -0
- panel_reactflow-0.3.0rc1/docs/examples/threejs-viewer-instances.md +12 -0
- panel_reactflow-0.3.0rc1/docs/examples/threejs-viewer.md +12 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/define-nodes-edges.md +110 -3
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/react-to-events.md +54 -2
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/index.md +4 -0
- panel_reactflow-0.3.0rc1/examples/node_edge_instances.py +138 -0
- panel_reactflow-0.3.0rc1/examples/threejs_viewer_instances.py +335 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/__init__.py +4 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/base.py +727 -94
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/test_api.py +312 -1
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/test_core.py +40 -1
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/zensical.toml +11 -1
- panel_reactflow-0.3.0rc0/docs/examples.md +0 -13
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.copier-answers.yml +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.gitattributes +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/CODEOWNERS +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/dependabot.yml +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/workflows/build.yml +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/workflows/test.yml +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.gitignore +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.pre-commit-config.yaml +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.prettierrc +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/LICENSE.txt +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/MANIFEST.in +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/README.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/logo.svg +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/declare-types.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/define-editors-edge.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/define-editors-node.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/define-nodes-edges.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/embed-views-in-nodes.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/quickstart.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/react-to-events.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/style-nodes-edges.png +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/declare-types.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/define-editors.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/embed-views-in-nodes.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/style-nodes-edges.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/quickstart.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/reference/panel_reactflow.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/releases.md +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/advanced.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/custom_editor.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/edge_editors.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/schema_types.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/simple.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/threejs_viewer.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/hatch_build.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/pixi.lock +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/pixi.toml +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/pyproject.toml +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/__version.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/css/reactflow.css +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/icons/gear.svg +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/panel-reactflow.bundle.css +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/panel-reactflow.bundle.js +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/models/reactflow.jsx +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/py.typed +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/schema.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/__init__.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/conftest.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/ui/__init__.py +0 -0
- {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/ui/test_ui.py +0 -0
|
@@ -70,6 +70,22 @@ jobs:
|
|
|
70
70
|
runs-on: ubuntu-latest
|
|
71
71
|
needs: build
|
|
72
72
|
steps:
|
|
73
|
-
- name: Deploy
|
|
73
|
+
- name: Deploy dev
|
|
74
|
+
uses: peaceiris/actions-gh-pages@v4
|
|
75
|
+
# Dev site built on PRs
|
|
76
|
+
if: |
|
|
77
|
+
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
|
|
78
|
+
(github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'dev') ||
|
|
79
|
+
(github.event_name == 'push' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc')))
|
|
80
|
+
with:
|
|
81
|
+
personal_token: ${{ secrets.HOLOVIZ_ACCESS_TOKEN }}
|
|
82
|
+
external_repository: holoviz-dev/panel-reactflow
|
|
83
|
+
publish_dir: ./builtdocs
|
|
84
|
+
force_orphan: true
|
|
85
|
+
- name: Deploy main
|
|
86
|
+
if: |
|
|
87
|
+
(github.event_name != 'pull_request') &&
|
|
88
|
+
((github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'main') ||
|
|
89
|
+
(github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))))
|
|
74
90
|
id: deployment
|
|
75
91
|
uses: actions/deploy-pages@v4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: panel-reactflow
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0rc1
|
|
4
4
|
Summary: A Panel wrapper for the Reactflow JS library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/panel-extensions/panel-reactflow
|
|
6
6
|
Project-URL: Source, https://github.com/panel-extensions/panel-reactflow
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Advanced
|
|
2
|
+
|
|
3
|
+
Schema-driven editor workflow with a custom node type, type stylesheets,
|
|
4
|
+
and top-panel event feedback.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
--8<-- "examples/advanced.py"
|
|
12
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Custom Editor
|
|
2
|
+
|
|
3
|
+
Registers a callable editor for `metric` nodes and pushes incremental updates
|
|
4
|
+
using `on_patch`.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
--8<-- "examples/custom_editor.py"
|
|
12
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Edge Editors
|
|
2
|
+
|
|
3
|
+
Shows schema-backed edge editing plus a custom callable edge editor for
|
|
4
|
+
specialized edge types.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
--8<-- "examples/edge_editors.py"
|
|
12
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Examples Gallery
|
|
2
|
+
|
|
3
|
+
Browse runnable examples from `examples/`. Each card links to a dedicated page
|
|
4
|
+
with a screenshot and full source code.
|
|
5
|
+
|
|
6
|
+
<div class="grid cards" markdown>
|
|
7
|
+
|
|
8
|
+
- 
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
**[Advanced](advanced.md)**
|
|
13
|
+
Schema-driven task nodes with event tracking.
|
|
14
|
+
|
|
15
|
+
- 
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
**[Custom Editor](custom-editor.md)**
|
|
20
|
+
Callable node editor with custom widgets.
|
|
21
|
+
|
|
22
|
+
- 
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
**[Edge Editors](edge-editors.md)**
|
|
27
|
+
Schema-backed and callable edge editors.
|
|
28
|
+
|
|
29
|
+
- 
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
**[Node/Edge Instances](node-edge-instances.md)**
|
|
34
|
+
Class-based nodes and edges with hooks.
|
|
35
|
+
|
|
36
|
+
- 
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
**[Schema Types](schema-types.md)**
|
|
41
|
+
Multiple node types, each with its own schema.
|
|
42
|
+
|
|
43
|
+
- 
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
**[Simple](simple.md)**
|
|
48
|
+
Minimal graph setup with top panel.
|
|
49
|
+
|
|
50
|
+
- 
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
**[ThreeJS Viewer](threejs-viewer.md)**
|
|
55
|
+
Graph-driven 3D cube viewer controls.
|
|
56
|
+
|
|
57
|
+
- 
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
**[ThreeJS Viewer (Instances)](threejs-viewer-instances.md)**
|
|
62
|
+
ThreeJS viewer with Node and Edge instances.
|
|
63
|
+
|
|
64
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Node/Edge Instances
|
|
2
|
+
|
|
3
|
+
Uses `Node` and `Edge` subclasses for rich behavior, custom editors, and event
|
|
4
|
+
hooks while still rendering in a standard ReactFlow canvas.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
--8<-- "examples/node_edge_instances.py"
|
|
12
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Schema Types
|
|
2
|
+
|
|
3
|
+
Demonstrates multiple node types where each type contributes its own schema,
|
|
4
|
+
including both Param-derived and raw JSON Schema definitions.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
--8<-- "examples/schema_types.py"
|
|
12
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# ThreeJS Viewer (Instances)
|
|
2
|
+
|
|
3
|
+
The ThreeJS viewer example implemented with `Node` and `Edge` subclass
|
|
4
|
+
instances for object-oriented graph behavior.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
--8<-- "examples/threejs_viewer_instances.py"
|
|
12
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# ThreeJS Viewer
|
|
2
|
+
|
|
3
|
+
Interactive 3D cube rendering controlled through graph-connected parameter
|
|
4
|
+
nodes.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
--8<-- "examples/threejs_viewer.py"
|
|
12
|
+
```
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Every graph in Panel-ReactFlow is built from two lists: **nodes** and
|
|
4
4
|
**edges**. Nodes represent entities on the canvas; edges represent
|
|
5
|
-
connections between them.
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
connections between them. Nodes can be plain dictionaries, `NodeSpec`
|
|
6
|
+
objects, or `Node` instances, so you can choose between lightweight payloads
|
|
7
|
+
and object-oriented node classes.
|
|
8
8
|
|
|
9
9
|
This guide covers how to create nodes and edges, use the helper dataclasses,
|
|
10
10
|
and update data after the graph is live.
|
|
@@ -104,6 +104,40 @@ nodes = [
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
+
## Define nodes as classes
|
|
108
|
+
|
|
109
|
+
Use `Node` when you want per-node Python state, event hooks, and optional
|
|
110
|
+
custom view/editor methods.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import panel as pn
|
|
114
|
+
from panel_reactflow import Node, ReactFlow
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class JobNode(Node):
|
|
118
|
+
def __init__(self, **params):
|
|
119
|
+
super().__init__(type="job", data={"status": "idle"}, **params)
|
|
120
|
+
|
|
121
|
+
def __panel__(self):
|
|
122
|
+
return pn.pane.Markdown(f"**{self.label}**: {self.data.get('status')}")
|
|
123
|
+
|
|
124
|
+
def on_move(self, payload, flow):
|
|
125
|
+
print(f"{self.id} moved to {payload['position']}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
nodes = [
|
|
129
|
+
JobNode(id="j1", label="Fetch", position={"x": 0, "y": 0}),
|
|
130
|
+
JobNode(id="j2", label="Process", position={"x": 260, "y": 60}),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
flow = ReactFlow(nodes=nodes)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`Node` instances stay as Python objects in `flow.nodes`; they are serialized
|
|
137
|
+
to dicts only when syncing to the frontend.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
107
141
|
## Define edges
|
|
108
142
|
|
|
109
143
|
Edges link two nodes by their `id`. Use the top-level `label` for the
|
|
@@ -128,6 +162,79 @@ edges = [
|
|
|
128
162
|
|
|
129
163
|
---
|
|
130
164
|
|
|
165
|
+
## Define edges as classes
|
|
166
|
+
|
|
167
|
+
Use `Edge` when you want object-oriented edge state and edge-specific hooks or
|
|
168
|
+
editor logic.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from panel_reactflow import Edge, ReactFlow
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class FlowEdge(Edge):
|
|
175
|
+
def __init__(self, **params):
|
|
176
|
+
super().__init__(type="flow", data={"weight": 1.0}, **params)
|
|
177
|
+
|
|
178
|
+
def on_data_change(self, payload, flow):
|
|
179
|
+
print(f"{self.id} updated:", payload["patch"])
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
flow = ReactFlow(
|
|
183
|
+
nodes=[
|
|
184
|
+
{"id": "n1", "position": {"x": 0, "y": 0}, "data": {}},
|
|
185
|
+
{"id": "n2", "position": {"x": 260, "y": 60}, "data": {}},
|
|
186
|
+
],
|
|
187
|
+
edges=[FlowEdge(id="e1", source="n1", target="n2")],
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`Edge` instances stay as Python objects in `flow.edges`; they are serialized
|
|
192
|
+
to dicts only when syncing to the frontend.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Data <-> parameter sync on `Node` and `Edge`
|
|
197
|
+
|
|
198
|
+
For class-based nodes/edges, Panel-ReactFlow supports two-way synchronization
|
|
199
|
+
between `data` and declared parameters.
|
|
200
|
+
|
|
201
|
+
### Which parameters are included?
|
|
202
|
+
|
|
203
|
+
Only subclass parameters with **explicit non-negative precedence**
|
|
204
|
+
(`precedence >= 0`) are treated as data fields.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
import param
|
|
208
|
+
from panel_reactflow import Node
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TaskNode(Node):
|
|
212
|
+
status = param.Selector(default="idle", objects=["idle", "running", "done"], precedence=0)
|
|
213
|
+
retries = param.Integer(default=0, precedence=0)
|
|
214
|
+
_internal_state = param.String(default="x", precedence=-1)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
In this example:
|
|
218
|
+
|
|
219
|
+
- `status` and `retries` are included in `data`
|
|
220
|
+
- `_internal_state` is not included
|
|
221
|
+
|
|
222
|
+
### Sync behavior
|
|
223
|
+
|
|
224
|
+
- **Parameter -> data**: updating `node.status` or `edge.weight` triggers an
|
|
225
|
+
automatic data patch to the graph and frontend.
|
|
226
|
+
- **Data -> parameter**: incoming graph patches/sync updates write values back
|
|
227
|
+
onto matching parameters.
|
|
228
|
+
- **Schema generation**: if no explicit type schema is provided, these
|
|
229
|
+
included parameters are used to generate a JSON schema for editors.
|
|
230
|
+
|
|
231
|
+
### Editor implication
|
|
232
|
+
|
|
233
|
+
If your editor widgets are bound with `from_param(...)`, you usually do not
|
|
234
|
+
need manual `on_patch` watchers for those data parameters.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
131
238
|
## Use the NodeSpec / EdgeSpec helpers
|
|
132
239
|
|
|
133
240
|
If you prefer a typed API, use the dataclass helpers. They validate fields
|
|
@@ -23,10 +23,10 @@ the `ReactFlow` instance as a second argument. You can also listen for
|
|
|
23
23
|
| `node_deleted` | A node is removed. | `node_id` |
|
|
24
24
|
| `node_moved` | A node is dragged to a new position. | `node_id`, `position` |
|
|
25
25
|
| `node_clicked` | A node is clicked (single click). | `node_id` |
|
|
26
|
-
| `node_data_changed` |
|
|
26
|
+
| `node_data_changed` | Node data is patched (via API, editor patch, or parameter-driven sync). | `node_id`, `patch` |
|
|
27
27
|
| `edge_added` | An edge is created (UI connect or API). | `edge` |
|
|
28
28
|
| `edge_deleted` | An edge is removed. | `edge_id` |
|
|
29
|
-
| `edge_data_changed` |
|
|
29
|
+
| `edge_data_changed` | Edge data is patched (via API, editor patch, or parameter-driven sync). | `edge_id`, `patch` |
|
|
30
30
|
| `selection_changed` | The active selection changes. | `nodes`, `edges` |
|
|
31
31
|
| `sync` | A batch sync from the frontend. | *(varies)* |
|
|
32
32
|
|
|
@@ -57,6 +57,58 @@ pn.Column(log, flow).servable()
|
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
+
## Handle events on `Node` classes
|
|
61
|
+
|
|
62
|
+
If you define nodes as `Node` subclasses, you can implement hooks directly on
|
|
63
|
+
the node instance:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from panel_reactflow import Node, ReactFlow
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TaskNode(Node):
|
|
70
|
+
def on_event(self, payload, flow):
|
|
71
|
+
print("any node event:", payload["type"])
|
|
72
|
+
|
|
73
|
+
def on_delete(self, payload, flow):
|
|
74
|
+
print("deleted:", self.id)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
flow = ReactFlow(nodes=[TaskNode(id="t1", position={"x": 0, "y": 0}, data={})])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Common hooks include `on_event` (wildcard), `on_add`, `on_move`, `on_click`,
|
|
81
|
+
`on_data_change`, and `on_delete`.
|
|
82
|
+
|
|
83
|
+
When a `Node` subclass parameter with `precedence >= 0` changes, it
|
|
84
|
+
automatically patches node data and will trigger `on_data_change`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Handle events on `Edge` classes
|
|
89
|
+
|
|
90
|
+
`Edge` subclasses can handle edge lifecycle and patch events directly:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from panel_reactflow import Edge, ReactFlow
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class WeightedEdge(Edge):
|
|
97
|
+
def on_data_change(self, payload, flow):
|
|
98
|
+
print("edge patch:", payload["patch"])
|
|
99
|
+
|
|
100
|
+
def on_delete(self, payload, flow):
|
|
101
|
+
print("edge deleted:", self.id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Common edge hooks include `on_event`, `on_add`, `on_data_change`,
|
|
105
|
+
`on_selection_changed`, and `on_delete`.
|
|
106
|
+
|
|
107
|
+
Likewise, changing an `Edge` subclass data parameter (`precedence >= 0`)
|
|
108
|
+
triggers `on_data_change` through the same data patch pipeline.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
60
112
|
## Listen for all events
|
|
61
113
|
|
|
62
114
|
Use the wildcard `"*"` to receive every event. This is useful for
|
|
@@ -66,6 +66,10 @@ flow.servable()
|
|
|
66
66
|
- [Style Nodes & Edges](how-to/style-nodes-edges.md)
|
|
67
67
|
- [React to Events](how-to/react-to-events.md)
|
|
68
68
|
|
|
69
|
+
## Examples
|
|
70
|
+
|
|
71
|
+
- [Examples gallery](examples/index.md)
|
|
72
|
+
|
|
69
73
|
## Reference
|
|
70
74
|
|
|
71
75
|
- [API reference](reference/panel_reactflow.md)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Complex example using Node and Edge class instances.
|
|
2
|
+
|
|
3
|
+
Demonstrates:
|
|
4
|
+
- ``Node`` / ``Edge`` subclass instances in ``ReactFlow``
|
|
5
|
+
- Per-instance ``__panel__`` node views
|
|
6
|
+
- Per-instance custom editors via ``editor(...)``
|
|
7
|
+
- Node/edge event hooks (``on_data_change``, ``on_selection_changed``)
|
|
8
|
+
- Programmatic updates with ``patch_node_data`` / ``patch_edge_data``
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
|
|
13
|
+
import panel as pn
|
|
14
|
+
import panel_material_ui as pmui
|
|
15
|
+
import param
|
|
16
|
+
|
|
17
|
+
from panel_reactflow import Edge, Node, ReactFlow
|
|
18
|
+
|
|
19
|
+
pn.extension()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PipelineNode(Node):
|
|
23
|
+
status = param.Selector(default="idle", objects=["idle", "running", "done", "failed"], precedence=0)
|
|
24
|
+
retries = param.Integer(default=0, bounds=(0, None), precedence=0)
|
|
25
|
+
owner = param.String(default="ops", precedence=0)
|
|
26
|
+
notes = param.String(default="", precedence=0)
|
|
27
|
+
|
|
28
|
+
def __init__(self, **params):
|
|
29
|
+
params.setdefault("type", "pipeline")
|
|
30
|
+
super().__init__(**params)
|
|
31
|
+
self._summary = pn.pane.Markdown(margin=(0, 0, 6, 0))
|
|
32
|
+
self._activity = pn.pane.Markdown("", styles={"font-size": "12px", "opacity": "0.8"})
|
|
33
|
+
self.param.watch(self._refresh_view, ["status", "owner", "retries", "label"])
|
|
34
|
+
self._refresh_view()
|
|
35
|
+
|
|
36
|
+
def _refresh_view(self, *_):
|
|
37
|
+
self._summary.object = (
|
|
38
|
+
f"**{self.label}** \n"
|
|
39
|
+
f"Status: `{self.status}` \n"
|
|
40
|
+
f"Owner: `{self.owner}` \n"
|
|
41
|
+
f"Retries: `{self.retries}`"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __panel__(self):
|
|
45
|
+
return pn.Column(self._summary, self._activity, margin=0, sizing_mode="stretch_width")
|
|
46
|
+
|
|
47
|
+
def editor(self, data, schema, *, id, type, on_patch):
|
|
48
|
+
status = pmui.Select.from_param(self.param.status, name="Status")
|
|
49
|
+
retries = pmui.IntInput.from_param(self.param.retries, name="Retries")
|
|
50
|
+
owner = pmui.TextInput.from_param(self.param.owner, name="Owner")
|
|
51
|
+
notes = pmui.TextAreaInput.from_param(self.param.notes, name="Notes", height=80)
|
|
52
|
+
return pn.Column(status, retries, owner, notes, sizing_mode="stretch_width")
|
|
53
|
+
|
|
54
|
+
def on_data_change(self, payload, flow):
|
|
55
|
+
if payload.get("node_id") == self.id:
|
|
56
|
+
self._activity.object = f"Last patch: `{payload.get('patch', {})}`"
|
|
57
|
+
|
|
58
|
+
def on_selection_changed(self, payload, flow):
|
|
59
|
+
selected = self.id in (payload.get("nodes") or [])
|
|
60
|
+
if selected:
|
|
61
|
+
self._activity.object = "Selected in canvas"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class WeightedEdge(Edge):
|
|
65
|
+
weight = param.Number(default=0.5, bounds=(0, 1), precedence=0)
|
|
66
|
+
channel = param.Selector(default="main", objects=["main", "backup", "shadow"], precedence=0)
|
|
67
|
+
enabled = param.Boolean(default=True, precedence=0)
|
|
68
|
+
|
|
69
|
+
def __init__(self, **params):
|
|
70
|
+
params.setdefault("type", "weighted")
|
|
71
|
+
super().__init__(**params)
|
|
72
|
+
|
|
73
|
+
def editor(self, data, schema, *, id, type, on_patch):
|
|
74
|
+
weight = pmui.FloatSlider.from_param(self.param.weight, name="Weight", step=0.01)
|
|
75
|
+
channel = pmui.Select.from_param(self.param.channel, name="Channel")
|
|
76
|
+
enabled = pmui.Checkbox.from_param(self.param.enabled, name="Enabled")
|
|
77
|
+
return pn.Column(weight, channel, enabled, sizing_mode="stretch_width")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
nodes = [
|
|
81
|
+
PipelineNode(id="extract", label="Extract", position={"x": 0, "y": 40}),
|
|
82
|
+
PipelineNode(id="transform", label="Transform", position={"x": 300, "y": 160}, status="running", retries=1, owner="ml", notes="Batch window"),
|
|
83
|
+
PipelineNode(id="load", label="Load", position={"x": 600, "y": 40}, owner="platform"),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
edges = [
|
|
87
|
+
WeightedEdge(id="e1", source="extract", target="transform", weight=0.72),
|
|
88
|
+
WeightedEdge(id="e2", source="transform", target="load", weight=0.63, channel="backup"),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
event_log = pmui.TextAreaInput(name="Events", value="", disabled=True, height=180, sizing_mode="stretch_width")
|
|
92
|
+
last_event = pn.pane.Markdown("**Last event:** _none_")
|
|
93
|
+
|
|
94
|
+
flow = ReactFlow(
|
|
95
|
+
nodes=nodes,
|
|
96
|
+
edges=edges,
|
|
97
|
+
editor_mode="side",
|
|
98
|
+
sizing_mode="stretch_both",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _log_event(payload):
|
|
102
|
+
event_type = payload.get("type", "unknown")
|
|
103
|
+
last_event.object = f"**Last event:** `{event_type}`"
|
|
104
|
+
snippet = str(payload)
|
|
105
|
+
event_log.value = f"{event_log.value}\n{event_type}: {snippet}"[-6000:]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
flow.on("*", _log_event)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _advance_nodes(_):
|
|
112
|
+
order = {"idle": "running", "running": "done", "done": "done", "failed": "idle"}
|
|
113
|
+
for node in nodes:
|
|
114
|
+
current = node.status
|
|
115
|
+
flow.patch_node_data(node.id, {"status": order.get(current, "idle")})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _randomize_weights(_):
|
|
119
|
+
for edge in edges:
|
|
120
|
+
flow.patch_edge_data(edge.id, {"weight": round(random.uniform(0.05, 0.95), 2)})
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
advance_btn = pmui.Button(name="Advance pipeline")
|
|
124
|
+
advance_btn.on_click(_advance_nodes)
|
|
125
|
+
|
|
126
|
+
weights_btn = pmui.Button(name="Randomize edge weights")
|
|
127
|
+
weights_btn.on_click(_randomize_weights)
|
|
128
|
+
|
|
129
|
+
controls = pn.Row(advance_btn, weights_btn, sizing_mode="stretch_width")
|
|
130
|
+
|
|
131
|
+
pn.Column(
|
|
132
|
+
pn.pane.Markdown("## Node/Edge Instance Workflow"),
|
|
133
|
+
controls,
|
|
134
|
+
last_event,
|
|
135
|
+
flow,
|
|
136
|
+
event_log,
|
|
137
|
+
sizing_mode="stretch_both",
|
|
138
|
+
).servable()
|