diff --git a/examples/nodes_2025_demo.ipynb b/examples/nodes_2025_demo.ipynb new file mode 100644 index 0000000..ef609dd --- /dev/null +++ b/examples/nodes_2025_demo.ipynb @@ -0,0 +1,395 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8dcd267e", + "metadata": {}, + "source": [ + "# NODES 2025 Demo\n", + "\n", + "This notebook represents the demo part of the [presentation](https://neo4j.com/nodes-2025/agenda/easy-jupyter-notebook-graph-visualization-in-10-minutes/) at the NODES 2025 conference." + ] + }, + { + "cell_type": "markdown", + "id": "e15fcce5", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb45b182", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install \"neo4j-viz[gds, neo4j]\" python-dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10b32405", + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "\n", + "# Load credentials for the neo4j database\n", + "load_dotenv(\"db_creds.env\")" + ] + }, + { + "cell_type": "markdown", + "id": "cfb884ec", + "metadata": {}, + "source": [ + "## Building our visualization graph\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "02304ee4", + "metadata": {}, + "source": [ + "### From a Create Query string\n", + "\n", + "The most simple way to test out neo4j-viz is by using `from_gql_create`.\n", + "The nodes and relationships are directly parsed from the provided query string. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05eda814", + "metadata": {}, + "outputs": [], + "source": [ + "from neo4j_viz.gql_create import from_gql_create\n", + "\n", + "VG = from_gql_create(\"\"\"\n", + " CREATE\n", + " (alice:Person {name: 'Alice', age: 30}),\n", + " (bob:Person {name: 'Bob', age: 25}),\n", + " (carol:Person {name: 'Carol', age: 27}),\n", + " (alice)-[:FRIENDS_WITH {since: 2015}]->(bob),\n", + " (bob)-[:FRIENDS_WITH {since: 2018}]->(carol)\n", + "\"\"\")\n", + "\n", + "VG.render(initial_zoom=1.5)" + ] + }, + { + "cell_type": "markdown", + "id": "32979c03", + "metadata": {}, + "source": [ + "## From a Neo4j database\n", + "\n", + "Now lets assume, you have a Neo4j database available which you want to inspect.\n", + "In the following, I assume the Movies dataset is imported. You can use `:play movies` in the Neo4j Browser to import the dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72b14cea", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import neo4j\n", + "from neo4j_viz.neo4j import from_neo4j\n", + "\n", + "driver = neo4j.GraphDatabase.driver(\n", + " uri=os.getenv(\"NEO4J_URI\"),\n", + " auth=(os.getenv(\"NEO4J_USERNAME\"), os.getenv(\"NEO4J_PASSWORD\")),\n", + ")\n", + "\n", + "# Limiting to 20 rows for demo purposes\n", + "VG = from_neo4j(driver, row_limit=20)\n", + "VG.render(initial_zoom=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "8c7091b4", + "metadata": {}, + "source": [ + "## From GDS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "153decdb", + "metadata": {}, + "outputs": [], + "source": [ + "from graphdatascience import GraphDataScience\n", + "\n", + "gds = GraphDataScience(\n", + " endpoint=os.getenv(\"NEO4J_URI\"),\n", + " auth=(os.getenv(\"NEO4J_USERNAME\"), os.getenv(\"NEO4J_PASSWORD\")),\n", + ")\n", + "gds.set_database(\"neo4j\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c7cdcb4", + "metadata": {}, + "outputs": [], + "source": [ + "G, _ = gds.graph.cypher.project(\n", + " query=\"\"\"\n", + " MATCH (s:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(t:Person)\n", + " WITH s, t, count(m) as common_movies\n", + " WHERE s < t\n", + " RETURN gds.graph.project('demo-graph', s, t, {\n", + " sourceNodeLabels: labels(s),\n", + " targetNodeLabels: labels(t),\n", + " relationshipProperties: {weight: common_movies},\n", + " relationshipType: 'CO_ACTED'\n", + " }, {undirectedRelationshipTypes: ['CO_ACTED']})\n", + "\"\"\"\n", + ")\n", + "\n", + "str(G)" + ] + }, + { + "cell_type": "markdown", + "id": "c5652cd6", + "metadata": {}, + "source": [ + "How do we make sure the projection matches our expectation? Especially if the Cypher queries get more complex" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1b405bd", + "metadata": {}, + "outputs": [], + "source": [ + "from neo4j_viz.gds import from_gds\n", + "\n", + "# Limit to a couple of nodes to inspect the projection. Sampling via random-walks implemented in GDS\n", + "VG = from_gds(gds, G, db_node_properties=[\"name\"], max_node_count=20)\n", + "VG.render()" + ] + }, + { + "cell_type": "markdown", + "id": "eabdc19f", + "metadata": {}, + "source": [ + "Lets run some GDS algorithms to get some more insights about our co-actor graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bfd2bc6", + "metadata": {}, + "outputs": [], + "source": [ + "gds.pageRank.mutate(G, relationshipWeightProperty=\"weight\", mutateProperty=\"pageRank\")\n", + "gds.leiden.mutate(G, mutateProperty=\"component\", relationshipWeightProperty=\"weight\")" + ] + }, + { + "cell_type": "markdown", + "id": "331f7f79", + "metadata": {}, + "source": [ + "Other supported datasources include `from_snowflake`, and `from_pandas`." + ] + }, + { + "cell_type": "markdown", + "id": "4c7da83d", + "metadata": {}, + "source": [ + "## Customizing the Visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c3e5d7a", + "metadata": {}, + "outputs": [], + "source": [ + "from neo4j_viz.gds import from_gds\n", + "\n", + "# make sure to include also the newly computed properties such as pageRank\n", + "# by default from_gds includes all properties of G\n", + "VG = from_gds(gds, G, db_node_properties=[\"name\"])\n", + "VG.render()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5fc8746", + "metadata": {}, + "outputs": [], + "source": [ + "# Lets first fix the caption of the nodes to show the name.\n", + "for node in VG.nodes:\n", + " node.caption = node.properties.get(\"name\")\n", + "\n", + "VG.render()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bb837b7", + "metadata": {}, + "outputs": [], + "source": [ + "# Inspect the computed communities to see which actors are grouped together\n", + "VG.color_nodes(property=\"component\", override=True)\n", + "VG.render()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c6839b0", + "metadata": {}, + "outputs": [], + "source": [ + "# Resize nodes based on pageRank property, i.e., more important actors appear larger\n", + "VG.resize_nodes(property=\"pageRank\")\n", + "VG.render()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8f4e4f0", + "metadata": {}, + "outputs": [], + "source": [ + "# To make sure certain nodes are always visible, we can pin them. Here we pin \"Keanu Reeves\".\n", + "pinned_nodes = {\n", + " node.id: True\n", + " for node in VG.nodes\n", + " if node.properties.get(\"name\") in [\"Keanu Reeves\"]\n", + "}\n", + "\n", + "VG.toggle_nodes_pinned(pinned_nodes)\n", + "VG.render()" + ] + }, + { + "cell_type": "markdown", + "id": "4e565d57", + "metadata": {}, + "source": [ + "Further customization ideas to explore: \n", + "\n", + "* Modify the layout such as by adding coordinates\n", + "* Use custom colors from [`palettable.wesanderson`](https://jiffyclub.github.io/palettable/) \n", + "* Change the size range for nodes" + ] + }, + { + "cell_type": "markdown", + "id": "311fbb2f", + "metadata": {}, + "source": [ + "## Saving the Visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efd0a82c", + "metadata": {}, + "outputs": [], + "source": [ + "# Use the save button in the rendered view. This produces a static image.\n", + "VG.render()" + ] + }, + { + "cell_type": "markdown", + "id": "a5128b85", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11201815", + "metadata": {}, + "outputs": [], + "source": [ + "# Save the raw HTML output to a file. This allows to share the interactive visualization.\n", + "\n", + "import os\n", + "from neo4j_viz.options import Renderer\n", + "\n", + "os.makedirs(\"./out\", exist_ok=True)\n", + "\n", + "# Save the visualization to a file\n", + "with open(\"out/co_acted.html\", \"w\") as f:\n", + " f.write(VG.render(renderer=Renderer.CANVAS).data)" + ] + }, + { + "cell_type": "markdown", + "id": "71c3f41e", + "metadata": {}, + "source": [ + "## Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "322d137f", + "metadata": {}, + "outputs": [], + "source": [ + "driver.close()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "429dec14", + "metadata": {}, + "outputs": [], + "source": [ + "gds.graph.get(\"demo-graph\").drop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "010ab463", + "metadata": {}, + "outputs": [], + "source": [ + "gds.close()" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}