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/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thank you for your contribution to the Graph Data Science Client project.
If relevant, link to the corresponding issue.
Please also include relevant motivation and context. List any dependencies that are required for this change. -->

Before submitting this PR, please read [Contributing to the Neo4j Ecosystem](https://github.com/neo4j/nvl-python-wrapper/blob/main/CONTRIBUTING.md).
Before submitting this PR, please read [Contributing to the Neo4j Ecosystem](https://github.com/neo4j/python-graph-visualization/blob/main/CONTRIBUTING.md).

Make sure:
- [ ] You signed the [Neo4j CLA](https://neo4j.com/developer/cla/#sign-cla) (Contributor License Agreement) so that we are allowed to ship your code in our library
Expand Down
23 changes: 2 additions & 21 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

defaults:
run:
Expand All @@ -40,27 +40,8 @@ jobs:
cache-dependency-path: pyproject.toml
- run: pip install ".[dev]"
- run: pip install ".[pandas]"
- run: pip install ".[neo4j]"
- run: pip install ".[gds]"

- name: Run tests
run: pytest tests/

tests-313:
runs-on: ubuntu-latest
defaults:
run:
working-directory: python-wrapper
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: 3.13
cache: 'pip'
cache-dependency-path: pyproject.toml
- run: pip install ".[dev]"
- run: pip install ".[pandas]"
# skip gds for now as it is not available for python 3.13

- name: Run tests
run: pytest tests/
16 changes: 13 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,26 @@ pytest python-wrapper/tests

Additionally, there are integration tests that require an external data source.
These require additional setup and configuration, such as environment variables specifying connection details.
To run tests requiring a Neo4j DB instance with GDS installed, execute:


For a local Neo4j instance with GDS installed, execute:
```sh
cd test-envs/neo4j-gds
docker compose up -d
```

To run tests requiring a Neo4j DB instance with GDS installed, execute:
```sh
pytest python-wrapper/tests --include-neo4j-and-gds
export NEO4J_URI=localhost:7687 # or credentials for Aura API
cd python-wrapper/
pytest tests --include-neo4j-and-gds
```

To run tests requiring a Snowflake connection, execute:

```sh
pytest python-wrapper/tests --include-snowflake
cd python-wrapper/
pytest tests/ --include-snowflake
```


Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Proper documentation is forthcoming.

* Easy to import graphs represented as:
* projections in the Neo4j Graph Data Science (GDS) library
* graphs from Neo4j query results
* pandas DataFrames
* Node features:
* Sizing
Expand Down
4 changes: 2 additions & 2 deletions docs/source/adjusting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ In this case, all nodes with the same caption will get the same color.

If there are fewer colors that unique values for the node ``property`` provided, the colors will be reused in a cycle.
To avoid that, you could use another palette or extend one with additional colors. Please refer to the
:doc:`Visualizing Neo4j Graph Data Science (GDS) Graphs tutorial <./tutorials/gds-nvl-example>` for an example on how
:doc:`Visualizing Neo4j Graph Data Science (GDS) Graphs tutorial <./tutorials/gds-example>` for an example on how
to do the latter.

If some nodes already have a ``color`` set, you can choose whether or not to override it with the ``override``
Expand Down Expand Up @@ -142,4 +142,4 @@ Each node and relationship has attributes that can be accessed and modified dire
VG.nodes[0].size = 10
VG.relationships[4].caption = "BUYS"

Any changes made to the nodes and relationships will be reflected in the next rendering of the graph.
Any changes made to the nodes and relationships will be reflected in the next rendering of the graph.
5 changes: 5 additions & 0 deletions docs/source/api-reference/from_neo4j.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Import from Neo4j
------------------------------------------------

.. automodule:: neo4j_viz.neo4j
:members:
9 changes: 9 additions & 0 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ To install the additional dependencies required for the :doc:`from_dfs importer

pip install neo4j-viz[pandas]

Neo4j ``from_neo4j`` importer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To install the additional dependencies required for the :doc:`from_neo4j importer <./api-reference/from_neo4j>` you can run:

.. code-block:: bash

pip install neo4j-viz[neo4j]


Neo4j Graph Data Science ``from_gds`` importer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
66 changes: 64 additions & 2 deletions docs/source/integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ and :doc:`Relationships <./api-reference/relationship>`.
VG = from_dfs(nodes, relationships)

For another example of the ``from_dfs`` importer in action, see the
:doc:`Visualizing Snowflake Tables tutorial <./tutorials/snowpark-nvl-example>`.
:doc:`Visualizing Snowflake Tables tutorial <./tutorials/snowpark-example>`.


Neo4j Graph Data Science (GDS) library
Expand Down Expand Up @@ -139,5 +139,67 @@ We use the "pagerank" property to determine the size of the nodes, and the "comp
VG.color_nodes("componentId")


Please see the :doc:`Visualizing Neo4j Graph Data Science (GDS) Graphs tutorial <./tutorials/gds-nvl-example>` for a
Please see the :doc:`Visualizing Neo4j Graph Data Science (GDS) Graphs tutorial <./tutorials/gds-example>` for a
more extensive example.


Neo4j Database
---------------

The ``neo4j-viz`` library provides a convenience method for importing data from Neo4j.
It requires and additional dependency to be installed, which you can do by running:

.. code-block:: bash

pip install neo4j-viz[neo4j]

Once you have installed the additional dependency, you can use the :doc:`from_neo4j <./api-reference/from_neo4j>` method
to import query results from Neo4j.

The ``from_neo4j`` method takes one mandatory positional parameters:

* A ``result`` representing the query result either in form of `neo4j.graph.Graph` or `neo4j.Result`.

The ``node_caption`` parameter is also optional, and indicates the value to use for the caption of each node in the visualization.

We can also provide an optional ``size_property`` parameter, which should refer to a node property of the projection,
and will be used to determine the size of the nodes in the visualization.

The last optional property, ``node_radius_min_max``, can be used (and is used by default) to scale the node sizes for
the visualization.
It is a tuple of two numbers, representing the radii (sizes) in pixels of the smallest and largest nodes respectively in
the visualization.
The node sizes will be scaled such that the smallest node will have the size of the first value, and the largest node
will have the size of the second value.
The other nodes will be scaled linearly between these two values according to their relative size.
This can be useful if node sizes vary a lot, or are all very small or very big.


Example
~~~~~~~

In this small example, we import a graph from a Neo4j query result.

.. code-block:: python

from neo4j import GraphDataScience
from neo4j_viz.gds import from_gds

# Modify this to match your Neo4j instance's URI and credentials
URI = "neo4j://localhost:7687"
auth = ("neo4j", "password")

with GraphDatabase.driver(URI, auth=auth) as driver:
driver.verify_connectivity()

result = driver.execute_query(
"MATCH (n)-[r]->(m) RETURN n,r,m",
database_="neo4j",
result_transformer_=Result.graph,
)

VG = from_neo4j(result)


Please see the :doc:`Visualizing Neo4j Graphs tutorial <./tutorials/neo4j-example>` for a
more extensive example.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"path": "../../../examples/gds-nvl-example.ipynb",
"path": "../../../examples/gds-example.ipynb",
"extra-media": [
"../../../examples/example_cora_graph.png"
]
Expand Down
3 changes: 3 additions & 0 deletions docs/source/tutorials/neo4j-example.nblink
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"path": "../../../examples/neo4j-example.ipynb"
}
3 changes: 0 additions & 3 deletions docs/source/tutorials/neo4j-nvl-example.nblink

This file was deleted.

3 changes: 3 additions & 0 deletions docs/source/tutorials/snowpark-example.nblink
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"path": "../../../examples/snowpark-example.ipynb"
}
3 changes: 0 additions & 3 deletions docs/source/tutorials/snowpark-nvl-example.nblink

This file was deleted.

File renamed without changes.
183 changes: 73 additions & 110 deletions examples/neo4j-nvl-example.ipynb → examples/neo4j-example.ipynb

Large diffs are not rendered by default.

File renamed without changes.
3 changes: 2 additions & 1 deletion python-wrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Proper documentation is forthcoming.

* Easy to import graphs represented as:
* projections in the Neo4j Graph Data Science (GDS) library
* graphs from Neo4j query results
* pandas DataFrames
* Node features:
* Sizing
Expand Down Expand Up @@ -112,4 +113,4 @@ This will return a `IPython.display.HTML` object that can be rendered in a Jupyt
### Examples

For more extensive examples, including how to import graphs from Neo4j GDS projections and Pandas DataFrames,
checkout the [tutorials chapter](https://neo4j.com/docs/nvl-python/preview/tutorials/index.html) in the documentation.
checkout the [tutorials chapter](https://neo4j.com/docs/nvl-python/preview/tutorials/index.html) in the documentation.
1 change: 1 addition & 0 deletions python-wrapper/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ docs = [
]
pandas = ["pandas>=2, <3", "pandas-stubs>=2, <3"]
gds = ["graphdatascience>=1, <2"] # not compatible yet with Python 3.13
neo4j = ["neo4j"]
notebook = [
"ipykernel==6.29.5",
"pykernel==0.1.6",
Expand Down
99 changes: 99 additions & 0 deletions python-wrapper/src/neo4j_viz/neo4j.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from __future__ import annotations

from typing import Optional, Union

import neo4j.graph
from neo4j import Result

from neo4j_viz.node import Node
from neo4j_viz.relationship import Relationship
from neo4j_viz.visualization_graph import VisualizationGraph


def from_neo4j(
result: Union[neo4j.graph.Graph, Result],
size_property: Optional[str] = None,
node_caption: Optional[str] = "labels",
relationship_caption: Optional[str] = "type",
node_radius_min_max: Optional[tuple[float, float]] = (3, 60),
) -> VisualizationGraph:
"""
Create a VisualizationGraph from a Neo4j Graph or Neo4j Result object.


Parameters
----------
result : Union[neo4j.graph.Graph, Result]
Query result either in shape of a Graph or result.
size_property : str, optional
Property to use for node size, by default None.
node_caption : str, optional
Property to use as the node caption, by default the node labels will be used.
relationship_caption : str, optional
Property to use as the relationship caption, by default the relationship type will be used.
node_radius_min_max : tuple[float, float], optional
Minimum and maximum node radius, by default (3, 60).
To avoid tiny or huge nodes in the visualization, the node sizes are scaled to fit in the given range.
"""

if isinstance(result, Result):
graph = result.graph()
elif isinstance(result, neo4j.graph.Graph):
graph = result
else:
raise ValueError(f"Invalid input type `{type(result)}`. Expected `neo4j.Graph` or `neo4j.Result`")

nodes = [_map_node(node, size_property, caption_property=node_caption) for node in graph.nodes]
relationships = []
for rel in graph.relationships:
mapped_rel = _map_relationship(rel, caption_property=relationship_caption)
if mapped_rel:
relationships.append(mapped_rel)

VG = VisualizationGraph(nodes, relationships)

if node_radius_min_max and size_property is not None:
VG.resize_nodes(node_radius_min_max=node_radius_min_max)

return VG


def _map_node(node: neo4j.graph.Node, size_property: Optional[str], caption_property: Optional[str]) -> Node:
if size_property:
size = node.get(size_property)
else:
size = None

labels = sorted([label for label in node.labels])
if caption_property:
if caption_property == "labels":
if len(labels) > 0:
caption = ":".join([label for label in labels])
else:
caption = None
else:
caption = str(node.get(caption_property))

return Node(id=node.element_id, caption=caption, labels=labels, size=size, **{k: v for k, v in node.items()})


def _map_relationship(rel: neo4j.graph.Relationship, caption_property: Optional[str]) -> Optional[Relationship]:
if rel.start_node is None or rel.end_node is None:
return None

if caption_property:
if caption_property == "type":
caption = rel.type
else:
caption = str(rel.get(caption_property))
else:
caption = None

return Relationship(
id=rel.element_id,
source=rel.start_node.element_id,
target=rel.end_node.element_id,
type_=rel.type,
caption=caption,
**{k: v for k, v in rel.items()},
)
25 changes: 19 additions & 6 deletions python-wrapper/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,9 @@ def gds() -> Generator[Any, None, None]:
from gds_helper import aura_api, connect_to_plugin_gds, create_aurads_instance
from graphdatascience import GraphDataScience

NEO4J_URI = os.environ.get("NEO4J_URI")
use_cloud_setup = os.environ.get("AURA_API_CLIENT_ID", None)

if NEO4J_URI:
gds = connect_to_plugin_gds(NEO4J_URI)
yield gds
gds.close()
else:
if use_cloud_setup:
api = aura_api()
id, dbms_connection_info = create_aurads_instance(api)

Expand All @@ -61,3 +57,20 @@ def gds() -> Generator[Any, None, None]:
os.environ["NEO4J_URI"] = ""

api.delete_instance(id)
else:
NEO4J_URI = os.environ.get("NEO4J_URI", "neo4j://localhost:7687")
gds = connect_to_plugin_gds(NEO4J_URI)
yield gds
gds.close()


@pytest.fixture(scope="package")
def neo4j_session() -> Generator[Any, None, None]:
import neo4j

NEO4J_URI = os.environ.get("NEO4J_URI", "neo4j://localhost:7687")

with neo4j.GraphDatabase.driver(NEO4J_URI) as driver:
driver.verify_connectivity()
with driver.session() as session:
yield session
Loading