Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## New features

* Added a color legend overlay to the visualization. It is captured automatically from `color_nodes`/`color_relationships`, can be set explicitly via `set_legend`, and toggled via `show_legend`.
* Added `neo4j_viz.streamlit.render_widget` to embed an interactive `GraphWidget` in a Streamlit app with two-way state sync (selection and options flow back to Python), following the Streamlit light/dark theme. Install with the `streamlit` extra (`pip install neo4j-viz[streamlit]`).

## Bug fixes

Expand Down
62 changes: 62 additions & 0 deletions docs/antora/modules/ROOT/pages/rendering.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,68 @@ But you can disable this by passing `show_hover_tooltip=False`.
Refer to the xref:getting-started.adoc[] and the link:{tutorials-docs-uri}[tutorials] for
examples of `render` method usage.

== Using with Streamlit

There are two ways to display a graph in a https://streamlit.io/[Streamlit] app.

=== Static rendering

The HTML object returned by `render` can be embedded directly. Pass its `data`
attribute to `st.iframe`:

[source, python]
----
import streamlit as st
from neo4j_viz import VisualizationGraph

st.iframe(VG.render(height="600px").data, height=600)
----

This is the simplest option and fully supports the in-canvas interactions
(pan, zoom, selection, layout switching), but nothing is communicated back to
Python.

=== Interactive widget with two-way sync

To read interactions such as the current selection back in Python, use
`neo4j_viz.streamlit.render_widget`. It renders the same interactive widget as
`render_widget` does in a notebook and syncs the `selected` and `options` state
back into the returned `GraphWidget` on each rerun.

This requires the `streamlit` optional dependency (Streamlit 1.58 or newer):

[source, bash]
----
pip install neo4j-viz[streamlit]
----

[source, python]
----
import streamlit as st
from neo4j_viz.streamlit import render_widget

widget = render_widget(VG.render_widget(), key="my-graph")

selection = widget.selected
st.write("Selected node IDs:", selection.nodeIds)
----

`render_widget` takes a `GraphWidget` (as returned by
`VisualizationGraph.render_widget()`), so you can configure the widget with all
the usual options (`layout`, `initial_zoom`, `height`, ...) when building it.
When displaying more than one graph in an app, give each a unique `key`.

[NOTE]
====
Streamlit does not support the Jupyter/anywidget comm protocol, so a
`GraphWidget` cannot be embedded directly (nor via the unmaintained
`streamlit-anywidget` bridge, which ignores the widget's custom traitlets
serializers). `render_widget` bridges the widget as a Streamlit
`Components v2` component instead. The graph follows the Streamlit light/dark
theme automatically; pass `theme="light"` or `theme="dark"` to
`VisualizationGraph.render_widget()` to pin it.
====

== Exporting to HTML

