22
33import warnings
44from itertools import chain
5- from typing import Optional , cast
5+ from typing import Collection , Optional
66from uuid import uuid4
77
88import pandas as pd
99from graphdatascience import Graph , GraphDataScience
10+ from graphdatascience .graph .v2 import GraphV2
11+ from graphdatascience .session import AuraGraphDataScience
1012
1113from neo4j_viz .colors import NEO4J_COLORS_DISCRETE , ColorSpace
1214
1517
1618
1719def _fetch_node_dfs (
18- gds : GraphDataScience ,
19- G : Graph ,
20+ gds : GraphDataScience | AuraGraphDataScience ,
21+ G : GraphV2 ,
2022 node_properties_by_label : dict [str , list [str ]],
21- node_labels : list [str ],
23+ node_labels : Collection [str ],
2224 additional_db_node_properties : list [str ],
2325) -> dict [str , pd .DataFrame ]:
2426 return {
25- lbl : gds .graph .nodeProperties .stream (
27+ lbl : gds .v2 . graph .node_properties .stream (
2628 G ,
2729 node_properties = node_properties_by_label [lbl ],
2830 node_labels = [lbl ],
29- separate_property_columns = True ,
3031 db_node_properties = additional_db_node_properties ,
3132 )
3233 for lbl in node_labels
3334 }
3435
3536
36- def _fetch_rel_dfs (gds : GraphDataScience , G : Graph ) -> list [pd .DataFrame ]:
37- rel_types = G .relationship_types ()
38-
39- rel_props = {rel_type : G .relationship_properties (rel_type ) for rel_type in rel_types }
37+ def _fetch_rel_dfs (gds : GraphDataScience | AuraGraphDataScience , G : GraphV2 ) -> list [pd .DataFrame ]:
38+ rel_props = G .relationship_properties ()
4039
4140 rel_dfs : list [pd .DataFrame ] = []
41+
4242 # Have to call per stream per relationship type as there was a bug in GDS < 2.21
4343 for rel_type , props in rel_props .items ():
44- assert isinstance (props , list )
45- if len (props ) > 0 :
46- rel_df = gds .graph .relationshipProperties .stream (
47- G , relationship_types = rel_type , relationship_properties = list (props ), separate_property_columns = True
44+ rel_df = gds .v2 .graph .relationships .stream (
45+ G , relationship_types = [rel_type ], relationship_properties = list (props )
46+ )
47+
48+ # there was a bug in the v2 endpoints in GDS (1.22) where for dataframe would have the incorrect shape
49+ if "propertyValue" and "relationshipProperty" in rel_df .keys ():
50+ rel_df = rel_df .pivot (
51+ index = ["sourceNodeId" , "targetNodeId" , "relationshipType" ],
52+ columns = "relationshipProperty" ,
53+ values = "propertyValue" ,
4854 )
49- else :
50- rel_df = gds . graph . relationships . stream ( G , relationship_types = [ rel_type ])
55+ rel_df = rel_df . reset_index ()
56+ rel_df . columns . name = None
5157
5258 rel_dfs .append (rel_df )
5359
5460 return rel_dfs
5561
5662
5763def from_gds (
58- gds : GraphDataScience ,
59- G : Graph ,
64+ gds : GraphDataScience | AuraGraphDataScience ,
65+ G : Graph | GraphV2 ,
6066 node_properties : Optional [list [str ]] = None ,
6167 db_node_properties : Optional [list [str ]] = None ,
6268 max_node_count : int = 10_000 ,
@@ -76,9 +82,9 @@ def from_gds(
7682
7783 Parameters
7884 ----------
79- gds : GraphDataScience
80- GraphDataScience object.
81- G : Graph
85+ gds
86+ GraphDataScience object. AuraGraphDataScience object if using Aura Graph Analytics.
87+ G
8288 Graph object.
8389 node_properties : list[str], optional
8490 Additional properties to include in the visualization node, by default None which means that all node
@@ -91,50 +97,54 @@ def from_gds(
9197 """
9298 if db_node_properties is None :
9399 db_node_properties = []
100+ if isinstance (G , Graph ):
101+ G_v2 = gds .v2 .graph .get (G .name ())
102+ else :
103+ G_v2 = G
94104
95- node_properties_from_gds = G .node_properties ()
96- assert isinstance (node_properties_from_gds , pd .Series )
97- actual_node_properties : dict [str , list [str ]] = cast (dict [str , list [str ]], node_properties_from_gds .to_dict ())
98- all_actual_node_properties = list (chain .from_iterable (actual_node_properties .values ()))
105+ gds_properties_per_label = G_v2 .node_properties ()
106+ all_gds_properties = list (chain .from_iterable (gds_properties_per_label .values ()))
99107
100108 node_properties_by_label_sets : dict [str , set [str ]] = dict ()
101109 if node_properties is None :
102- node_properties_by_label_sets = {k : set (v ) for k , v in actual_node_properties .items ()}
110+ node_properties_by_label_sets = {k : set (v ) for k , v in gds_properties_per_label .items ()}
103111 else :
104112 for prop in node_properties :
105- if prop not in all_actual_node_properties :
113+ if prop not in all_gds_properties :
106114 raise ValueError (f"There is no node property '{ prop } ' in graph '{ G .name ()} '" )
107115
108- for label , props in actual_node_properties .items ():
116+ for label , props in gds_properties_per_label .items ():
109117 node_properties_by_label_sets [label ] = {
110- prop for prop in actual_node_properties [label ] if prop in node_properties
118+ prop for prop in gds_properties_per_label [label ] if prop in node_properties
111119 }
112120
113121 node_properties_by_label = {k : list (v ) for k , v in node_properties_by_label_sets .items ()}
114122
115- node_count = G .node_count ()
123+ node_count = G_v2 .node_count ()
116124 if node_count > max_node_count :
117125 warnings .warn (
118- f"The '{ G .name ()} ' projection's node count ({ G .node_count ()} ) exceeds `max_node_count` ({ max_node_count } ), so subsampling will be applied. Increase `max_node_count` if needed"
126+ f"The '{ G_v2 .name ()} ' projection's node count ({ G_v2 .node_count ()} ) exceeds `max_node_count` ({ max_node_count } ), so subsampling will be applied. Increase `max_node_count` if needed"
119127 )
120128 sampling_ratio = float (max_node_count ) / node_count
121129 sample_name = f"neo4j-viz_sample_{ uuid4 ()} "
122- G_fetched , _ = gds .graph .sample .rwr (sample_name , G , samplingRatio = sampling_ratio , nodeLabelStratification = True )
130+ G_fetched , _ = gds .v2 .graph .sample .rwr (
131+ G_v2 , sample_name , sampling_ratio = sampling_ratio , node_label_stratification = True
132+ )
123133 else :
124- G_fetched = G
134+ G_fetched = G_v2
125135
126136 property_name = None
127137 try :
128138 # Since GDS does not allow us to only fetch node IDs, we add the degree property
129139 # as a temporary property to ensure that we have at least one property for each label to fetch
130140 if sum ([len (props ) == 0 for props in node_properties_by_label .values ()]) > 0 :
131141 property_name = f"neo4j-viz_property_{ uuid4 ()} "
132- gds .degree . mutate (G_fetched , mutateProperty = property_name )
142+ gds .v2 . degree_centrality . mutate (G_fetched , mutate_property = property_name )
133143 for props in node_properties_by_label .values ():
134144 props .append (property_name )
135145
136146 node_dfs = _fetch_node_dfs (
137- gds , G_fetched , node_properties_by_label , G_fetched . node_labels (), db_node_properties
147+ gds , G_fetched , node_properties_by_label , node_properties_by_label . keys (), db_node_properties
138148 )
139149 if property_name is not None :
140150 for df in node_dfs .values ():
@@ -145,7 +155,7 @@ def from_gds(
145155 if G_fetched .name () != G .name ():
146156 G_fetched .drop ()
147157 elif property_name is not None :
148- gds .graph .nodeProperties .drop (G_fetched , node_properties = [property_name ])
158+ gds .v2 . graph .node_properties .drop (G_fetched , node_properties = [property_name ])
149159
150160 for df in node_dfs .values ():
151161 if property_name is not None and property_name in df .columns :
@@ -154,7 +164,7 @@ def from_gds(
154164 node_props_df = pd .concat (node_dfs .values (), ignore_index = True , axis = 0 ).drop_duplicates (subset = ["nodeId" ])
155165
156166 for lbl , df in node_dfs .items ():
157- if "labels" in all_actual_node_properties :
167+ if "labels" in all_gds_properties :
158168 df .rename (columns = {"labels" : "__labels" }, inplace = True )
159169 df ["labels" ] = lbl
160170
0 commit comments