Skip to content

Commit 59d98bc

Browse files
authored
Merge pull request #222 from neo4j/support-db-properties-from-gds
support db properties from gds
2 parents eee3e80 + 4615490 commit 59d98bc

File tree

4 files changed

+52
-9
lines changed

4 files changed

+52
-9
lines changed

changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
## New features
88

9+
- Allow to include db node properties in addition to the properties in the GDS Graph. Specify `additional_db_node_properties` in `from_gds`.
10+
11+
912
## Bug fixes
1013

1114
- fixed a bug in `from_neo4j`, where the node size would always be set to the `size` property.

docs/source/integration.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ The default is ``None``, which means that all properties of the nodes in the pro
120120
Apart from being visible through on-hover tooltips, these properties could be used to color the nodes, or give captions
121121
to them in the visualization, or simply included in the nodes' ``Node.properties`` maps without directly impacting the
122122
visualization.
123+
If you want to include node properties stored at the Neo4j database, you can include them in the visualization by using the `additional_db_node_properties` parameter.
123124

124125
The last optional property, ``node_radius_min_max``, can be used (and is used by default) to scale the node sizes for
125126
the visualization.
@@ -401,4 +402,3 @@ In this small example, we import a toy graph representing a social network from
401402
402403
For a full example of the ``from_snowflake`` importer in action, please see the
403404
:doc:`Visualizing Snowflake Tables tutorial <./tutorials/snowflake-example>`.
404-

python-wrapper/src/neo4j_viz/gds.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,19 @@
1313

1414

1515
def _fetch_node_dfs(
16-
gds: GraphDataScience, G: Graph, node_properties_by_label: dict[str, list[str]], node_labels: list[str]
16+
gds: GraphDataScience,
17+
G: Graph,
18+
node_properties_by_label: dict[str, list[str]],
19+
node_labels: list[str],
20+
additional_db_node_properties: list[str],
1721
) -> dict[str, pd.DataFrame]:
1822
return {
1923
lbl: gds.graph.nodeProperties.stream(
20-
G, node_properties=node_properties_by_label[lbl], node_labels=[lbl], separate_property_columns=True
24+
G,
25+
node_properties=node_properties_by_label[lbl],
26+
node_labels=[lbl],
27+
separate_property_columns=True,
28+
db_node_properties=additional_db_node_properties,
2129
)
2230
for lbl in node_labels
2331
}
@@ -49,6 +57,7 @@ def from_gds(
4957
G: Graph,
5058
size_property: Optional[str] = None,
5159
additional_node_properties: Optional[list[str]] = None,
60+
additional_db_node_properties: Optional[list[str]] = None,
5261
node_radius_min_max: Optional[tuple[float, float]] = (3, 60),
5362
max_node_count: int = 10_000,
5463
) -> VisualizationGraph:
@@ -71,14 +80,19 @@ def from_gds(
7180
Property to use for node size, by default None.
7281
additional_node_properties : list[str], optional
7382
Additional properties to include in the visualization node, by default None which means that all node
74-
properties will be fetched.
83+
properties from the Graph will be fetched.
84+
additional_db_node_properties : list[str], optional
85+
Additional node properties to fetch from the database, by default None. Only works if the graph was projected from the database.
7586
node_radius_min_max : tuple[float, float], optional
7687
Minimum and maximum node radius, by default (3, 60).
7788
To avoid tiny or huge nodes in the visualization, the node sizes are scaled to fit in the given range.
7889
max_node_count : int, optional
7990
The maximum number of nodes to fetch from the graph. The graph will be sampled using random walk with restarts
8091
if its node count exceeds this number.
8192
"""
93+
if additional_db_node_properties is None:
94+
additional_db_node_properties = []
95+
8296
node_properties_from_gds = G.node_properties()
8397
assert isinstance(node_properties_from_gds, pd.Series)
8498
actual_node_properties: dict[str, list[str]] = cast(dict[str, list[str]], node_properties_from_gds.to_dict())
@@ -102,9 +116,8 @@ def from_gds(
102116
}
103117

104118
if size_property is not None:
105-
# For some reason mypy are unable to understand that this is dict[str, set[str]]
106-
for label, props in node_properties_by_label_sets.items(): # type: ignore
107-
props.add(size_property) # type: ignore
119+
for label, label_props in node_properties_by_label_sets.items():
120+
label_props.add(size_property)
108121

109122
node_properties_by_label = {k: list(v) for k, v in node_properties_by_label_sets.items()}
110123

@@ -129,7 +142,9 @@ def from_gds(
129142
for props in node_properties_by_label.values():
130143
props.append(property_name)
131144

132-
node_dfs = _fetch_node_dfs(gds, G_fetched, node_properties_by_label, G_fetched.node_labels())
145+
node_dfs = _fetch_node_dfs(
146+
gds, G_fetched, node_properties_by_label, G_fetched.node_labels(), additional_db_node_properties
147+
)
133148
if property_name is not None:
134149
for df in node_dfs.values():
135150
df.drop(columns=[property_name], inplace=True)

python-wrapper/tests/test_gds.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import re
2-
from typing import Any
2+
from typing import Any, Generator
33

44
import pandas as pd
55
import pytest
@@ -8,6 +8,19 @@
88
from neo4j_viz import Node
99

1010

11+
@pytest.fixture(scope="class")
12+
def db_setup(gds: Any) -> Generator[None, None, None]:
13+
gds.run_cypher(
14+
"CREATE "
15+
" (a:_CI_A {name:'Alice', height:20, id:42, _id: 1337, caption: 'hello'})"
16+
" ,(b:_CI_A:_CI_B {name:'Bob', height:10, id: 84, size: 11, labels: [1,2]})"
17+
" ,(a)-[:KNOWS {year: 2025, id: 41, source: 1, target: 2}]->(b)"
18+
" ,(b)-[:RELATED {year: 2015, _type: 'A', caption:'hej'}]->(a)"
19+
)
20+
yield
21+
gds.run_cypher("MATCH (n:_CI_A|_CI_B) DETACH DELETE n")
22+
23+
1124
@pytest.mark.requires_neo4j_and_gds
1225
def test_from_gds_integration_size(gds: Any) -> None:
1326
from neo4j_viz.gds import from_gds
@@ -74,6 +87,18 @@ def test_from_gds_integration_size(gds: Any) -> None:
7487
]
7588

7689

90+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
91+
@pytest.mark.requires_neo4j_and_gds
92+
def test_from_gds_integration_all_db_properties(gds: Any, db_setup: None) -> None:
93+
from neo4j_viz.gds import from_gds
94+
95+
with gds.graph.project("g2", ["_CI_A", "_CI_B"], "*") as G:
96+
VG = from_gds(gds, G, node_radius_min_max=None, additional_db_node_properties=["name"])
97+
98+
assert len(VG.nodes) == 2
99+
assert {n.properties["name"] for n in VG.nodes} == {"Alice", "Bob"}
100+
101+
77102
@pytest.mark.requires_neo4j_and_gds
78103
def test_from_gds_integration_all_properties(gds: Any) -> None:
79104
from neo4j_viz.gds import from_gds

0 commit comments

Comments
 (0)