The object returned by the `render` method is a `IPython.display.HTML` object.
Expand Down
Binary file removed examples/datasets/cora/cora_nodes.parquet.gzip
Binary file not shown.
Binary file removed examples/datasets/cora/cora_rels.parquet.gzip
Binary file not shown.
27 changes: 19 additions & 8 deletions examples/getting-started.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "50baeb1722b94ae58cd6604cb9f4257d",
"model_id": "ef711de91aae4e4994fefd733268edda",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<neo4j_viz.widget.GraphWidget object at 0x10c4b0ce0>"
"<neo4j_viz.widget.GraphWidget object at 0x10add3da0>"
]
},
"execution_count": 1,
Expand Down Expand Up @@ -126,7 +126,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 2,
"id": "652ybsznrab",
"metadata": {},
"outputs": [],
Expand All @@ -145,7 +145,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 3,
"id": "686e0beb",
"metadata": {},
"outputs": [],
Expand All @@ -155,7 +155,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 4,
"id": "c68712f0",
"metadata": {},
"outputs": [],
Expand All @@ -166,7 +166,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 5,
"id": "f174b6ed00027bf5",
"metadata": {},
"outputs": [],
Expand All @@ -188,10 +188,21 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 6,
"id": "1fc58d6d",
"metadata": {},
"outputs": [],
"outputs": [
{
"data": {
"text/plain": [
"<function neo4j_viz.widget.GraphWidget.on_selection_change.<locals>.handler(change: 'dict[str, Any]') -> 'None'>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Run this cell once to register the callback, then select a node in the widget above.\n",
"def on_selection_change(selection: GraphSelection) -> None:\n",
Expand Down
94 changes: 48 additions & 46 deletions examples/streamlit-example.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,71 @@
import streamlit as st
from IPython.display import HTML
import streamlit.components.v1 as components
from pandas import read_parquet
import pathlib
import random

import streamlit as st

from neo4j_viz.pandas import from_dfs
from neo4j_viz import VisualizationGraph
from neo4j_viz import Node, Relationship, VisualizationGraph
from neo4j_viz.streamlit import render_widget

# Path to this file
script_path = pathlib.Path(__file__).resolve()
script_dir_path = pathlib.Path(__file__).parent.resolve()


@st.cache_data
def create_visualization_graph() -> VisualizationGraph:
cora_nodes_path = f"{script_dir_path}/datasets/cora/cora_nodes.parquet.gzip"
cora_rels_path = f"{script_dir_path}/datasets/cora/cora_rels.parquet.gzip"

nodes_df = read_parquet(cora_nodes_path)
nodes_df = nodes_df.rename(columns={"nodeId": "id"})

rels_df = read_parquet(cora_rels_path)
rels_df = rels_df.rename(
columns={"sourceNodeId": "source", "targetNodeId": "target"}
)

# Drop the features column since it's not needed for visualization
# Also numpy arrays are not supported by the visualization library
nodes_df.drop(columns="features", inplace=True)

VG = from_dfs(nodes_df, rels_df)
VG.color_nodes(property="subject")

return VG
st.title("Neo4j Viz Streamlit Example")
st.text("This is an example of how to use Streamlit with the Graph Visualization for Python library by Neo4j.")


@st.cache_data
def render_graph(
_VG: VisualizationGraph, height: int, initial_zoom: float = 0.1
) -> HTML:
return VG.render(initial_zoom=initial_zoom, height=f"{height}px")
def create_small_graph() -> VisualizationGraph:
people = [("Alice", "Engineer"), ("Bob", "Designer"), ("Carol", "Engineer"), ("Dan", "Manager")]
nodes = [Node(id=str(i), caption=name, properties={"role": role}) for i, (name, role) in enumerate(people)]
relationships = [
Relationship(source="0", target="1", caption="KNOWS"),
Relationship(source="1", target="2", caption="KNOWS"),
Relationship(source="2", target="3", caption="KNOWS"),
]
vg = VisualizationGraph(nodes=nodes, relationships=relationships)
# Coloring by a property populates the legend overlay shown in the widget.
vg.color_nodes(property="role")
return vg


VG = create_visualization_graph()
small_graph = create_small_graph()

st.title("Neo4j Viz Streamlit Example")
st.text(
"This is an example of how to use Streamlit with the Graph "
"Visualization for Python library by Neo4j."
)
# Nodes/relationships added at runtime, kept in session state so they survive reruns.
if "added_nodes" not in st.session_state:
st.session_state.added_nodes = []
st.session_state.added_relationships = []

with st.sidebar:
height = st.slider("Height in pixels", 100, 2000, 600, 50)
if st.button("Add random node"):
existing_ids = [n.id for n in small_graph.nodes] + [n.id for n in st.session_state.added_nodes]
new_node = Node(id=f"added-{len(st.session_state.added_nodes)}", caption="New", size=20)
st.session_state.added_nodes.append(new_node)
# Link the new node to a random existing one.
st.session_state.added_relationships.append(
Relationship(source=new_node.id, target=random.choice(existing_ids), caption="LINKS_TO")
)
show_code = st.checkbox("Show code")

st.header("Visualization")
st.header("Interactive widget")
st.text(
"A visualization of the famous Cora citation network. Each of its "
"seven scientific subjects is represented by a different color."
"A small graph rendered as an interactive widget, colored by role (see the "
"legend overlay). Selecting nodes or relationships in the graph syncs the "
"selection back to Python, and Python can push data changes back to the graph "
"— use the sidebar button to watch a new node appear and link into the graph."
)

components.html(
render_graph(VG, height=height).data,
height=height,
)
# Build the widget from the small graph plus any runtime additions.
graph_widget = small_graph.render_widget(height=f"{height}px")
if st.session_state.added_nodes:
graph_widget.add_data(st.session_state.added_nodes, st.session_state.added_relationships)

widget = render_widget(graph_widget, key="small-graph-widget")

selection = widget.selected
st.write(f"Selected {len(selection.nodeIds)} node(s) and {len(selection.relationshipIds)} relationship(s).")
if selection.nodeIds:
st.write("Selected node IDs:", selection.nodeIds)

if show_code:
st.header("Code")
Expand Down
3 changes: 2 additions & 1 deletion js-applet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"dev:widget": "vite build --watch",
"dev:html": "vite build --watch --config vite.config.html.ts",
"dev:jupyter": "cd ../python-wrapper && ANYWIDGET_HMR=1 uv run jupyter lab ../examples/",
"build": "tsc && vite build && vite build --config vite.config.html.ts",
"build": "tsc && vite build && vite build --config vite.config.html.ts && npm run build:streamlit",
"build:widget": "vite build",
"build:html": "vite build --config vite.config.html.ts",
"build:streamlit": "vite build --config vite.config.streamlit.ts",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
Loading
Loading