Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ jobs:
environment: ["test-ui"]
timeout-minutes: 60
env:
PYTHONIOENCODING: utf8
PYTHONUTF8: 1
PANEL_LOG_LEVEL: info
FAIL: "--screenshot only-on-failure --full-page-screenshot --output ui_screenshots --tracing retain-on-failure"
steps:
Expand Down
112 changes: 112 additions & 0 deletions docs/how-to/style-nodes-edges.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,97 @@ Python logic and lets you iterate on visuals without restarting the server.

---

## 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 EdgeType, NodeType, ReactFlow

pn.extension("jsoneditor")

TASK_NODE_CSS = """
.react-flow__node-task {
border-radius: 8px;
border: 1.5px solid #7c3aed;
background: linear-gradient(168deg, #faf5ff 0%, #ffffff 60%);
box-shadow: 0 1px 3px rgba(124, 58, 237, 0.10);
min-width: 160px;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.react-flow__node-task.selectable:hover {
border-color: #6d28d9;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.18);
transform: translateY(-1px);
}
.react-flow__node-task.selected {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.25),
0 4px 14px rgba(124, 58, 237, 0.15);
}
"""

TYPED_EDGE_CSS = """
.react-flow__edge-pipe .react-flow__edge-path {
stroke: #2563eb;
stroke-width: 2.5px;
}
.react-flow__edge-signal .react-flow__edge-path {
stroke: #dc2626;
stroke-width: 2px;
stroke-dasharray: 6 3;
}
.react-flow__edge-text {
fill: #475569;
font-size: 12px;
}
"""

nodes = [
{"id": "n1", "type": "task", "label": "Ingest", "position": {"x": 0, "y": 0}, "data": {"status": "idle"}},
{"id": "n2", "type": "task", "label": "Transform", "position": {"x": 280, "y": 0}, "data": {"status": "running"}},
{"id": "n3", "type": "task", "label": "Export", "position": {"x": 560, "y": 0}, "data": {"status": "done"}},
]

edges = [
{"id": "e1", "source": "n1", "target": "n2", "type": "pipe", "label": "pipe", "data": {}},
{"id": "e2", "source": "n2", "target": "n3", "type": "signal", "label": "signal", "data": {}},
]

task_schema = {
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["idle", "running", "done"]},
},
}

flow = ReactFlow(
nodes=nodes,
edges=edges,
node_types={"task": NodeType(type="task", label="Task", schema=task_schema)},
edge_types={
"pipe": EdgeType(type="pipe", label="Pipe"),
"signal": EdgeType(type="signal", label="Signal"),
},
stylesheets=[TASK_NODE_CSS, TYPED_EDGE_CSS],
sizing_mode="stretch_both",
)

