Skip to content

Commit 1e01b0f

Browse files
committed
Add set_node_caption convenience method
ref GDS-59
1 parent 557d21f commit 1e01b0f

File tree

4 files changed

+272
-3
lines changed

4 files changed

+272
-3
lines changed

changelog.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
## Breaking changes
44

5-
* Removed `table` property from nodes and relationships returned from `from_snowflake`, the table is represented by the `caption` field.
6-
* Changed default value of `override` parameter in `VisualizationGraph.color_nodes()` from `False` to `True`. The method now overrides existing node colors by default. To preserve existing colors, explicitly pass `override=False`.
5+
- Removed `table` property from nodes and relationships returned from `from_snowflake`, the table is represented by the `caption` field.
6+
- Changed default value of `override` parameter in `VisualizationGraph.color_nodes()` from `False` to `True`. The method now overrides existing node colors by default. To preserve existing colors, explicitly pass `override=False`.
77

88
## New features
99

10+
- Added `set_node_captions()` convenience method to `VisualizationGraph` for setting node captions from a field or property.
1011

1112
## Bug fixes
1213

1314
## Improvements
1415

15-
1616
## Other changes

docs/source/customizing.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,45 @@ If you have not yet created a ``VisualizationGraph`` object, please refer to one
2020
:backlinks: none
2121

2222

23+
Setting node captions
24+
---------------------
25+
26+
Node captions are the text labels displayed on nodes in the visualization.
27+
28+
The ``set_node_captions`` method
29+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30+
31+
By calling the :meth:`neo4j_viz.VisualizationGraph.set_node_captions` method, you can set node captions based on a
32+
node field (like ``id``, ``size``, etc.) or a node property (members of the ``Node.properties`` map).
33+
34+
The method accepts an ``override`` parameter (default ``True``) that controls whether to replace existing captions.
35+
If ``override=False``, only nodes without captions will be updated.
36+
37+
Here's an example of setting node captions from a property:
38+
39+
.. code-block:: python
40+
41+
# VG is a VisualizationGraph object with nodes that have a "name" property
42+
VG.set_node_captions(property="name")
43+
44+
You can also set captions from a node field, and choose not to override existing captions:
45+
46+
.. code-block:: python
47+
48+
# VG is a VisualizationGraph object
49+
VG.set_node_captions(field="id", override=False)
50+
51+
For more complex scenarios where you need fallback logic or want to combine multiple properties, you can iterate over
52+
nodes directly:
53+
54+
.. code-block:: python
55+
56+
# VG is a VisualizationGraph object
57+
for node in VG.nodes:
58+
caption = node.properties.get("name") or node.properties.get("title") or node.id
59+
node.caption = str(caption)
60+
61+
2362
Coloring nodes
2463
--------------
2564

python-wrapper/src/neo4j_viz/visualization_graph.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,75 @@ def toggle_nodes_pinned(self, pinned: dict[NodeIdType, bool]) -> None:
194194

195195
node.pinned = node_pinned
196196

