diff --git a/docs/assets/screenshots/declare-types.png b/docs/assets/screenshots/declare-types.png index eb3eb77..4b578da 100644 Binary files a/docs/assets/screenshots/declare-types.png and b/docs/assets/screenshots/declare-types.png differ diff --git a/docs/assets/screenshots/define-editors-edge.png b/docs/assets/screenshots/define-editors-edge.png index 0c7e147..aa05be0 100644 Binary files a/docs/assets/screenshots/define-editors-edge.png and b/docs/assets/screenshots/define-editors-edge.png differ diff --git a/docs/assets/screenshots/define-editors-node.png b/docs/assets/screenshots/define-editors-node.png index 9099e7c..dd88f1c 100644 Binary files a/docs/assets/screenshots/define-editors-node.png and b/docs/assets/screenshots/define-editors-node.png differ diff --git a/docs/assets/screenshots/define-nodes-edges.png b/docs/assets/screenshots/define-nodes-edges.png index 94d48e8..5e55ff9 100644 Binary files a/docs/assets/screenshots/define-nodes-edges.png and b/docs/assets/screenshots/define-nodes-edges.png differ diff --git a/docs/assets/screenshots/embed-views-in-nodes.png b/docs/assets/screenshots/embed-views-in-nodes.png index 36fd37f..8529bb0 100644 Binary files a/docs/assets/screenshots/embed-views-in-nodes.png and b/docs/assets/screenshots/embed-views-in-nodes.png differ diff --git a/docs/assets/screenshots/quickstart.png b/docs/assets/screenshots/quickstart.png index b7c03a8..3902463 100644 Binary files a/docs/assets/screenshots/quickstart.png and b/docs/assets/screenshots/quickstart.png differ diff --git a/docs/assets/screenshots/react-to-events.png b/docs/assets/screenshots/react-to-events.png index 6c7861d..2b481ed 100644 Binary files a/docs/assets/screenshots/react-to-events.png and b/docs/assets/screenshots/react-to-events.png differ diff --git a/docs/assets/screenshots/style-nodes-edges.png b/docs/assets/screenshots/style-nodes-edges.png index 151140e..0683678 100644 Binary files a/docs/assets/screenshots/style-nodes-edges.png and b/docs/assets/screenshots/style-nodes-edges.png differ diff --git a/docs/how-to/declare-types.md b/docs/how-to/declare-types.md index a6534d4..371750c 100644 --- a/docs/how-to/declare-types.md +++ b/docs/how-to/declare-types.md @@ -1,24 +1,142 @@ # Declare Node & Edge Types -Node and edge types are lightweight descriptors that tell Panel-ReactFlow -**what kind of data a node or edge carries**. A type defines a name, an -optional display label, optional input/output ports (for nodes), and an -optional JSON Schema for its `data` payload. +Node and edge types are lightweight descriptors that define **what data each +kind of node/edge carries**. A type can provide: -Types are separate from editors. A type says "a *task* node has a -*status* string and a *priority* integer"; an editor says "render a -dropdown and a number input for those fields." This separation lets you -reuse the same type with different editors, or rely on the auto-generated -form. +- a type name (`type`) +- a display label (`label`) +- node handles (`inputs` / `outputs`) +- a schema for the `data` payload (`schema`) + +Types are separate from editors. A type defines structure; an editor defines +the UI used to edit it. ![Screenshot: multiple node types with different schemas](../assets/screenshots/declare-types.png) --- -## Node types +## Complete runnable example + +This script is a minimal, working example that produces the visualization +shown above. + +```python +import param +import panel as pn + +from panel_reactflow import EdgeType, NodeType, ReactFlow + +pn.extension("jsoneditor") + + +class Job(param.Parameterized): + status = param.Selector(objects=["idle", "running", "done"]) + retries = param.Integer(default=0) + + +decision_schema = { + "type": "object", + "properties": { + "question": {"type": "string", "title": "Question"}, + "outcome": { + "type": "string", + "enum": ["yes", "no", "maybe"], + "title": "Outcome", + }, + }, +} + +node_types = { + "job": NodeType(type="job", label="Job", schema=Job, inputs=["in"], outputs=["out"]), + "decision": NodeType( + type="decision", + label="Decision", + schema=decision_schema, + inputs=["in"], + outputs=["yes", "no"], + ), +} + +edge_types = { + "flow": EdgeType( + type="flow", + label="Flow", + schema={ + "type": "object", + "properties": {"weight": {"type": "number", "title": "Weight"}}, + }, + ), +} + +nodes = [ + { + "id": "j1", + "type": "job", + "label": "Fetch Data", + "position": {"x": 0, "y": 0}, + "data": {"status": "idle", "retries": 0}, + }, + { + "id": "d1", + "type": "decision", + "label": "Valid?", + "position": {"x": 300, "y": 250}, + "data": {"question": "Is data valid?", "outcome": "yes"}, + }, + { + "id": "j2", + "type": "job", + "label": "Process", + "position": {"x": 600, "y": 400}, + "data": {"status": "running", "retries": 1}, + }, +] + +edges = [ + {"id": "e1", "source": "j1", "target": "d1", "type": "flow", "data": {"weight": 1.0}}, + {"id": "e2", "source": "d1", "target": "j2", "type": "flow", "data": {"weight": 0.8}}, +] + +TASK_NODE_CSS = """ +.react-flow__node-job { + background-color: white; + border-radius: 8px; + border: 1.5px solid #7c3aed; +} + +.react-flow__node-decision { + background-color: white; + border-radius: 8px; + border: 1.5px solid green; +} +""" + +flow = ReactFlow( + nodes=nodes, + edges=edges, + node_types=node_types, + edge_types=edge_types, + editor_mode="node", + sizing_mode="stretch_both", + stylesheets=[TASK_NODE_CSS] +) + +pn.Column(flow, sizing_mode="stretch_both").servable() +``` + +## How this code maps to the visualization + +- `node_types["job"]` and `node_types["decision"]` define the two node kinds you see. +- `inputs` and `outputs` define the left/right handles rendered on each node. +- `edge_types["flow"]` defines the edge payload schema used by both connections. +- `nodes` controls labels (`Fetch Data`, `Valid?`, `Process`) and positions. +- `editor_mode="side"` makes selection open the schema-driven editor in the right panel. + +--- + +## Node type snippet -Use `NodeType` to describe a node type. Provide `inputs` and `outputs` to -control the handles (ports) shown on each side of the node. +Use `NodeType` to define node handles and payload schema. ```python from panel_reactflow import NodeType @@ -42,12 +160,9 @@ node_types = { } ``` ---- - -## Edge types +## Edge type snippet -Use `EdgeType` to describe an edge type. Edges with a schema get the same -auto-generated editor support as nodes. +Use `EdgeType` for edge payload schema and label. ```python from panel_reactflow import EdgeType @@ -71,8 +186,7 @@ edge_types = { ## Schema sources -The `schema` field accepts multiple formats. All are normalized to -JSON Schema before being sent to the frontend or used by editors. +The `schema` field accepts multiple inputs and normalizes them to JSON Schema. | Source | Example | |--------|---------| @@ -108,9 +222,9 @@ node_types = {"config": NodeType(type="config", label="Config", schema=Config)} --- -## Register on ReactFlow +## Register on `ReactFlow` -Pass types as dictionaries keyed by type name. +Pass `node_types` and `edge_types` as dictionaries keyed by type name: ```python flow = ReactFlow( @@ -121,5 +235,5 @@ flow = ReactFlow( ) ``` -Types without a schema still work — the node or edge simply has no -schema-driven validation or auto-generated form. +Types without a schema still work; they just do not get schema-driven +validation or auto-generated forms. diff --git a/docs/how-to/define-editors.md b/docs/how-to/define-editors.md index 06101b3..67dec14 100644 --- a/docs/how-to/define-editors.md +++ b/docs/how-to/define-editors.md @@ -18,6 +18,65 @@ or falls back to a raw JSON editor. --- +## Complete runnable example (node editor screenshot) + +This script is a minimal, working example for the screenshot above. Run it, +then click the `Start` node to open the schema-driven editor in the side panel. + +```python +import panel as pn + +from panel_reactflow import NodeType, ReactFlow + +pn.extension("jsoneditor") + +task_schema = { + "type": "object", + "properties": { + "status": {"type": "string", "enum": ["idle", "running", "done"], "title": "Status"}, + "priority": {"type": "integer", "title": "Priority"}, + "notes": {"type": "string", "title": "Notes"}, + }, +} + +nodes = [ + { + "id": "start", + "type": "task", + "label": "Start", + "position": {"x": 0, "y": 0}, + "data": {"status": "idle", "priority": 1, "notes": ""}, + }, + { + "id": "finish", + "type": "task", + "label": "Finish", + "position": {"x": 300, "y": 80}, + "data": {"status": "done", "priority": 2, "notes": "All clear"}, + }, +] + +edges = [{"id": "e1", "source": "start", "target": "finish"}] + +flow = ReactFlow( + nodes=nodes, + edges=edges, + node_types={"task": NodeType(type="task", label="Task", schema=task_schema)}, + editor_mode="side", + sizing_mode="stretch_both", +) + +pn.Column(flow, sizing_mode="stretch_both").servable() +``` + +## How this code maps to the node-editor screenshot + +- `task_schema` defines fields rendered in the side-panel form. +- `node_types={"task": ...}` binds that schema to both `task` nodes. +- Clicking a node selects it and opens its editor because `editor_mode="side"`. + +--- + ## Editor signature Every editor — whether a simple function, a lambda, or a class — receives @@ -106,6 +165,57 @@ edge type) or `default_edge_editor` for a blanket default. ![Screenshot: an edge editor open in the side panel](../assets/screenshots/define-editors-edge.png) +### Complete runnable example (edge editor screenshot) + +This script reproduces the edge-editor screenshot. Run it, then click the +`pipe` edge to open its schema-driven editor. + +```python +import panel as pn + +from panel_reactflow import EdgeType, NodeType, ReactFlow + +pn.extension("jsoneditor") + +pipe_schema = { + "type": "object", + "properties": { + "throughput": {"type": "number", "title": "Throughput"}, + "protocol": { + "type": "string", + "enum": ["tcp", "udp", "http"], + "title": "Protocol", + }, + }, +} + +nodes = [ + {"id": "src", "type": "device", "label": "Source", "position": {"x": 0, "y": 0}, "data": {}}, + {"id": "sink", "type": "device", "label": "Sink", "position": {"x": 400, "y": 0}, "data": {}}, +] + +edges = [ + { + "id": "e1", + "source": "src", + "target": "sink", + "type": "pipe", + "label": "pipe", + "data": {"throughput": 100.0, "protocol": "tcp"}, + }, +] + +flow = ReactFlow( + nodes=nodes, + edges=edges, + node_types={"device": NodeType(type="device", label="Device")}, + edge_types={"pipe": EdgeType(type="pipe", label="Pipe", schema=pipe_schema)}, + sizing_mode="stretch_both", +) + +pn.Column(flow, sizing_mode="stretch_both").servable() +``` + ### Schema-driven edge editor If you declare an `EdgeType` with a schema and do not provide an explicit diff --git a/docs/how-to/define-nodes-edges.md b/docs/how-to/define-nodes-edges.md index 8a3b69b..a062a56 100644 --- a/docs/how-to/define-nodes-edges.md +++ b/docs/how-to/define-nodes-edges.md @@ -13,6 +13,58 @@ and update data after the graph is live. --- +## Complete runnable example + +This script is a minimal, working example that produces the visualization +shown above. + +```python +import panel as pn + +from panel_reactflow import ReactFlow + +pn.extension("jsoneditor") + +nodes = [ + { + "id": "n1", + "type": "panel", + "label": "Start", + "position": {"x": 0, "y": 0}, + "data": {"status": "idle"}, + "view": pn.pane.Markdown("Optional node body"), + }, + { + "id": "n2", + "type": "panel", + "label": "End", + "position": {"x": 300, "y": 80}, + "data": {"status": "done"}, + }, +] + +edges = [ + {"id": "e1", "source": "n1", "target": "n2", "label": "next"}, +] + +flow = ReactFlow( + nodes=nodes, + edges=edges, + sizing_mode="stretch_both", +) + +pn.Column(flow, sizing_mode="stretch_both").servable() +``` + +## How this code maps to the visualization + +- `nodes` defines the two boxes (`Start`, `End`) and where they appear. +- `edges` defines the single connection labeled `next`. +- `view` on `n1` adds inline content inside that node. +- `ReactFlow(nodes=..., edges=...)` renders the graph from those lists. + +--- + ## Define nodes A node dict requires `id`, `position`, and `data`. The display label is a diff --git a/docs/how-to/embed-views-in-nodes.md b/docs/how-to/embed-views-in-nodes.md index 242ef23..d4e6711 100644 --- a/docs/how-to/embed-views-in-nodes.md +++ b/docs/how-to/embed-views-in-nodes.md @@ -15,6 +15,74 @@ thumbnail on an image-processing node. --- +## Complete runnable example + +This script is a minimal, working example that produces the visualization +shown above. + +```python +import panel as pn + +from panel_reactflow import ReactFlow + +pn.extension("jsoneditor") + +nodes = [ + { + "id": "source", + "type": "panel", + "label": "Data Source", + "position": {"x": 0, "y": 0}, + "data": {}, + "view": pn.pane.Markdown("**Status:** connected\n\nRows: 1,204 • Updated 3s ago"), + }, + { + "id": "metric", + "type": "panel", + "label": "KPI", + "position": {"x": 320, "y": 0}, + "data": {}, + "view": pn.indicators.Number( + value=87.3, + name="Accuracy", + format="{value}%", + colors=[(90, "orange"), (100, "green")], + ), + }, + { + "id": "output", + "type": "panel", + "label": "Output", + "position": {"x": 160, "y": 180}, + "data": {}, + "view": pn.pane.HTML( + "
✅ Pipeline healthy
" + ), + }, +] + +edges = [ + {"id": "e1", "source": "source", "target": "metric"}, + {"id": "e2", "source": "metric", "target": "output"}, +] + +flow = ReactFlow( + nodes=nodes, + edges=edges, + sizing_mode="stretch_both", +) + +pn.Column(flow, sizing_mode="stretch_both").servable() +``` + +## How this code maps to the visualization + +- Each node has a different `view` (`Markdown`, `Number`, `HTML`). +- Those views are rendered inline inside the three visible nodes. +- `edges` creates the two visible connections between those nodes. + +--- + ## Add a view to a node Set the `view` key when defining a node. The value can be any Panel diff --git a/docs/quickstart.md b/docs/quickstart.md index cf1198e..2e07e02 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -63,9 +63,3 @@ flow.servable() ```bash panel serve app.py --show ``` - -## What you just used - -- `label` is a top-level node field (not part of `data`). -- `node_types` describe structure and schema only. -- The default editor auto-generates widgets from the JSON Schema. diff --git a/src/panel_reactflow/dist/css/reactflow.css b/src/panel_reactflow/dist/css/reactflow.css index b911015..5872012 100644 --- a/src/panel_reactflow/dist/css/reactflow.css +++ b/src/panel_reactflow/dist/css/reactflow.css @@ -35,8 +35,8 @@ .rf-node-toolbar-button { position: absolute; - right: 0.5em; - top: 0.5em; + right: 0.25em; + top: 0.25em; border: none; background: transparent; font-size: 17px; diff --git a/src/panel_reactflow/models/reactflow.jsx b/src/panel_reactflow/models/reactflow.jsx index 54da2e7..652f2e9 100644 --- a/src/panel_reactflow/models/reactflow.jsx +++ b/src/panel_reactflow/models/reactflow.jsx @@ -621,10 +621,9 @@ export function render({ model, view }) { const typeSpec = allNodeTypes[node.type] || {}; const realKeys = Object.keys(data).filter((k) => k !== "view_idx"); const hasEditor = realKeys.length > 0 || !!typeSpec.schema; - console.log(node) return { ...node, - className: (node.type === "panel" || model.stylesheets) ? "" : "react-flow__node-default", + className: (node.type === "panel" || model.stylesheets.length > 7) ? "" : "react-flow__node-default", data: { ...data, view: baseView,