pn.Column(flow, sizing_mode="stretch_both").servable()
```

## How this code maps to the visualization

- `nodes` and `edges` define the same three-node, two-edge layout.
- `.react-flow__node-task` styles all task nodes (background, border, shadow).
- `.react-flow__edge-pipe` and `.react-flow__edge-signal` style each edge type differently.
- `stylesheets=[TASK_NODE_CSS, TYPED_EDGE_CSS]` applies the custom CSS.

---

## How CSS classes are assigned

| Element | Class pattern | Example |
Expand Down Expand Up @@ -120,6 +211,25 @@ flow = ReactFlow(

---

## Color mode and theme integration

`ReactFlow` exposes a `color_mode` parameter (`"light"` or `"dark"`). If you
do not set it explicitly, it defaults to the current `pn.config.theme` when
the component is created.

```python
flow = ReactFlow(
nodes=nodes,
edges=edges,
color_mode="dark", # optional override
)
```

When using `panel-material-ui`, theme toggles are reflected automatically, so
the graph updates along with the active light/dark theme.

---

## Style selected elements

The `.selected` class is added automatically when a user clicks a node or
Expand All @@ -145,5 +255,7 @@ SELECTION_CSS = """
- Use CSS `transition` for smooth hover and selection effects.
- Scope styles to a node or edge type to keep visuals consistent across
instances of the same type.
- If you define a custom node type and do not provide `stylesheets`, nodes fall
back to the default node styling (`.react-flow__node-default`).
- For rapid prototyping, define styles as inline Python strings. For
production apps, move them to `.css` files and reference by path.
8 changes: 8 additions & 0 deletions src/panel_reactflow/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,12 @@ class ReactFlow(ReactComponent):
default_node_editor = param.Parameter(default=None, doc="Default node editor factory.", precedence=-1)
default_edge_editor = param.Parameter(default=None, doc="Default edge editor factory.", precedence=-1)

color_mode = param.ObjectSelector(default=None, objects=["light", "dark"], doc="Color mode for the graph.")

allow_edge_loops = param.Boolean(default=False, doc="Allow to have edge loops in the graph (can lead to update infinite loops).")

display_side_bar = param.Boolean(default=True, doc="Display the side bar to drag and drop new nodes.")

debounce_ms = param.Integer(default=150, bounds=(0, None), doc="Debounce delay in milliseconds when sync_mode='debounce'.")

default_edge_options = param.Dict(default={}, doc="Default React Flow edge options.")
Expand Down Expand Up @@ -1211,6 +1217,8 @@ class ReactFlow(ReactComponent):
_render_policy = "manual"

def __init__(self, **params: Any):
if "color_mode" not in params:
params["color_mode"] = "dark" if pn.config.theme == "dark" else "light"
self._node_ids: list[str] = []
self._edge_ids: list[str] = []
# Normalize type specs before parent init so the frontend receives
Expand Down
21 changes: 13 additions & 8 deletions src/panel_reactflow/dist/css/reactflow.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
.react-flow__node-minimal {
padding: 0;
border-radius: 6px;
border: 1px solid var(--xy-node-border, #d0d7de);
background-color: var(--xy-node-background-color, #ffffff);
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08);
color: var(--xy-node-color, #101828);
border: 1px solid var(--xy-node-border, var(--panel-border-color));
background-color: var(--xy-node-background-color, var(--panel-background-color));
box-shadow: 0 1px 2px var(--panel-shadow-color);
color: var(--xy-node-color, var(--panel-on-background-color));
font-size: 13px;
min-width: 140px;
}
.react-flow__node-default {
text-align: left;
width: unset;
}
.react-flow__node-panel.selectable:hover,
.react-flow__node-default.selectable:hover,
.react-flow__node-minimal.selectable:hover {
Expand All @@ -18,7 +22,7 @@
.react-flow__node-panel.selected,
.react-flow__node-default.selected,
.react-flow__node-minimal.selected {
border-color: var(--xy-node-border-selected, #2684ff);
border-color: var(--xy-node-border-selected, var(--panel-primary-color));
box-shadow: 0 0 0 1px rgba(38, 132, 255, 0.35);
}

Expand All @@ -30,21 +34,22 @@
}

.rf-node-toolbar-button {
float: right;
position: absolute;
right: 0.5em;
top: 0.5em;
border: none;
background: transparent;
font-size: 17px;
line-height: 18px;
cursor: pointer;
z-index: 2;
padding: 0;
margin: 0 0 0 6px;
transition: color 0.1s;
}

.rf-node-toolbar-button--open {
color: #3477db;
filter: drop-shadow(0 0 1px #3477db) brightness(1.15);
filter: drop-shadow(0 0 1px var(--panel-primary-color)) brightness(1.15);
}

.rf-node-toolbar-button--closed {
Expand Down
8 changes: 7 additions & 1 deletion src/panel_reactflow/models/reactflow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function makeNodeComponent(typeName, typeSpec, editorMode) {


const applyFigureStyles = async () => {
const views = [...Bokeh.index.find_by_id(data.view.key)]
const views = [...Bokeh.index.find_by_id(data.view?.key)]
if (!views.length) {
return;
}
Expand Down Expand Up @@ -228,6 +228,7 @@ function FlowInner({
nodeTypes,
nodeEditors,
edgeEditors,
colorMode,
editable,
enableConnect,
enableDelete,
Expand Down Expand Up @@ -471,6 +472,7 @@ function FlowInner({
edges={edges}
nodeTypes={nodeTypes}
defaultEdgeOptions={defaultEdgeOptions}
colorMode={colorMode}
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onSelectionChange={onSelectionChange}
Expand Down Expand Up @@ -503,6 +505,7 @@ export function render({ model, view }) {
const [defaultEdgeOptions] = model.useState("default_edge_options");
const [selection, setSelection] = model.useState("selection");
const [syncMode] = model.useState("sync_mode");
const [colorMode] = model.useState("color_mode");
const [debounceMs] = model.useState("debounce_ms");
const [editable] = model.useState("editable");
const [editorMode] = model.useState("editor_mode");
Expand Down Expand Up @@ -618,8 +621,10 @@ 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",
data: {
...data,
view: baseView,
Expand Down Expand Up @@ -664,6 +669,7 @@ export function render({ model, view }) {
views={views}
viewportSetter={setViewport}
defaultEdgeOptions={defaultEdgeOptions}
colorMode={colorMode}
nodeTypes={hydratedNodeTypes}
nodeEditors={nodeEditors}
edgeEditors={edgeEditors}
Expand Down
Loading