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
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.display_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
52 changes: 52 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,58 @@ 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.display_widget`. It renders the same interactive widget as
`render_widget` does in a notebook and syncs the `selected` and `options` state
back into the `GraphWidget` you pass in (in place) 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 display_widget

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

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

`display_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`.

== 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.
4 changes: 2 additions & 2 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
118 changes: 71 additions & 47 deletions examples/streamlit-example.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,93 @@
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 display_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.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")


VG = create_visualization_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."
"This is an example of how to use Streamlit with the Graph Visualization for Python library by Neo4j."
)


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


small_graph = create_small_graph()

# 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
)

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

selection = graph_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