197+
def set_node_captions(
198+
self,
199+
*,
200+
field: Optional[str] = None,
201+
property: Optional[str] = None,
202+
override: bool = True,
203+
) -> None:
204+
"""
205+
Set the caption for nodes in the graph based on either a node field or a node property.
206+
207+
Parameters
208+
----------
209+
field:
210+
The field of the nodes to use as the caption. Must be None if `property` is provided.
211+
property:
212+
The property of the nodes to use as the caption. Must be None if `field` is provided.
213+
override:
214+
Whether to override existing captions of the nodes, if they have any.
215+
216+
Examples
217+
--------
218+
Given a VisualizationGraph `VG`:
219+
220+
>>> nodes = [
221+
... Node(id="0", properties={"name": "Alice", "age": 30}),
222+
... Node(id="1", properties={"name": "Bob", "age": 25}),
223+
... ]
224+
>>> VG = VisualizationGraph(nodes=nodes)
225+
226+
Set node captions from a property:
227+
228+
>>> VG.set_node_captions(property="name")
229+
230+
Set node captions from a field, only if not already set:
231+
232+
>>> VG.set_node_captions(field="id", override=False)
233+
234+
Set captions from multiple properties with fallback:
235+
236+
>>> for node in VG.nodes:
237+
... caption = node.properties.get("name") or node.properties.get("title") or node.id
238+
... if override or node.caption is None:
239+
... node.caption = str(caption)
240+
"""
241+
if not ((field is None) ^ (property is None)):
242+
raise ValueError(
243+
f"Exactly one of the arguments `field` (received '{field}') and `property` (received '{property}') must be provided"
244+
)
245+
246+
if property:
247+
# Use property
248+
for node in self.nodes:
249+
if not override and node.caption is not None:
250+
continue
251+
252+
value = node.properties.get(property, "")
253+
node.caption = str(value)
254+
else:
255+
# Use field
256+
assert field is not None
257+
attribute = to_snake(field)
258+
259+
for node in self.nodes:
260+
if not override and node.caption is not None:
261+
continue
262+
263+
value = getattr(node, attribute, "")
264+
node.caption = str(value)
265+
197266
def resize_nodes(
198267
self,
199268
sizes: Optional[dict[NodeIdType, RealNumber]] = None,
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import pytest
2+
3+
from neo4j_viz import Node, VisualizationGraph
4+
5+
6+
def test_set_node_captions_from_property() -> None:
7+
"""Test setting captions from a node property."""
8+
nodes = [
9+
Node(id="1", properties={"name": "Alice", "age": 30}),
10+
Node(id="2", properties={"name": "Bob", "age": 25}),
11+
Node(id="3", properties={"name": "Charlie", "age": 35}),
12+
]
13+
VG = VisualizationGraph(nodes=nodes, relationships=[])
14+
15+
VG.set_node_captions(property="name")
16+
17+
assert VG.nodes[0].caption == "Alice"
18+
assert VG.nodes[1].caption == "Bob"
19+
assert VG.nodes[2].caption == "Charlie"
20+
21+
22+
def test_set_node_captions_from_field() -> None:
23+
"""Test setting captions from a node field."""
24+
nodes = [
25+
Node(id="node-1", properties={"name": "Alice"}),
26+
Node(id="node-2", properties={"name": "Bob"}),
27+
Node(id="node-3", properties={"name": "Charlie"}),
28+
]
29+
VG = VisualizationGraph(nodes=nodes, relationships=[])
30+
31+
VG.set_node_captions(field="id")
32+
33+
assert VG.nodes[0].caption == "node-1"
34+
assert VG.nodes[1].caption == "node-2"
35+
assert VG.nodes[2].caption == "node-3"
36+
37+
38+
def test_set_node_captions_override_true() -> None:
39+
"""Test that override=True replaces existing captions."""
40+
nodes = [
41+
Node(id="1", caption="OldCaption1", properties={"name": "Alice"}),
42+
Node(id="2", caption="OldCaption2", properties={"name": "Bob"}),
43+
Node(id="3", properties={"name": "Charlie"}),
44+
]
45+
VG = VisualizationGraph(nodes=nodes, relationships=[])
46+
47+
VG.set_node_captions(property="name", override=True)
48+
49+
assert VG.nodes[0].caption == "Alice"
50+
assert VG.nodes[1].caption == "Bob"
51+
assert VG.nodes[2].caption == "Charlie"
52+
53+
54+
def test_set_node_captions_override_false() -> None:
55+
"""Test that override=False preserves existing captions."""
56+
nodes = [
57+
Node(id="1", caption="ExistingCaption", properties={"name": "Alice"}),
58+
Node(id="2", properties={"name": "Bob"}),
59+
Node(id="3", caption="AnotherCaption", properties={"name": "Charlie"}),
60+
]
61+
VG = VisualizationGraph(nodes=nodes, relationships=[])
62+
63+
VG.set_node_captions(property="name", override=False)
64+
65+
assert VG.nodes[0].caption == "ExistingCaption" # Not overridden
66+
assert VG.nodes[1].caption == "Bob" # Set (was None)
67+
assert VG.nodes[2].caption == "AnotherCaption" # Not overridden
68+
69+
70+
def test_set_node_captions_missing_property() -> None:
71+
"""Test behavior when property is missing from some nodes."""
72+
nodes = [
73+
Node(id="1", properties={"name": "Alice"}),
74+
Node(id="2", properties={"age": 25}), # No "name" property
75+
Node(id="3", properties={"name": "Charlie"}),
76+
]
77+
VG = VisualizationGraph(nodes=nodes, relationships=[])
78+
79+
VG.set_node_captions(property="name")
80+
81+
assert VG.nodes[0].caption == "Alice"
82+
assert VG.nodes[1].caption == "" # Empty string for missing property
83+
assert VG.nodes[2].caption == "Charlie"
84+
85+
86+
def test_set_node_captions_numeric_property() -> None:
87+
"""Test setting captions from numeric properties."""
88+
nodes = [
89+
Node(id="1", properties={"score": 100}),
90+
Node(id="2", properties={"score": 200}),
91+
Node(id="3", properties={"score": 300}),
92+
]
93+
VG = VisualizationGraph(nodes=nodes, relationships=[])
94+
95+
VG.set_node_captions(property="score")
96+
97+
assert VG.nodes[0].caption == "100"
98+
assert VG.nodes[1].caption == "200"
99+
assert VG.nodes[2].caption == "300"
100+
101+
102+
def test_set_node_captions_field_and_property_both_provided() -> None:
103+
"""Test that providing both field and property raises an error."""
104+
nodes = [Node(id="1", properties={"name": "Alice"})]
105+
VG = VisualizationGraph(nodes=nodes, relationships=[])
106+
107+
with pytest.raises(ValueError, match="Exactly one of the arguments"):
108+
VG.set_node_captions(field="id", property="name")
109+
110+
111+
def test_set_node_captions_neither_field_nor_property() -> None:
112+
"""Test that providing neither field nor property raises an error."""
113+
nodes = [Node(id="1", properties={"name": "Alice"})]
114+
VG = VisualizationGraph(nodes=nodes, relationships=[])
115+
116+
with pytest.raises(ValueError, match="Exactly one of the arguments"):
117+
VG.set_node_captions()
118+
119+
120+
def test_set_node_captions_field_with_snake_case() -> None:
121+
"""Test that field names are converted to snake_case."""
122+
nodes = [
123+
Node(id="1", caption_size=1, properties={"name": "Alice"}),
124+
Node(id="2", caption_size=2, properties={"name": "Bob"}),
125+
]
126+
VG = VisualizationGraph(nodes=nodes, relationships=[])
127+
128+
VG.set_node_captions(field="captionSize")
129+
130+
assert VG.nodes[0].caption == "1"
131+
assert VG.nodes[1].caption == "2"
132+
133+
134+
def test_set_node_captions_empty_graph() -> None:
135+
"""Test setting captions on an empty graph."""
136+
VG = VisualizationGraph(nodes=[], relationships=[])
137+
138+
# Should not raise an error
139+
VG.set_node_captions(property="name")
140+
141+
assert len(VG.nodes) == 0
142+
143+
144+
def test_set_node_captions_complex_property_values() -> None:
145+
"""Test setting captions from properties with complex types."""
146+
nodes = [
147+
Node(id="1", properties={"tags": ["tag1", "tag2"]}),
148+
Node(id="2", properties={"metadata": {"key": "value"}}),
149+
Node(id="3", properties={"value": None}),
150+
]
151+
VG = VisualizationGraph(nodes=nodes, relationships=[])
152+
153+
VG.set_node_captions(property="tags")
154+
assert VG.nodes[0].caption == "['tag1', 'tag2']"
155+
assert VG.nodes[1].caption == ""
156+
assert VG.nodes[2].caption == ""
157+
158+
VG.set_node_captions(property="metadata")
159+
assert VG.nodes[0].caption == ""
160+
assert VG.nodes[1].caption == "{'key': 'value'}"
161+
assert VG.nodes[2].caption == ""

0 commit comments

Comments
 (0)