diff --git a/MANIFEST.in b/MANIFEST.in
index 7e69755c..38bd6261 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1 @@
-include examples/example_graph.svg
+include examples/example_graph.png
diff --git a/README.md b/README.md
index ff059467..35e6011f 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Alternatively, you can export the output to a file and view it in a web browser.
The package wraps the [Neo4j Visualization JavaScript library (NVL)](https://neo4j.com/docs/nvl/current/).
-
+
## Some notable features
diff --git a/docs/antora/modules/ROOT/images/getting-started-graph-colored.svg b/docs/antora/modules/ROOT/images/getting-started-graph-colored.svg
deleted file mode 100644
index 06349b2f..00000000
--- a/docs/antora/modules/ROOT/images/getting-started-graph-colored.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/antora/modules/ROOT/images/getting-started-graph.svg b/docs/antora/modules/ROOT/images/getting-started-graph.svg
deleted file mode 100644
index a9a4ec97..00000000
--- a/docs/antora/modules/ROOT/images/getting-started-graph.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/antora/modules/ROOT/images/getting-started-widget-colored.png b/docs/antora/modules/ROOT/images/getting-started-widget-colored.png
new file mode 100644
index 00000000..d1222773
Binary files /dev/null and b/docs/antora/modules/ROOT/images/getting-started-widget-colored.png differ
diff --git a/docs/antora/modules/ROOT/images/getting-started-widget.png b/docs/antora/modules/ROOT/images/getting-started-widget.png
new file mode 100644
index 00000000..0b90b40d
Binary files /dev/null and b/docs/antora/modules/ROOT/images/getting-started-widget.png differ
diff --git a/docs/antora/modules/ROOT/pages/getting-started.adoc b/docs/antora/modules/ROOT/pages/getting-started.adoc
index eab70ccc..ac0fd2b8 100644
--- a/docs/antora/modules/ROOT/pages/getting-started.adoc
+++ b/docs/antora/modules/ROOT/pages/getting-started.adoc
@@ -48,15 +48,15 @@ VG.render(initial_zoom=2)
[TIP]
====
-See the link:{tutorials-docs-uri}/getting-started/[Jupyter notebook] to interact with the graph.
+See the link:{tutorials-docs-uri}/getting-started/[Getting started tutorial] to interact with the graph.
====
-image::getting-started-graph.svg["Graph"]
+image::getting-started-widget.png["The graph rendered as an interactive widget"]
As we can see in the graph above, the radius of one of the nodes is larger than the others.
This is because we set the “size” field of the node to 20, while the others are set to 10.
-At this time all nodes have the same color.
+At this time the nodes are colored using the default colors.
If we want to distinguish between the different types of nodes, we can color them differently with the `color_nodes` method.
We can pass the field we want to use to color the nodes as an argument.
In this case, we will use the "caption" field.
@@ -72,10 +72,10 @@ VG.render(initial_zoom=2)
[TIP]
====
-See the link:{tutorials-docs-uri}/getting-started/[Jupyter notebook] to interact with the graph.
+See the link:{tutorials-docs-uri}/getting-started/[Getting started tutorial] to interact with the graph.
====
-image::getting-started-graph-colored.svg["Graph with colors"]
+image::getting-started-widget-colored.png["The graph with nodes colored by caption"]
We are now easily able to distinguish between the different types of nodes in the graph.
diff --git a/docs/antora/modules/ROOT/pages/installation.adoc b/docs/antora/modules/ROOT/pages/installation.adoc
index d574ee60..e1eb2c4a 100644
--- a/docs/antora/modules/ROOT/pages/installation.adoc
+++ b/docs/antora/modules/ROOT/pages/installation.adoc
@@ -18,7 +18,7 @@ Each integration page includes installation instructions for the needed dependen
=== Notebook tutorials
-You need to install some additional dependencies to run the link:{tutorials-docs-uri}[Jupyter notebook tutorials]:
+You need to install some additional dependencies to run the link:{tutorials-docs-uri}[tutorials]:
[source, console]
----
diff --git a/docs/source/tutorials/gds-example.nblink b/docs/source/tutorials/gds-example.nblink
index 27f52967..b4e7691c 100644
--- a/docs/source/tutorials/gds-example.nblink
+++ b/docs/source/tutorials/gds-example.nblink
@@ -1,3 +1,6 @@
{
- "path": "../../../examples/gds-example.ipynb"
+ "path": "../../../examples/gds-example.ipynb",
+ "extra-media": [
+ "../../../examples/gds-example-graph.png"
+ ]
}
diff --git a/docs/source/tutorials/getting-started.nblink b/docs/source/tutorials/getting-started.nblink
index 7cbbf076..af3261f3 100644
--- a/docs/source/tutorials/getting-started.nblink
+++ b/docs/source/tutorials/getting-started.nblink
@@ -1,3 +1,7 @@
{
- "path": "../../../examples/getting-started.ipynb"
+ "path": "../../../examples/getting-started.ipynb",
+ "extra-media": [
+ "../../../examples/getting-started-widget.png",
+ "../../../examples/getting-started-widget-colored.png"
+ ]
}
diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst
index bfc22864..d928dcf5 100644
--- a/docs/source/tutorials/index.rst
+++ b/docs/source/tutorials/index.rst
@@ -3,10 +3,10 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
-Jupyter notebooks
+Tutorials
=====================================================
-This chapter contains Jupyter notebook tutorials for the `neo4j-viz` package.
+This chapter contains tutorials for the `neo4j-viz` package.
.. toctree::
diff --git a/docs/source/tutorials/neo4j-example.nblink b/docs/source/tutorials/neo4j-example.nblink
index 014929c0..b38cc33f 100644
--- a/docs/source/tutorials/neo4j-example.nblink
+++ b/docs/source/tutorials/neo4j-example.nblink
@@ -1,3 +1,6 @@
{
- "path": "../../../examples/neo4j-example.ipynb"
+ "path": "../../../examples/neo4j-example.ipynb",
+ "extra-media": [
+ "../../../examples/example_graph.png"
+ ]
}
diff --git a/examples/creds.env.example b/examples/creds.env.example
new file mode 100644
index 00000000..3b6145b3
--- /dev/null
+++ b/examples/creds.env.example
@@ -0,0 +1,22 @@
+# Copy this file to `creds.env` in this directory and fill in your own values.
+# The notebooks load `creds.env` automatically, so you do not have to edit them.
+
+# Neo4j database connection, used by neo4j-example.ipynb and gds-example.ipynb
+NEO4J_URI=neo4j://localhost:7687
+NEO4J_USERNAME=neo4j
+NEO4J_PASSWORD=password
+NEO4J_DB=neo4j
+
+# Aura Graph Analytics, optionally used by gds-example.ipynb to run GDS in Aura
+# instead of a local Neo4j instance with the GDS plugin.
+AURA_INSTANCEID=
+AURA_API_CLIENT_ID=
+AURA_API_CLIENT_SECRET=
+AURA_API_PROJECT_ID=
+
+# Snowflake connection, used by snowflake-example.ipynb
+SNOWFLAKE_ACCOUNT=
+SNOWFLAKE_USER=
+SNOWFLAKE_PASSWORD=
+SNOWFLAKE_ROLE=
+SNOWFLAKE_WAREHOUSE=
diff --git a/examples/example_graph.png b/examples/example_graph.png
new file mode 100644
index 00000000..234ab9cd
Binary files /dev/null and b/examples/example_graph.png differ
diff --git a/examples/example_graph.svg b/examples/example_graph.svg
deleted file mode 100644
index 3c2bce79..00000000
--- a/examples/example_graph.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/examples/example_instruments_1.png b/examples/example_instruments_1.png
index 32fcc537..d0de87ea 100644
Binary files a/examples/example_instruments_1.png and b/examples/example_instruments_1.png differ
diff --git a/examples/example_instruments_2.png b/examples/example_instruments_2.png
index 2d1b9a69..939bce39 100644
Binary files a/examples/example_instruments_2.png and b/examples/example_instruments_2.png differ
diff --git a/examples/gds-example-graph.png b/examples/gds-example-graph.png
new file mode 100644
index 00000000..0c70d490
Binary files /dev/null and b/examples/gds-example-graph.png differ
diff --git a/examples/gds-example.ipynb b/examples/gds-example.ipynb
index c4baedad..cefccf30 100644
--- a/examples/gds-example.ipynb
+++ b/examples/gds-example.ipynb
@@ -5,7 +5,9 @@
"id": "133e0a86",
"metadata": {},
"source": [
- "# Visualizing Neo4j Graph Data Science (GDS) Graphs"
+ "# Visualizing Neo4j Graph Data Science (GDS) Graphs\n",
+ "\n",
+ "[](https://colab.research.google.com/github/neo4j/python-graph-visualization/blob/main/examples/gds-example.ipynb)\n"
]
},
{
@@ -20,6 +22,19 @@
"%pip install python-dotenv"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "7f948daf",
+ "metadata": {},
+ "source": [
+ "### Credentials\n",
+ "\n",
+ "The connection details are read from an environment file named `creds.env`, next to this notebook.\n",
+ "Copy `creds.env.example` to `creds.env` and fill in the URI, username, password, and database of your Neo4j instance, and optionally your Aura Graph Analytics credentials.\n",
+ "\n",
+ "On Colab, upload a `creds.env` file with the same contents to the session, or set the same variables as [secrets](https://colab.research.google.com/notebooks/snippets/secrets.ipynb).\n"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -29,7 +44,8 @@
"source": [
"import dotenv\n",
"\n",
- "dotenv.load_dotenv()"
+ "# Connection details are read from `creds.env` (see above).\n",
+ "dotenv.load_dotenv(\"creds.env\")"
]
},
{
@@ -157,7 +173,7 @@
"metadata": {},
"outputs": [],
"source": [
- "VG.render()"
+ "VG.render_widget()"
]
},
{
@@ -189,7 +205,7 @@
"metadata": {},
"outputs": [],
"source": [
- "VG.render()"
+ "VG.render_widget()"
]
},
{
@@ -210,7 +226,7 @@
"outputs": [],
"source": [
"VG.resize_nodes(property=\"pagerank\")\n",
- "VG.render()"
+ "VG.render_widget()"
]
},
{
@@ -242,7 +258,7 @@
"outputs": [],
"source": [
"VG.color_nodes(property=\"subject\")\n",
- "VG.render()"
+ "VG.render_widget()"
]
},
{
@@ -265,7 +281,7 @@
"from neo4j_viz.colors import ColorSpace\n",
"\n",
"VG.color_nodes(field=\"size\", color_space=ColorSpace.CONTINUOUS, override=True)\n",
- "VG.render()"
+ "VG.render_widget()"
]
},
{
@@ -315,7 +331,15 @@
"metadata": {},
"outputs": [],
"source": [
- "VG.render()"
+ "VG.render_widget()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a2fb5780",
+ "metadata": {},
+ "source": [
+ ""
]
},
{
@@ -343,7 +367,7 @@
"source": [
"from neo4j_viz import Layout\n",
"\n",
- "VG.render(layout=Layout.CIRCULAR, initial_zoom=0.5)"
+ "VG.render_widget(layout=Layout.CIRCULAR, initial_zoom=0.5)"
]
},
{
diff --git a/examples/getting-started-widget-colored.png b/examples/getting-started-widget-colored.png
new file mode 100644
index 00000000..d1222773
Binary files /dev/null and b/examples/getting-started-widget-colored.png differ
diff --git a/examples/getting-started-widget.png b/examples/getting-started-widget.png
new file mode 100644
index 00000000..0b90b40d
Binary files /dev/null and b/examples/getting-started-widget.png differ
diff --git a/examples/getting-started.ipynb b/examples/getting-started.ipynb
index b52fcee7..d2429e10 100644
--- a/examples/getting-started.ipynb
+++ b/examples/getting-started.ipynb
@@ -5,7 +5,9 @@
"id": "1006bb2e58af56e2",
"metadata": {},
"source": [
- "# Getting started"
+ "# Getting started\n",
+ "\n",
+ "[](https://colab.research.google.com/github/neo4j/python-graph-visualization/blob/main/examples/getting-started.ipynb)\n"
]
},
{
@@ -59,13 +61,18 @@
"\n",
"VG = VisualizationGraph(nodes=nodes, relationships=relationships)\n",
"\n",
- "VG.color_nodes(field=\"size\")\n",
- "VG.set_node_captions(field=\"size\")\n",
- "\n",
"widget = VG.render_widget(initial_zoom=2)\n",
"widget"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "69be3030",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ },
{
"cell_type": "markdown",
"id": "38947ca89d48f139",
@@ -74,7 +81,7 @@
"As we can see in the graph above, the radius of one of the nodes is larger than the others.\n",
"This is because we set the \"size\" field of the node to 20, while the others are set to 10.\n",
"\n",
- "At this time all nodes have the same color.\n",
+ "At this time the nodes are colored using the default colors.\n",
"If we want to distinguish between the different types of nodes, we can color them differently with the `color_nodes` method.\n",
"We can pass the field we want to use to color the nodes as an argument.\n",
"In this case, we will use the \"caption\" field.\n",
@@ -82,6 +89,24 @@
"We will use the default colorscheme, which is the Neo4j colorscheme.\n"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8a552cdc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "widget.color_nodes(field=\"caption\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c61b6779",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ },
{
"cell_type": "markdown",
"id": "28e00bdb7c99a5db",
diff --git a/examples/neo4j-example.ipynb b/examples/neo4j-example.ipynb
index c55498e0..3b4789fd 100644
--- a/examples/neo4j-example.ipynb
+++ b/examples/neo4j-example.ipynb
@@ -2,24 +2,30 @@
"cells": [
{
"cell_type": "markdown",
- "id": "0d3ffc27",
+ "id": "168b38b2",
"metadata": {},
"source": [
- "# Visualizing Neo4j Graphs"
+ "# Visualizing Neo4j Graphs\n",
+ "\n",
+ "[](https://colab.research.google.com/github/neo4j/python-graph-visualization/blob/main/examples/neo4j-example.ipynb)\n",
+ "\n",
+ "This tutorial shows how to import a graph from a Neo4j database and visualize it as an interactive widget with the `neo4j-viz` library.\n"
]
},
{
"cell_type": "markdown",
- "id": "6b83277d",
+ "id": "ab107b04",
"metadata": {},
"source": [
- "Below is a basic connection to a Neo4j database. We use the `Result.graph` result transformer to map the result to a graph object and store the result in `result`."
+ "## Setup\n",
+ "\n",
+ "Install the dependencies used in this tutorial:"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "39e8a71b",
+ "id": "b9c127db",
"metadata": {},
"outputs": [],
"source": [
@@ -30,16 +36,21 @@
},
{
"cell_type": "markdown",
- "id": "d8e503a3cdf26557",
+ "id": "d425ab6d",
"metadata": {},
"source": [
- "Let us start by setting up a connection to our database."
+ "### Credentials\n",
+ "\n",
+ "The connection details are read from an environment file named `creds.env`, next to this notebook.\n",
+ "Copy `creds.env.example` to `creds.env` and fill in the URI, username, password, and database of your Neo4j instance.\n",
+ "\n",
+ "On Colab, upload a `creds.env` file with the same contents to the session, or set the same variables as [secrets](https://colab.research.google.com/notebooks/snippets/secrets.ipynb).\n"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "801d0bed",
+ "id": "1bee0b2a",
"metadata": {},
"outputs": [],
"source": [
@@ -47,31 +58,32 @@
"\n",
"import dotenv\n",
"\n",
- "dotenv.load_dotenv()\n",
- "\n",
+ "# Connection details are read from `creds.env` (see above).\n",
+ "dotenv.load_dotenv(\"creds.env\")\n",
"\n",
- "URI = os.environ.get(\"NEO4J_URI\", \"bolt://localhost:7687\")\n",
- "auth = (os.environ.get(\"NEO4J_USERNAME\"), os.environ.get(\"NEO4J_PASSWORD\"))"
+ "URI = os.environ.get(\"NEO4J_URI\", \"neo4j://localhost:7687\")\n",
+ "AUTH = (os.environ.get(\"NEO4J_USERNAME\", \"neo4j\"), os.environ.get(\"NEO4J_PASSWORD\", \"password\"))\n",
+ "DATABASE = os.environ.get(\"NEO4J_DB\", \"neo4j\")"
]
},
{
"cell_type": "markdown",
- "id": "f15b2bee0f5c8e0b",
+ "id": "74a6dbd9",
"metadata": {},
"source": [
- "To illustrate the library, we will create a small example graph representing web pages."
+ "To illustrate the library, we will create a small example graph of people and the products they buy."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "fb95d30d7e932224",
+ "id": "d47aa983",
"metadata": {},
"outputs": [],
"source": [
"from neo4j import GraphDatabase\n",
"\n",
- "with GraphDatabase.driver(URI, auth=auth) as driver:\n",
+ "with GraphDatabase.driver(URI, auth=AUTH) as driver:\n",
" driver.verify_connectivity()\n",
" driver.execute_query(\n",
" \"\"\"\n",
@@ -114,33 +126,34 @@
"\n",
" (elsa)-[:BUYS {amount: 3}]->(chocolate),\n",
" (elsa)-[:BUYS {amount: 3}]->(milk)\n",
- " \"\"\"\n",
+ " \"\"\",\n",
+ " database_=DATABASE,\n",
" )"
]
},
{
"cell_type": "markdown",
- "id": "af09ad6ec575aca4",
+ "id": "e1f6896e",
"metadata": {},
"source": [
- "We can now fetch data from our database, to later include in our visualization."
+ "We can now fetch the graph from the database. The `Result.graph` result transformer maps the query result to a graph object."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "b2ed863e8986fd27",
+ "id": "765f110f",
"metadata": {},
"outputs": [],
"source": [
"from neo4j import Result, RoutingControl\n",
"\n",
- "with GraphDatabase.driver(URI, auth=auth) as driver:\n",
+ "with GraphDatabase.driver(URI, auth=AUTH) as driver:\n",
" driver.verify_connectivity()\n",
"\n",
" result = driver.execute_query(\n",
" \"MATCH (n)-[r]->(m) RETURN n,r,m\",\n",
- " database_=\"neo4j\",\n",
+ " database_=DATABASE,\n",
" routing_=RoutingControl.READ,\n",
" result_transformer_=Result.graph,\n",
" )\n",
@@ -150,3323 +163,64 @@
},
{
"cell_type": "markdown",
- "id": "365a1c31",
- "metadata": {},
- "source": [
- "Below we map the graph object's nodes and relationships to the correct format for NVL. The Node and Relationship type documentation can be found in NVL's docs: https://neo4j.com/docs/nvl/current/base-library/#_nodes"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "a28bd5aa",
+ "id": "eb823ae9",
"metadata": {},
"source": [
- "Now, we can render our result with NVL the following way:\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "id": "2322065c",
- "metadata": {
- "tags": [
- "preserve-output"
- ]
- },
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- "\n",
- "
\n",
- " \n",
- " \n",
- " neo4j-viz\n",
- " \n",
- " \n",
- " \n",
- " \n",
- " \n",
- "\n",
- " \n",
- " \n",
- " \n",
- "\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "execution_count": 5,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from neo4j_viz.neo4j import from_neo4j\n",
+ "## Visualizing the graph\n",
"\n",
- "VG = from_neo4j(result)\n",
+ "The [`from_neo4j`](https://neo4j.com/docs/python-graph-visualization/current/reference/api-reference/from_neo4j/) function converts the query result into a [`VisualizationGraph`](https://neo4j.com/docs/python-graph-visualization/current/reference/api-reference/visualization-graph/), setting node captions from their labels and coloring the nodes by label.\n",
"\n",
- "VG.render()"
+ "Calling [`render_widget`](https://neo4j.com/docs/python-graph-visualization/current/reference/api-reference/visualization-graph/#neo4j_viz.VisualizationGraph.render_widget) displays the graph as an interactive widget, with two-way sync between Python and the browser.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "46aaa12317299ad0",
+ "id": "3e7f06cf",
"metadata": {},
"outputs": [],
"source": [
- "VG.color_nodes(field=\"caption\")"
+ "from neo4j_viz.neo4j import from_neo4j\n",
+ "\n",
+ "VG = from_neo4j(result)\n",
+ "\n",
+ "widget = VG.render_widget()\n",
+ "widget"
]
},
{
- "cell_type": "code",
- "execution_count": 7,
- "id": "e8b0f4c6",
- "metadata": {
- "tags": [
- "preserve-output"
- ]
- },
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- "\n",
- " \n",
- " \n",
- " \n",
- " neo4j-viz\n",
- " \n",
- " \n",
- " \n",
- " \n",
- " \n",
- "\n",
- " \n",
- " \n",
- " \n",
- "\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "execution_count": 7,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "cell_type": "markdown",
+ "id": "3b912fb6",
+ "metadata": {},
"source": [
- "VG.render()"
+ ""
]
},
{
"cell_type": "markdown",
- "id": "deb635ed",
+ "id": "29016dab",
"metadata": {},
"source": [
"## Expanding the graph on double-click\n",
"\n",
- "Instead of loading the whole graph up front, we can start small and let the user grow it interactively. Rendering with `render_widget()` (rather than `render()`) returns an interactive `GraphWidget` that emits discrete interaction events via its synced `last_event` attribute (a typed `InteractionEvent` with `type` and `id`). Register a callback with `widget.on_node_event(...)` to react to a specific node mouse event - here `\"double_click\"` - whose callback receives the double-clicked `Node`. In the callback we query Neo4j for that node's neighborhood and `add_data(...)` the new nodes and relationships, so double-clicking progressively expands the graph."
+ "Instead of loading the whole graph up front, we can start small and let the user grow it interactively.\n",
+ "`render_widget()` returns a `GraphWidget` that emits interaction events via its synced `last_event` attribute (a typed `InteractionEvent` with `type` and `id`).\n",
+ "Register a callback with `widget.on_node_event(...)` to react to a specific node mouse event - here `\"double_click\"` - whose callback receives the double-clicked `Node`.\n",
+ "In the callback we query Neo4j for that node's neighborhood and `add_data(...)` the new nodes and relationships, so double-clicking progressively expands the graph.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "c1d2d585",
+ "id": "8f14c805",
"metadata": {},
"outputs": [],
"source": [
- "from neo4j import GraphDatabase, Result, RoutingControl\n",
- "from neo4j_viz.neo4j import from_neo4j\n",
- "\n",
"# Start from just the `Person` nodes -- we'll fetch each person's purchases on demand.\n",
- "with GraphDatabase.driver(URI, auth=auth) as driver:\n",
+ "with GraphDatabase.driver(URI, auth=AUTH) as driver:\n",
" people = driver.execute_query(\n",
" \"MATCH (n:Person) RETURN n\",\n",
- " database_=\"neo4j\",\n",
+ " database_=DATABASE,\n",
" routing_=RoutingControl.READ,\n",
" result_transformer_=Result.graph,\n",
" )\n",
@@ -3475,7 +229,7 @@
"\n",
"# Keep a driver open so the double-click callback can query the database on demand.\n",
"# Call `driver.close()` when you're done exploring.\n",
- "driver = GraphDatabase.driver(URI, auth=auth)\n",
+ "driver = GraphDatabase.driver(URI, auth=AUTH)\n",
"\n",
"\n",
"def expand_neighborhood(node):\n",
@@ -3487,7 +241,7 @@
" driver.execute_query(\n",
" \"MATCH (n)-[r]-(m) WHERE elementId(n) = $id RETURN n, r, m\",\n",
" parameters_={\"id\": node.id},\n",
- " database_=\"neo4j\",\n",
+ " database_=DATABASE,\n",
" routing_=RoutingControl.READ,\n",
" result_transformer_=Result.graph,\n",
" )\n",
@@ -3512,30 +266,33 @@
},
{
"cell_type": "markdown",
- "id": "12664b80cf6051a1",
+ "id": "756b7e05",
"metadata": {},
"source": [
- "Lastly we clean up our database by removing our toy graph."
+ "Lastly, we clean up the database by removing the example graph."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "fe49b6a242e8fff0",
+ "id": "1c54ef49",
"metadata": {},
"outputs": [],
"source": [
- "with GraphDatabase.driver(URI, auth=auth) as driver:\n",
- " result = driver.execute_query(\"MATCH (n:Person|Product) DETACH DELETE n RETURN count(n) \")\n",
+ "with GraphDatabase.driver(URI, auth=AUTH) as driver:\n",
+ " result = driver.execute_query(\n",
+ " \"MATCH (n:Person|Product) DETACH DELETE n RETURN count(n)\",\n",
+ " database_=DATABASE,\n",
+ " )\n",
" print(result.summary.counters)"
]
},
{
"cell_type": "markdown",
- "id": "daf2f685c2e13fb9",
+ "id": "dc32cec9",
"metadata": {},
"source": [
- "**NOTE:** Since in this example we didn't already have a Neo4j DB populated with data, it would actually have been more convenient to use the serverless `from_gql_create` importer method to create our `VisualizationGraph`."
+ "**NOTE:** Since this example creates its own data, we could also have built the `VisualizationGraph` directly from a GQL `CREATE` query with the [`from_gql_create`](https://neo4j.com/docs/python-graph-visualization/current/reference/api-reference/from_gql_create/) importer, without a database.\n"
]
}
],
diff --git a/examples/nodes_2025_demo.ipynb b/examples/nodes_2025_demo.ipynb
index 6ad6f351..0122b34f 100644
--- a/examples/nodes_2025_demo.ipynb
+++ b/examples/nodes_2025_demo.ipynb
@@ -38,7 +38,7 @@
"from dotenv import load_dotenv\n",
"\n",
"# Load credentials for the neo4j database\n",
- "load_dotenv(\"db_creds.env\")"
+ "load_dotenv(\"creds.env\")"
]
},
{
diff --git a/examples/snowflake-example.ipynb b/examples/snowflake-example.ipynb
index c85af1c7..9346cccb 100644
--- a/examples/snowflake-example.ipynb
+++ b/examples/snowflake-example.ipynb
@@ -5,7 +5,9 @@
"id": "0d3ffc27",
"metadata": {},
"source": [
- "# Visualizing Snowflake Tables as a Graph"
+ "# Visualizing Snowflake Tables as a Graph\n",
+ "\n",
+ "[](https://colab.research.google.com/github/neo4j/python-graph-visualization/blob/main/examples/snowflake-example.ipynb)\n"
]
},
{
@@ -34,7 +36,8 @@
"metadata": {},
"outputs": [],
"source": [
- "%pip install neo4j-viz[snowflake]"
+ "%pip install neo4j-viz[snowflake]\n",
+ "%pip install python-dotenv"
]
},
{
@@ -43,7 +46,11 @@
"metadata": {},
"source": [
"We can now proceed to set up our connection to Snowflake by initializing a new session.\n",
- "Please note that you may need more or fewer connection parameters depending on your Snowflake configuration."
+ "Please note that you may need more or fewer connection parameters depending on your Snowflake configuration.\n",
+ "\n",
+ "The connection details are read from an environment file named `creds.env`, next to this notebook.\n",
+ "Copy `creds.env.example` to `creds.env` and fill in your Snowflake connection parameters.\n",
+ "On Colab, upload a `creds.env` file with the same contents to the session.\n"
]
},
{
@@ -55,8 +62,12 @@
"source": [
"import os\n",
"\n",
+ "import dotenv\n",
"from snowflake.snowpark import Session\n",
"\n",
+ "# Connection details are read from `creds.env` (see above).\n",
+ "dotenv.load_dotenv(\"creds.env\")\n",
+ "\n",
"# Configure according to your own setup\n",
"connection_parameters = {\n",
" \"account\": os.environ.get(\"SNOWFLAKE_ACCOUNT\"),\n",
@@ -237,11 +248,7 @@
"cell_type": "code",
"execution_count": null,
"id": "e8b0f4c6",
- "metadata": {
- "tags": [
- "preserve-output"
- ]
- },
+ "metadata": {},
"outputs": [],
"source": [
"widget = VG.render_widget()\n",
diff --git a/justfile b/justfile
index f5b7a017..3e7075f2 100644
--- a/justfile
+++ b/justfile
@@ -148,12 +148,14 @@ api-docs:
render-docs:
./scripts/render_docs.sh
-# Regenerate the documentation images (README + getting-started guide) from the
-# example graphs via GraphWidget.save() in a headless browser. All images are
-# built locally without a database. Optionally restrict to named images:
-# `just generate-docs-images getting-started-graph`
+# Regenerate the documentation images (README, getting-started guide and
+# tutorial notebooks) as widget screenshots in a headless browser. Most images
+# are built locally without a database; the GDS image needs a running Graph Data
+# Science instance (see `just local-neo4j-setup`) and is skipped unless
+# NEO4J_URI is set. Optionally restrict to named images:
+# `just generate-docs-images getting-started-widget`
generate-docs-images *names:
#!/usr/bin/env bash
set -e
- cd {{py_dir}} && uv sync --group dev --group notebook
- cd {{py_dir}} && uv run --group dev --group notebook python {{root_dir}}/scripts/regenerate_docs_images.py {{names}}
+ cd {{py_dir}} && uv sync --group dev --group notebook --extra gds
+ cd {{py_dir}} && uv run --group dev --group notebook --extra gds python {{root_dir}}/scripts/regenerate_docs_images.py {{names}}
diff --git a/python-wrapper/README.md b/python-wrapper/README.md
index c3f6527a..f164c036 100644
--- a/python-wrapper/README.md
+++ b/python-wrapper/README.md
@@ -16,7 +16,7 @@ Alternatively, you can export the output to a file and view it in a web browser.
The package wraps the [Neo4j Visualization JavaScript library (NVL)](https://neo4j.com/docs/nvl/current/).
-
+
## Some notable features
diff --git a/scripts/regenerate_docs_images.py b/scripts/regenerate_docs_images.py
index b4c97b48..c909a158 100755
--- a/scripts/regenerate_docs_images.py
+++ b/scripts/regenerate_docs_images.py
@@ -3,18 +3,22 @@
For each image, a disposable JupyterLab server is booted, a distilled cell
sequence reproducing the corresponding example's data and styling is run in
-headless Chrome, and the rendered graph is saved via ``GraphWidget.save()``.
-The images are copied back to their repository locations. The output format
-(PNG or SVG) is derived from each target's file ending.
-
-All graphs are built locally without a database, via the toy graph from the
-getting-started guide and the neo4j-example's CREATE query through the
-``from_gql_create`` GQL integration. Run from anywhere inside the repo.
+headless Chrome, and the whole widget element (graph canvas plus its UI chrome:
+toolbar, search, legend) is screenshotted. The images are copied back to their
+repository locations.
+
+Most graphs are built locally without a database, via the toy graph from the
+getting-started guide and the examples' CREATE queries through the
+``from_gql_create`` GQL integration. The GDS image needs a running Graph Data
+Science instance: set ``NEO4J_URI`` (see ``just local-neo4j-setup``) to include
+it, otherwise that spec is skipped. Run from anywhere inside the repo.
"""
from __future__ import annotations
import argparse
+import functools
+import os
import shutil
import struct
import sys
@@ -22,18 +26,32 @@
from dataclasses import dataclass
from pathlib import Path
+from playwright.sync_api import Page
+
GIT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(GIT_ROOT / "python-wrapper"))
from tests.browser_harness import (
DONE_MARKER,
- ERROR_MARKER,
BrowserUnavailableError,
jupyter_server,
run_notebook_in_browser,
)
_ANTORA_IMAGES = GIT_ROOT / "docs" / "antora" / "modules" / "ROOT" / "images"
+_EXAMPLES = GIT_ROOT / "examples"
+
+# The widget wrapper element: the graph canvas plus its UI chrome (toolbar,
+# search, legend). Screenshotting it captures the widget as a user sees it.
+_WIDGET_SELECTOR = '[data-neo4j-viz-ndl="true"]'
+# The canvas mounts before the force-directed layout settles, so give the
+# widget a moment to paint before capturing it.
+_WIDGET_SETTLE_MS = 3000
+# JupyterLab chrome that would otherwise overlap the widget in the capture.
+_CHROME_HIDE_CSS = (
+ ".jp-NotebookPanel-toolbar, .jp-Notebook-toolbar, .jp-Notification, "
+ ".jp-Notification-Stack, .jp-Dialog, .jp-StatusBar { display: none !important; }"
+)
_TOY_GRAPH_CELLS = """\
from neo4j_viz import Node, Relationship, VisualizationGraph
@@ -104,6 +122,73 @@
)
"""
+# The graph from examples/snowflake-example.ipynb, built without Snowflake via
+# the GQL CREATE integration. The labels stand in for the Snowflake table names,
+# so the captions and colors match what from_snowflake produces.
+_SNOWFLAKE_CELLS = """\
+from neo4j_viz.gql_create import from_gql_create
+
+VG = from_gql_create(
+ \"\"\"
+ CREATE
+ (alice:PERSONS {SNOWFLAKEID: 'Alice'}),
+ (bob:PERSONS {SNOWFLAKEID: 'Bob'}),
+ (carol:PERSONS {SNOWFLAKEID: 'Carol'}),
+ (dave:PERSONS {SNOWFLAKEID: 'Dave'}),
+ (eve:PERSONS {SNOWFLAKEID: 'Eve'}),
+
+ (guitar:INSTRUMENTS {SNOWFLAKEID: 'Guitar'}),
+ (synthesizer:INSTRUMENTS {SNOWFLAKEID: 'Synthesizer'}),
+ (bongos:INSTRUMENTS {SNOWFLAKEID: 'Bongos'}),
+ (trumpet:INSTRUMENTS {SNOWFLAKEID: 'Trumpet'}),
+
+ (alice)-[:LIKES]->(guitar),
+ (alice)-[:LIKES]->(synthesizer),
+ (alice)-[:LIKES]->(bongos),
+ (bob)-[:LIKES]->(guitar),
+ (bob)-[:LIKES]->(synthesizer),
+ (carol)-[:LIKES]->(bongos),
+ (dave)-[:LIKES]->(guitar),
+ (dave)-[:LIKES]->(trumpet),
+ (dave)-[:LIKES]->(bongos)
+ \"\"\"
+)
+"""
+
+# The Cora graph from examples/gds-example.ipynb. Unlike the other specs this
+# needs a running GDS instance (see the module docstring).
+_GDS_CELLS = (
+ """\
+import os
+
+from graphdatascience import GraphDataScience
+
+gds = GraphDataScience(
+ os.environ["NEO4J_URI"],
+ auth=(os.environ.get("NEO4J_USERNAME", "neo4j"), os.environ.get("NEO4J_PASSWORD", "password")),
+)
+
+G = gds.graph.datasets.load_cora(graph_name="cora")
+gds.node_similarity.mutate(G, mutate_relationship_type="SIMILAR", mutate_property="similarity")
+gds.page_rank.mutate(G, mutate_property="pagerank")
+gds.louvain.mutate(G, mutate_property="componentId")
+""",
+ """\
+import matplotlib.colors as mcolors
+from palettable.colorbrewer.qualitative import Dark2_7
+
+from neo4j_viz.gds import from_gds
+
+VG = from_gds(gds, G, max_node_count=100)
+VG.resize_nodes(property="pagerank")
+
+number_of_components = len({n.properties["componentId"] for n in VG.nodes})
+color_map = Dark2_7.mpl_colormap.resampled(number_of_components)
+colors = [mcolors.rgb2hex(color_map(i)) for i in range(number_of_components)]
+VG.color_nodes(property="componentId", colors=colors, override=True)
+""",
+)
+
@dataclass(frozen=True)
class ImageSpec:
@@ -111,18 +196,18 @@ class ImageSpec:
name: str
target: Path
- # Widget canvas size in pixels; for PNG output this is also the expected
- # image size. SVG output covers the entire graph, so the widget size only
- # needs to be large enough to mount the widget.
+ # Widget canvas size in pixels; the screenshot is the same size (the widget
+ # element's bounding box).
size: tuple[int, int]
# Cells up to and including the one that displays the widget. The harness
- # waits for the widget's canvas before running the generated save cell.
+ # waits for the widget's canvas before running the generated marker cell.
cells: tuple[str, ...] = ()
-
- @property
- def fmt(self) -> str:
- """The output format, derived from the target's file ending."""
- return self.target.suffix.lstrip(".")
+ # Extra locations to copy the produced image to (e.g. the examples/ copy
+ # that a notebook references alongside the Antora page copy).
+ also_copy: tuple[Path, ...] = ()
+ # When set, the spec only runs if this environment variable is present. Used
+ # for images that need an external service (e.g. GDS via NEO4J_URI).
+ requires_env: str | None = None
@property
def filename(self) -> str:
@@ -136,30 +221,22 @@ def _render_cell(size: tuple[int, int]) -> str:
)
-def _save_cell(spec: ImageSpec) -> str:
- filename = spec.filename
- if spec.fmt == "svg":
- # SVG export covers the entire graph and defaults to a transparent
- # background, so it blends with the docs page.
- save_call = f"await widget.save({filename!r})"
- output_check = (
- f"assert Path({filename!r}).read_text().lstrip().startswith('