Skip to content

Commit d47b875

Browse files
authored
Merge pull request #171 from neo4j/rels-only-df
Make node_dfs optional in from_dfs integration
2 parents 7562fba + f9731fc commit d47b875

File tree

4 files changed

+62
-12
lines changed

4 files changed

+62
-12
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
## New features
88

9+
* Allow visualization based only on relationship DataFrames, without specifying node DataFrames in `from_dfs`
910

1011
## Bug fixes
1112

docs/source/integration.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ The ``from_dfs`` method takes two mandatory positional parameters:
3838
on corresponding nodes under that field name.
3939
Otherwise, the column name will be a key in each node's `properties` dictionary, that maps to the node's corresponding
4040
value in the column.
41+
If the graph has no node properties, the nodes can be derived from the relationships DataFrame alone.
4142
* A Pandas ``DataFrame``, or iterable (eg. list) of DataFrames representing the relationships of the graph.
4243
The rows of the DataFrame(s) should represent the individual relationships, and the columns should represent the
4344
relationship IDs and attributes.

python-wrapper/src/neo4j_viz/pandas.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,40 @@
1313

1414

1515
def _from_dfs(
16-
node_dfs: DFS_TYPE,
16+
node_dfs: Optional[DFS_TYPE],
1717
rel_dfs: DFS_TYPE,
1818
node_radius_min_max: Optional[tuple[float, float]] = (3, 60),
1919
rename_properties: Optional[dict[str, str]] = None,
2020
) -> VisualizationGraph:
21+
relationships = _parse_relationships(rel_dfs, rename_properties=rename_properties)
22+
23+
if node_dfs is None:
24+
has_size = False
25+
node_ids = set()
26+
for rel in relationships:
27+
node_ids.add(rel.source)
28+
node_ids.add(rel.target)
29+
nodes = [Node(id=id) for id in node_ids]
30+
else:
31+
nodes, has_size = _parse_nodes(node_dfs, rename_properties=rename_properties)
32+
33+
VG = VisualizationGraph(nodes=nodes, relationships=relationships)
34+
35+
if node_radius_min_max is not None and has_size:
36+
VG.resize_nodes(node_radius_min_max=node_radius_min_max)
37+
38+
return VG
39+
40+
41+
def _parse_nodes(node_dfs: DFS_TYPE, rename_properties: Optional[dict[str, str]]) -> tuple[list[Node], bool]:
2142
if isinstance(node_dfs, DataFrame):
2243
node_dfs_iter: Iterable[DataFrame] = [node_dfs]
44+
elif node_dfs is None:
45+
node_dfs_iter = []
2346
else:
2447
node_dfs_iter = node_dfs
2548

2649
all_node_field_aliases = Node.all_validation_aliases()
27-
all_rel_field_aliases = Relationship.all_validation_aliases()
2850

2951
has_size = True
3052
nodes = []
@@ -42,13 +64,18 @@ def _from_dfs(
4264
properties[key] = value
4365

4466
nodes.append(Node(**top_level, properties=properties))
67+
return nodes, has_size
68+
69+
70+
def _parse_relationships(rel_dfs: DFS_TYPE, rename_properties: Optional[dict[str, str]]) -> list[Relationship]:
71+
all_rel_field_aliases = Relationship.all_validation_aliases()
4572

4673
if isinstance(rel_dfs, DataFrame):
4774
rel_dfs_iter: Iterable[DataFrame] = [rel_dfs]
4875
else:
4976
rel_dfs_iter = rel_dfs
77+
relationships: list[Relationship] = []
5078

51-
relationships = []
5279
for rel_df in rel_dfs_iter:
5380
for _, row in rel_df.iterrows():
5481
top_level = {}
@@ -62,17 +89,11 @@ def _from_dfs(
6289
properties[key] = value
6390

6491
relationships.append(Relationship(**top_level, properties=properties))
65-
66-
VG = VisualizationGraph(nodes=nodes, relationships=relationships)
67-
68-
if node_radius_min_max is not None and has_size:
69-
VG.resize_nodes(node_radius_min_max=node_radius_min_max)
70-
71-
return VG
92+
return relationships
7293

7394

7495
def from_dfs(
75-
node_dfs: DFS_TYPE,
96+
node_dfs: Optional[DFS_TYPE],
7697
rel_dfs: DFS_TYPE,
7798
node_radius_min_max: Optional[tuple[float, float]] = (3, 60),
7899
) -> VisualizationGraph:
@@ -85,8 +106,9 @@ def from_dfs(
85106
86107
Parameters
87108
----------
88-
node_dfs: Union[DataFrame, Iterable[DataFrame]]
109+
node_dfs: Optional[Union[DataFrame, Iterable[DataFrame]]]
89110
DataFrame or iterable of DataFrames containing node data.
111+
If None, the nodes will be created from the source and target node ids in the rel_dfs.
90112
rel_dfs: Union[DataFrame, Iterable[DataFrame]]
91113
DataFrame or iterable of DataFrames containing relationship data.
92114
node_radius_min_max : tuple[float, float], optional

python-wrapper/tests/test_pandas.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pandas import DataFrame
22
from pydantic_extra_types.color import Color
33

4+
from neo4j_viz.node import Node
45
from neo4j_viz.pandas import from_dfs
56

67

@@ -45,6 +46,31 @@ def test_from_df() -> None:
4546
assert VG.relationships[1].properties == {"weight": 2.0}
4647

4748

49+
def test_from_rel_dfs() -> None:
50+
relationships = [
51+
DataFrame(
52+
{
53+
"source": [0, 1],
54+
"target": [1, 0],
55+
"caption": ["REL", "REL2"],
56+
"weight": [1.0, 2.0],
57+
}
58+
),
59+
DataFrame(
60+
{
61+
"source": [2, 3],
62+
"target": [1, 0],
63+
"caption": ["REL", "REL2"],
64+
"weight": [1.0, 2.0],
65+
}
66+
),
67+
]
68+
VG = from_dfs(None, relationships)
69+
70+
assert len(VG.relationships) == 4
71+
assert VG.nodes == [Node(id=id) for id in [0, 1, 2, 3]]
72+
73+
4874
def test_from_dfs() -> None:
4975
nodes = [
5076
DataFrame(

0 commit comments

Comments
 (0)