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.
Files changed (80) hide show
  1. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/workflows/docs.yml +17 -1
  2. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/PKG-INFO +1 -1
  3. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/advanced.png +0 -0
  4. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/custom_editor.png +0 -0
  5. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/edge_editors.png +0 -0
  6. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/node_edge_instances.png +0 -0
  7. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/schema_types.png +0 -0
  8. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/simple.png +0 -0
  9. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/threejs_viewer.png +0 -0
  10. panel_reactflow-0.3.0rc1/docs/assets/screenshots/examples/threejs_viewer_instances.png +0 -0
  11. panel_reactflow-0.3.0rc1/docs/examples/advanced.md +12 -0
  12. panel_reactflow-0.3.0rc1/docs/examples/custom-editor.md +12 -0
  13. panel_reactflow-0.3.0rc1/docs/examples/edge-editors.md +12 -0
  14. panel_reactflow-0.3.0rc1/docs/examples/index.md +64 -0
  15. panel_reactflow-0.3.0rc1/docs/examples/node-edge-instances.md +12 -0
  16. panel_reactflow-0.3.0rc1/docs/examples/schema-types.md +12 -0
  17. panel_reactflow-0.3.0rc1/docs/examples/simple.md +11 -0
  18. panel_reactflow-0.3.0rc1/docs/examples/threejs-viewer-instances.md +12 -0
  19. panel_reactflow-0.3.0rc1/docs/examples/threejs-viewer.md +12 -0
  20. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/define-nodes-edges.md +110 -3
  21. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/react-to-events.md +54 -2
  22. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/index.md +4 -0
  23. panel_reactflow-0.3.0rc1/examples/node_edge_instances.py +138 -0
  24. panel_reactflow-0.3.0rc1/examples/threejs_viewer_instances.py +335 -0
  25. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/__init__.py +4 -0
  26. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/base.py +727 -94
  27. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/test_api.py +312 -1
  28. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/test_core.py +40 -1
  29. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/zensical.toml +11 -1
  30. panel_reactflow-0.3.0rc0/docs/examples.md +0 -13
  31. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.copier-answers.yml +0 -0
  32. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.gitattributes +0 -0
  33. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/CODEOWNERS +0 -0
  34. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/dependabot.yml +0 -0
  35. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/workflows/build.yml +0 -0
  36. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.github/workflows/test.yml +0 -0
  37. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.gitignore +0 -0
  38. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.pre-commit-config.yaml +0 -0
  39. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/.prettierrc +0 -0
  40. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/LICENSE.txt +0 -0
  41. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/MANIFEST.in +0 -0
  42. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/README.md +0 -0
  43. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/logo.svg +0 -0
  44. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/declare-types.png +0 -0
  45. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/define-editors-edge.png +0 -0
  46. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/define-editors-node.png +0 -0
  47. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/define-nodes-edges.png +0 -0
  48. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/embed-views-in-nodes.png +0 -0
  49. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/quickstart.png +0 -0
  50. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/react-to-events.png +0 -0
  51. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/assets/screenshots/style-nodes-edges.png +0 -0
  52. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/declare-types.md +0 -0
  53. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/define-editors.md +0 -0
  54. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/embed-views-in-nodes.md +0 -0
  55. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/how-to/style-nodes-edges.md +0 -0
  56. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/quickstart.md +0 -0
  57. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/reference/panel_reactflow.md +0 -0
  58. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/docs/releases.md +0 -0
  59. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/advanced.py +0 -0
  60. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/custom_editor.py +0 -0
  61. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/edge_editors.py +0 -0
  62. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/schema_types.py +0 -0
  63. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/simple.py +0 -0
  64. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/examples/threejs_viewer.py +0 -0
  65. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/hatch_build.py +0 -0
  66. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/pixi.lock +0 -0
  67. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/pixi.toml +0 -0
  68. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/pyproject.toml +0 -0
  69. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/__version.py +0 -0
  70. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/css/reactflow.css +0 -0
  71. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/icons/gear.svg +0 -0
  72. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/panel-reactflow.bundle.css +0 -0
  73. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/dist/panel-reactflow.bundle.js +0 -0
  74. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/models/reactflow.jsx +0 -0
  75. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/py.typed +0 -0
  76. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/src/panel_reactflow/schema.py +0 -0
  77. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/__init__.py +0 -0
  78. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/conftest.py +0 -0
  79. {panel_reactflow-0.3.0rc0 → panel_reactflow-0.3.0rc1}/tests/ui/__init__.py +0 -0
  80. {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 to GitHub Pages
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.0rc0
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
@@ -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
+ ![Screenshot: advanced example](../assets/screenshots/examples/advanced.png)
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
+ ![Screenshot: custom editor example](../assets/screenshots/examples/custom_editor.png)
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
+ ![Screenshot: edge editors example](../assets/screenshots/examples/edge_editors.png)
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
+ - ![Advanced example](../assets/screenshots/examples/advanced.png)
9
+
10
+ ---
11
+
12
+ **[Advanced](advanced.md)**
13
+ Schema-driven task nodes with event tracking.
14
+
15
+ - ![Custom editor example](../assets/screenshots/examples/custom_editor.png)
16
+
17
+ ---
18
+
19
+ **[Custom Editor](custom-editor.md)**
20
+ Callable node editor with custom widgets.
21
+
22
+ - ![Edge editors example](../assets/screenshots/examples/edge_editors.png)
23
+
24
+ ---
25
+
26
+ **[Edge Editors](edge-editors.md)**
27
+ Schema-backed and callable edge editors.
28
+
29
+ - ![Node/edge instances example](../assets/screenshots/examples/node_edge_instances.png)
30
+
31
+ ---
32
+
33
+ **[Node/Edge Instances](node-edge-instances.md)**
34
+ Class-based nodes and edges with hooks.
35
+
36
+ - ![Schema types example](../assets/screenshots/examples/schema_types.png)
37
+
38
+ ---
39
+
40
+ **[Schema Types](schema-types.md)**
41
+ Multiple node types, each with its own schema.
42
+
43
+ - ![Simple example](../assets/screenshots/examples/simple.png)
44
+
45
+ ---
46
+
47
+ **[Simple](simple.md)**
48
+ Minimal graph setup with top panel.
49
+
50
+ - ![ThreeJS viewer example](../assets/screenshots/examples/threejs_viewer.png)
51
+
52
+ ---
53
+
54
+ **[ThreeJS Viewer](threejs-viewer.md)**
55
+ Graph-driven 3D cube viewer controls.
56
+
57
+ - ![ThreeJS viewer instances example](../assets/screenshots/examples/threejs_viewer_instances.png)
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
+ ![Screenshot: node and edge instances example](../assets/screenshots/examples/node_edge_instances.png)
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
+ ![Screenshot: schema types example](../assets/screenshots/examples/schema_types.png)
7
+
8
+ ## Source
9
+
10
+ ```python
11
+ --8<-- "examples/schema_types.py"
12
+ ```
@@ -0,0 +1,11 @@
1
+ # Simple
2
+
3
+ A minimal two-node graph setup with an edge and an optional top panel.
4
+
5
+ ![Screenshot: simple example](../assets/screenshots/examples/simple.png)
6
+
7
+ ## Source
8
+
9
+ ```python
10
+ --8<-- "examples/simple.py"
11
+ ```
@@ -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
+ ![Screenshot: threejs viewer instances example](../assets/screenshots/examples/threejs_viewer_instances.png)
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
+ ![Screenshot: threejs viewer example](../assets/screenshots/examples/threejs_viewer.png)
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. Both are plain Python dictionaries, so you can
6
- construct them from any data source a database, a config file, or user
7
- input at runtime.
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` | `patch_node_data()` is called. | `node_id`, `patch` |
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` | `patch_edge_data()` is called. | `edge_id`, `patch` |
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()